Native CMP

Documentation

Webstudio Native CMP

Everything you need to install, configure, translate, style and extend the consent manager. Looking for ready-made service setups? See the examples.

Overview

Webstudio Native CMP is a consent manager for cookies, third-party scripts and embeds, built from regular Webstudio building blocks. There is no external service, dashboard or vendor script: everything lives in your project and can be edited in the Builder.

  • Engine: a small script in Project Settings → Custom Code. It reads the stored decision before the page paints, blocks and activates third-party code, deletes cookies and keeps everything in sync during client-side navigation.
  • Consent Manager Slot: the notice, preferences modal, purposes and switches as native instances, shared across all pages.
  • Variables: consentServices describes what each service does technically; consentTranslations holds all copy per language.
  • Attributes: behavior is wired with data-cmp-* attributes, so any element you design can open the settings, show a status or wait for consent.
  • Craft tokens: every part of the UI is a composite Token backed by semantic CSS variables.

The CMP gives you technical building blocks for a GDPR- and ePrivacy-friendly setup: prior blocking, granular purposes, equally prominent choices, revocation and cookie cleanup. Whether a site is compliant also depends on its texts, services and privacy policy.

Installation

Using an AI agent? Claude Code, Codex, Cursor or any agent connected to the Webstudio MCP can set everything up for you: point it to nativecmp.com/install.txt. The guide includes an installer that adds the engine, tokens and the Consent Manager Slot to your project.

1. Add the engine

Open Project Settings → Custom Code and paste the CMP snippet at the very top, before any analytics or tag manager code. It contains three parts:

Custom Code

<!-- Webstudio Native CMP: configuration -->
<script>
window.cmpConfig = {
storageName: "cmp_consent",
cookieExpiresAfterDays: 365,
};
</script>
<style data-cmp-critical>/* critical CSS */</style>
<script data-cmp-engine="1.1.0">/* engine */</script>
<!-- Consent-managed head scripts go below this line -->

2. Add the Consent Manager Slot

Copy the Consent Manager Slot to every page. On new projects, add it to your page template so new pages include it automatically. The Slot is shared: editing it on one page updates every page.

3. Configure services and copy

Select the Consent Manager root instance and edit the consentServices and consentTranslations variables in the Data panel. The examples contain ready-made entries for the most common services.

4. Block third-party code

Change existing scripts and embeds so they only run with consent (see Blocking scripts & embeds) and add a Privacy settings link to your footer with data-cmp-action="open-modal".

5. Publish and test

Open the site in a private window. The notice must appear, nothing tracked should load before a decision, and your choice must survive reloads and page changes. cmp.getConsents() in the browser console shows the current state.

How it works

  • Before the first paint: the engine runs in <head>, reads the cmp_consent cookie and sets state attributes on <html>. The critical CSS uses them to show or hide the notice immediately, so returning visitors never see a flash.
  • After the page is parsed: the engine reads the service definitions from the Slot, validates the stored decision, activates allowed scripts and embeds, and renders switches, gates and status indicators.
  • During client-side navigation: Webstudio remounts page content on every route change. A MutationObserver notices new consent-related elements and re-syncs them. Consent state lives in the engine, not in the page, so it survives navigation.
  • No double execution: React may re-create embed content (for example right after hydration). The engine remembers what it has already executed per embed container, so scripts never run twice.

Effective consent

A service is active when it is required, when the visitor accepted it once in a gate, or when it has consent and the decision is confirmed. Services marked optOut also run before a decision, until the visitor declines them.

Changed services

If the service list changes after a visitor decided, the stored decision becomes unconfirmed: the notice opens again with the “services changed” message, and non-required services stay blocked until the visitor decides again.

Configuration

Set options in window.cmpConfig at the top of the Custom Code snippet. All options are optional.

OptionDefaultDescription
storage"cookie"Where the decision is stored: "cookie" or "localStorage".
storageName"cmp_consent"Cookie or localStorage key.
cookieExpiresAfterDays365Lifetime of the consent cookie.
cookieDomain""Set e.g. ".example.com" to share consent across subdomains.
cookiePath"/"Path of the consent cookie.
defaultfalsePreselection for services without their own default.
mustConsentfalseOpen the preferences modal instead of the notice. It can’t be closed until the visitor decides.
noNoticefalseContextual consent only: no notice on page load. Consent Gates, Privacy settings links and the JavaScript API ask for consent. See Contextual consent only.
confirmDelay800Milliseconds the modal stays visible after Accept all / Decline all, so visitors see the switches change.
consentModenullGoogle Consent Mode v2 mapping, see Google Consent Mode & GTM.
consentModeDefaultsnullExtra fields for gtag("consent", "default", …), e.g. { security_storage: "granted" }.
dataLayerfalsePush { event: "cmp_consent", cmp_type, cmp_consents } to window.dataLayer on every decision.
fallbackLanguage""Language shown when no translation matches the page. Defaults to the first entry.
debugfalseLog service definitions and cookie deletions to the console.

Set noNotice: true if you only want to protect embeds such as YouTube or Google Maps and don’t need a notice on page load. The generator calls this Contextual consent only. Visitors allow a provider right where its content appears: Load content allows it for the current page, Always allow stores the decision straight away. The Privacy settings link still opens the full preferences.

In this mode the root gets data-cmp-mode="contextual", and Always allow buttons are visible before any decision. Head scripts such as analytics stay blocked until visitors enable them in the preferences, so use it for sites without consent-based tracking. mustConsent takes precedence if both are set.

Stored value

The cookie contains URL-encoded JSON: { "consents": { "google-analytics": true, … }, "timestamp": "2026-09-13T10:00:00.000Z", "version": 1 }. The timestamp documents when the decision was made.

Services

consentServices is a JSON array on the Consent Manager root. It is rendered as a hidden Service Registry that the engine reads.

consentServices

[
{ "name": "consent-manager", "required": true },
{ "name": "google-analytics", "cookies": ["^_ga", "_gid", "^_gat"] },
{ "name": "youtube" },
{ "name": "chat", "optOut": true, "dependsOn": ["google-analytics"] }
]
FieldTypeDescription
namestringUnique key. Referenced by data-cmp-service, data-cmp-gate, data-cmp-status and translations. Use lowercase and dashes.
requiredbooleanAlways active. The switch is disabled and the “Always active” badge is shown.
defaultbooleanPreselected in the preferences modal before the visitor decides.
optOutbooleanRuns before a decision, until the visitor declines. Shows “Active by default”.
cookiesstring[]Cookies deleted when consent is missing or revoked. Patterns starting with ^ are regular expressions (^_ga matches _ga_ABC123); other values match exactly. No spaces inside a pattern.
onlyOncebooleanAccept scripts run at most once per page load, even across client-side navigation.
contextualOnlybooleanIgnored by Accept all / Decline all. Only its switch or a Consent Gate changes it.
dependsOnstring[]Enabling this service enables the listed services; disabling one of them disables this service.

Cookie deletion works for first-party cookies the browser lets JavaScript see: the current host, the host with a leading dot and every parent domain, on path / and the current path. HttpOnly cookies and cookies on third-party domains can’t be removed from the page.

Languages & translations

consentTranslations contains one entry per language. The Slot renders one notice and modal per entry and the engine shows the one matching the page language from Page Settings → Language (rendered as <html lang>).

consentTranslations

[
{
"lang": "en",
"dir": "ltr",
"privacyLabel": "privacy policy",
"privacyUrl": "/privacy",
"notice": {
"title": "We value your privacy",
"text": "We use cookies … Learn more in our",
"changes": "Our services have changed since your last visit.",
"customize": "Customize", "decline": "Decline all", "accept": "Accept all"
},
"modal": {
"title": "Privacy settings", "text": "Choose which services we may use …",
"close": "Close privacy settings", "toggleAll": "Enable or disable all services",
"alwaysActive": "Always active", "activeByDefault": "Active by default",
"service": "service", "services": "services",
"decline": "Decline all", "save": "Save selection", "accept": "Accept all"
},
"purposes": [
{
"id": "analytics", "title": "Analytics", "description": "…",
"services": [
{ "name": "google-analytics", "title": "Google Analytics", "description": "…" }
]
}
]
}
]

Language matching

  • Exact match, case-insensitive: de-ATde-at.
  • Primary language: de-ATde, or a regional entry of the same language.
  • cmpConfig.fallbackLanguage, otherwise the first entry.

The language updates live when client-side navigation changes <html lang>. Consent is shared across languages. Purposes group services in the modal; a purpose switch toggles all its non-required services and shows a mixed state when only some are active.

Any language

  • Add an entry with any language code: fr, pt-BR, tr, fa, …. The engine doesn’t need a list of supported languages.
  • The generator includes built-in notice, modal, purpose and gate texts and descriptions for all 23 catalog services in English, German, Spanish, French, Italian, Portuguese, Dutch, Polish, Arabic and Hebrew.
  • For other codes the generator starts from English and marks every field English – translate in its translation editor. The editor also imports translations from pasted configurations.

Right-to-left languages

Each entry may set "dir": "rtl" or "ltr". Without it, the Consent Manager detects right-to-left from the language code: ar, arc, ckb, dv, fa, he, ku, ps, sd, ug, ur and yi. The language wrapper gets the matching dir attribute, so the notice, modal and switches mirror automatically.

  • Layouts use flex and grid, which follow the writing direction.
  • Tokens that need explicit mirroring use logical properties or the :dir(rtl) state. For example, consent-switch mirrors its thumb and consent-modal-close uses margin-inline-end.
  • Set dir="rtl" on the Body of right-to-left pages too, so your own page content mirrors as well. See the Arabic demo.

Other translated elements

Add data-cmp-lang="de" to any element, for example a footer link in a shared Slot, to show it only on German pages.

Blocking scripts & embeds

Mark third-party code with data-cmp-service. Put it in Custom Code (runs once per visit) or an HTML Embed (runs on the pages that contain it).

External scripts

HTML

<script type="text/plain" data-cmp-service="google-analytics"
data-src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>

type="text/plain" stops the browser from running the script. On consent the engine inserts a real copy into <head> with all other attributes (id, data-*, nonce, async). Use data-type to restore a type such as module. The same external URL is only loaded once per page load.

Inline scripts and decline handlers

HTML

<script type="text/plain" data-cmp-service="google-analytics">
gtag("config", "G-XXXX");
</script>
<script type="text/plain" data-cmp-service="google-analytics" data-cmp-on="decline">
window["ga-disable-G-XXXX"] = true;
</script>

Inline scripts run when consent is granted, again after a revoke followed by a new consent, and once for every page that renders them. data-cmp-on="decline" runs a script when the service is denied or revoked.

Templates for iframes and widgets

HTML Embed

<template data-cmp-service="youtube">
<iframe src="https://www.youtube-nocookie.com/embed/VIDEO_ID" title="Video"></iframe>
</template>

Everything inside a <template> is inert. On consent the content is inserted after the template (scripts go to <head>); on revoke it is removed again. This is the most robust option for iframes, widgets and social embeds.

Attribute swapping

HTML

<iframe data-cmp-service="vimeo" data-src="https://player.vimeo.com/video/ID"></iframe>
<img data-cmp-service="tracker" data-src="https://example.com/pixel.gif" alt="">
<link rel="stylesheet" data-cmp-service="google-fonts" data-href="https://fonts.googleapis.com/…">

For other elements the engine copies data-src, data-srcset, data-href, data-poster and data-data into the real attributes on consent and removes them on revoke (iframes switch to about:blank). Iframes, images, video, audio, embed and object elements stay hidden until granted. Stylesheet links are added to and removed from <head>.

For YouTube and Vimeo, use Webstudio’s native components inside an interaction-mode Consent Gate (see Consent Gates). Turn off Show preview, Autoplay and Preconnect first: with those settings on, the components contact YouTube or Vimeo as soon as the page loads.

Element state

Every managed element gets data-cmp-state="granted" or "denied", which you can use for styling.

Consent Gates

A Consent Gate lets visitors allow a single service right where its content appears. Gates have two modes.

Placeholder mode (default)

The content stays hidden behind a designed placeholder until the visitor allows the service. Use it for HTML embeds such as maps, social posts and widgets.

Structure

<div data-cmp-gate="google-maps">                 <!-- consent-gate -->
<div data-cmp-gate-notice> <!-- visible until consent -->
<p>Google Maps</p>
<button data-cmp-action="accept-once">Load content</button>
<button data-cmp-action="accept-always" data-cmp-if="confirmed">Always allow Google Maps</button>
<button data-cmp-action="open-modal">Privacy settings</button>
</div>
<div data-cmp-gate-content> <!-- HTML Embed with a template -->
<template data-cmp-service="google-maps">…</template>
</div>
</div>

Interaction mode

The content stays visible and works like a normal component until someone interacts with it. The first click inside data-cmp-gate-content is held back and the notice appears on top. After Load content or Always allow, the original click continues automatically, so a video starts playing right away.

Structure

<div data-cmp-gate="youtube" data-cmp-gate-mode="interaction">   <!-- consent-gate -->
<div data-cmp-gate-notice> <!-- consent-gate-notice + is-consent-gate-overlay -->
<p>YouTube video</p>
<button data-cmp-action="accept-once">Load content</button>
<button data-cmp-action="accept-always" data-cmp-if="confirmed">Always allow YouTube</button>
<button data-cmp-action="dismiss">Cancel</button>
<button data-cmp-action="open-modal">Privacy settings</button>
</div>
YouTube component with data-cmp-gate-content <!-- consent-video -->
</div>
  • Focus moves into the notice when it opens. Cancel (dismiss) or Escape closes it and returns focus to the element that was clicked.
  • While the notice is open, the gate has data-cmp-requested and grows to at least 300 px (400 px on phones), so the notice never gets cut off.
  • Visitors who already allowed the service interact with the content directly.
  • If consent is revoked while a native player is loaded, the engine stops it (about:blank). It can be played again after the next page load.
  • Interaction mode works for anything that only loads on click, for example your own Load map button.

Webstudio’s YouTube and Vimeo components

Put the component inside an interaction-mode gate, add data-cmp-gate-content to it in Settings → Attributes, and use these settings:

SettingYouTubeVimeoWhy
Show previewOffOffOn loads the thumbnail from img.youtube.com, or calls vimeo.com/api and loads from vimeocdn.com.
AutoplayOffOffOn loads the player immediately.
PreconnectOffOn opens a connection to YouTube on page load.
Privacy enhanced modeOnUses youtube-nocookie.com once the player loads.
Do not trackOnAsks Vimeo not to track the session.
Preview ImageYour own uploadYour own uploadSelf-hosted thumbnails need no consent.

The generator copies ready-made YouTube and Vimeo gates with these settings and your video URL. The tests confirm that no request reaches YouTube, Vimeo or their image servers before consent.

Accept once and always

  • Load content (accept-once) grants consent for the current page view only; nothing is stored.
  • Always allow (accept-always) stores consent for this service. It is only offered after the visitor made a first decision, so a single click in a gate never counts as a decision about all other services.
  • Buttons outside a gate can target a service with data-cmp-name="youtube".

Attribute reference

Actions

Value of data-cmp-actionEffect
accept-allGrant all services (except contextualOnly), save and close.
decline-allDeny all non-required services, save and close.
saveSave the current switch positions and close.
open-modalOpen the preferences modal. Works on any element on any page.
closeClose the modal. Unsaved switch changes are discarded for returning visitors. Ignored when mustConsent requires a decision.
accept-onceAllow the service of the closest gate for this page view.
accept-alwaysAllow and store the service of the closest gate.
dismissClose an interaction-mode gate notice without deciding.
resetDelete the stored decision and show the notice again.

Structure and bindings

AttributeUsed onPurpose
data-cmp-noticeNotice regionShown while the notice is open.
data-cmp-modalModal wrapperShown while the preferences modal is open.
data-cmp-dialogDialog elementReceives focus; Tab is trapped inside.
data-cmp-toggleCheckboxservice:<name>, purpose:<id> or all.
data-cmp-purposePurpose rowGroups service rows for purpose switches and badges.
data-cmp-service-itemService rowConnects a translated row to a service.
data-cmp-service-defRegistry itemTechnical service definition (rendered from consentServices).
data-cmp-serviceScript, template, link, iframe…Blocked until the service is granted.
data-cmp-onScriptaccept (default) or decline.
data-cmp-gateGate wrapperService name of a Consent Gate.
data-cmp-gate-modeGate wrapperinteraction: keep the content visible and hold back the first click until consent.
data-cmp-gate-notice / data-cmp-gate-contentGate childrenNotice and gated content.
data-cmp-requestedGate wrapper (set by the engine)An interaction-mode notice is open.
data-cmp-statusAny elementLive status of a service; sets data-cmp-state.
data-cmp-langAny elementOnly shown when it matches the page language.
data-cmp-previewDesign framenotice or modal: shows the fixed UI inline for designing.

Conditional visibility

data-cmp-ifVisible when
confirmed / unconfirmedThe visitor has (not) made a decision.
changedThe service list changed since the stored decision.
required / not-requiredThe closest service or purpose is (not) required.
opt-outThe closest service is opt-out.
granted / deniedThe closest service, status or gate is (not) active.

State on <html>

AttributeValues
data-cmploading until the page is parsed, then ready.
data-cmp-opennotice or modal while one is open.
data-cmp-confirmedtrue or false.
data-cmp-changedPresent when services changed.
data-cmp-confirmingPresent during the confirm delay.
data-cmp-languageResolved language, e.g. de.

JavaScript API & events

JavaScript

cmp.show();                          // open the preferences modal
cmp.showNotice(); cmp.hide();
cmp.getConsent("youtube"); // effective consent: true / false
cmp.getConsents(); // { "google-analytics": true, … }
cmp.isConfirmed(); // visitor has decided
cmp.getServices(); // parsed service definitions
cmp.getLanguage(); // "en"
cmp.setConsent("youtube", true); // change and save (pass false as 3rd argument to skip saving)
cmp.acceptAll(); cmp.declineAll(); cmp.save(); cmp.reset();
cmp.refresh(); // re-sync after custom DOM changes

const off = cmp.on("save", ({ type, consents, changes }) => {});
document.addEventListener("cmp:service", (event) => console.log(event.detail));
EventDetailFires when
readyconsentsService definitions were read after the first page load.
changeconsentsA switch changed (not yet saved).
save{ type, consents, changes }A decision was stored. type is accept, decline, save, contextual-accept or api.
service{ name, consent, service }A service’s effective consent changed.
init{ service }A service was seen for the first time.
modal{ open }The preferences modal opened or closed.
gate{ name, requested }An interaction-mode gate held back a click and opened its notice.

Every event is also dispatched on document as cmp:<event>.

Design & Craft tokens

All styles follow the Craft architecture: theme variables → semantic variables → composite Tokens. Restyle everything on Global Root, or edit a single Token in the Style panel.

Theme variables

VariableDefaultUsed for
--theme-background#ffffffNotice, modal and card surfaces
--theme-surface#f3f4f6Service rows, footer, gates
--theme-foreground#15171cPrimary text, scrim, overlays
--theme-foreground-muted#4d5461Descriptions
--theme-border#dde0e5Separators
--theme-control#6f7582Switch track, secondary button border
--theme-accent#1d4ed8Primary buttons, active switches, links, focus
--theme-on-accent#ffffffText on accent
--theme-warning#9a4a00“Services changed” message

Semantic variables

--foreground-primary, --foreground-secondary, --foreground-disabled, --foreground-accent, --foreground-on-accent, --foreground-warning, --foreground-inverse, --background-primary, --background-secondary, --background-disabled, --background-accent, --background-control, --background-inverse, --border-default, --border-control, --border-focus, --overlay-scrim, --overlay-interaction-hover, --overlay-interaction-pressed, --overlay-on-accent-hover, --overlay-on-accent-pressed, --gap-xs|s|m|l, --spacing-default, --focus-width, --focus-offset, --duration-default, --easing-default, --radius-small, --radius-default, --radius-full, --shadow-overlay, --layer-overlay, --width-content.

Composite tokens

BlockElements and variants
consent-noticeconsent-notice-body, consent-notice-title, consent-notice-text, consent-notice-changes, consent-notice-actions
consent-modalconsent-modal-scrim, consent-modal-dialog, consent-modal-header, consent-modal-title, consent-modal-close, consent-modal-text, consent-modal-body, consent-modal-footer
consent-purposesconsent-purpose, is-consent-purpose-all, consent-purpose-header, consent-purpose-label, consent-purpose-description, consent-purpose-services, consent-purpose-summary
consent-servicesconsent-service, consent-service-header, consent-service-label, consent-service-description
consent-buttonis-consent-button-primary, is-consent-button-secondary, is-consent-button-ghost
consent-switchStates: :checked, :indeterminate, :disabled, :focus-visible, ::before (thumb)
consent-gateconsent-gate-notice, consent-gate-title, consent-gate-text, consent-gate-actions, consent-gate-content
consent-statusconsent-status-label, is-consent-status-label-granted
Othersconsent, consent-language, consent-link, consent-badge, consent-preview, is-consent-preview-modal

Designing on the canvas

Scripts don’t run in the Builder, so the fixed notice and modal are hidden on regular pages and only the first language is shown. Use the draft page /consent-preview: its frames (data-cmp-preview) show the notice and modal inline. Changes apply to the shared Slot everywhere.

Accessibility

  • Default color pairs meet WCAG 2.2 AA: body text ≥ 6.8:1, accent buttons 7.1:1, switch tracks ≥ 3.7:1 against their backgrounds.
  • The modal is a role="dialog" with aria-modal, labelled by its title and described by its text. Focus moves into it, Tab is trapped, Escape closes it and focus returns to the opener.
  • Switches are native checkboxes with role="switch"; purpose switches expose a mixed state. Every switch has a visible label and a description.
  • Required and opt-out states are communicated with text badges, not color alone. Focus rings use --border-focus.
  • The notice is a labelled region that doesn’t steal focus, so keyboard and screen reader users can keep reading the page.

Automated audits may report that role="switch" needs aria-checked. For native checkboxes the checked state is exposed by the browser, and adding a static aria-checked would be wrong.

Troubleshooting

SymptomCheck
The notice never appearsIs the CMP snippet in Custom Code and is the Consent Manager Slot on the page? Was a decision already stored? Run cmp.reset().
A script doesn’t run after consentDoes data-cmp-service exactly match a name in consentServices? Is the original type text/plain? Check document.head for a script with data-cmp-for.
A script runs before consentThe original tag must not have a JavaScript type, and the URL must be in data-src, not src.
An embed appears twice or stays emptyUse a <template> inside an HTML Embed rather than an iframe with data-src next to other markup.
Tracking cookies remain after decliningCompare cookie names with the cookies patterns. Cookies on other domains or HttpOnly cookies can’t be deleted by JavaScript.
Wrong languageCheck Page Settings → Language and cmp.getLanguage(); add a matching entry to consentTranslations.
A native video contacts YouTube or Vimeo before consentTurn off Show preview, Autoplay and Preconnect on the component, and check that it has data-cmp-gate-content inside a gate with data-cmp-gate-mode="interaction".
A right-to-left language shows left-to-rightAdd "dir": "rtl" to its entry, or use a language code from the right-to-left list.
The notice reopens for returning visitorsThe service list changed. This is intentional: new services need a new decision.
Want to debugSet debug: true in cmpConfig and watch the console.

Migrating from other consent managers

  • Remove the old consent manager’s script and its styles from Custom Code.
  • Recreate your services in consentServices and your copy in consentTranslations.
  • Replace attributes on blocked elements: data-name="x"data-cmp-service="x". type="text/plain", data-src, data-href and data-type work as before.
  • Replace show() calls with cmp.show() and callbacks with cmp.on("service", …).
  • Use a new storageName if the old manager used the same cookie name, so visitors are asked once with the new UI.

We use cookies and similar technologies for essential functions and, with your consent, for analytics, marketing and embedded media. You can change your choice at any time. Learn more in our privacy policy.

Our services have changed since your last visit. Please review your choice.

Wir verwenden Cookies und ähnliche Technologien für notwendige Funktionen und, mit Ihrer Einwilligung, für Statistik, Marketing und eingebettete Medien. Sie können Ihre Auswahl jederzeit ändern. Mehr dazu in unserer Datenschutzerklärung.

Unsere Dienste haben sich seit Ihrem letzten Besuch geändert. Bitte prüfen Sie Ihre Auswahl.

Usamos cookies y tecnologías similares para funciones esenciales y, con tu consentimiento, para analítica, marketing y contenido incrustado. Puedes cambiar tu elección en cualquier momento. Más información en nuestra política de privacidad.

Nuestros servicios han cambiado desde tu última visita. Revisa tu elección.

نستخدم ملفات تعريف الارتباط والتقنيات المشابهة للوظائف الأساسية، وبموافقتك للإحصاءات والتسويق والمحتوى المضمّن. يمكنك تغيير اختيارك في أي وقت. لمعرفة المزيد، اطّلع على سياسة الخصوصية.

تغيّرت خدماتنا منذ زيارتك الأخيرة. يُرجى مراجعة اختيارك.