X Enterprises
nuxt-x-affiliate

CookieConsent

Consent-aware cookie banner + preferences modal that auto-fires configured tracking scripts (GTM / GA4 / Clarity / Meta Pixel / Hotjar / etc.) only after the visitor grants consent. Drop tracking IDs into app.config.xAffiliate.tracking and the banner handles the rest.

CookieConsent

A drop-in consent flow for the entire site. The banner reads app.config.xAffiliate.tracking, shows on first visit, persists the visitor's choice to localStorage, and dynamically injects GTM / GA4 / Clarity / Meta Pixel / Hotjar / Segment / LinkedIn Insight / etc. once the matching consent category is granted.

The companion useXAFConsentTracking() composable and xaf-consent-tracking.client.ts plugin handle the script injection and rehydrate-on-revisit flow so returning visitors get their tracking scripts loaded immediately (no flash of banner).

Mirrors the nuxt-x-marketing XMarkPrivacyCookieConsent pattern — same composable shape, same storage convention (xAffiliate.consent vs xMarketing.consent), same plugin bootstrap.

Components

<XAFCookieConsent />

<XAFCookieConsent
  :policy-url="legalBase + '/cookie-policy'"
  :privacy-url="legalBase + '/privacy-policy'"
/>

The component is auto-teleported to <body> and renders a preferences modal via UModal when the visitor clicks "Customize".

Convenience configuration (90% of sites)

// app.config.ts — drop in your tracking IDs
export default defineAppConfig({
  xAffiliate: {
    tracking: {
      gtmId: 'GTM-XXXXXXX',
      ga4Id: 'G-XXXXXXXX',
      clarityId: 'abc123def4',
    },
  },
})

When gtmId is set, the layer auto-injects GTM via https://www.googletagmanager.com/gtm.js. When GTM is present, the layer skips the standalone GA4 / Clarity snippets to avoid double-counting page views (GTM manages them). Set tracking.ga4Id or tracking.clarityId only when you're NOT using GTM.

Escape hatch — custom scripts

For everything else (Meta Pixel, Hotjar, Segment, LinkedIn Insight, Pinterest, TikTok, etc.), use the scripts[] array. Each entry fires once the matching category is granted:

export default defineAppConfig({
  xAffiliate: {
    tracking: {
      scripts: [
        {
          id: 'meta-pixel',
          src: 'https://connect.facebook.net/en_US/fbevents.js',
          category: 'marketing',
          attrs: { async: '' },
        },
        {
          id: 'meta-pixel-init',
          inline: 'fbq("init", "1234567890"); fbq("track", "PageView");',
          category: 'marketing',
        },
        {
          id: 'hotjar',
          src: 'https://static.hotjar.com/c/hotjar-1234567.js?sv=6',
          category: 'analytics',
          attrs: { async: '' },
        },
      ],
    },
  },
})

src and inline are mutually supportive — set src for an external script, inline for a hand-written snippet like a gtag('js', new Date()) call. attrs map to attributes on the injected <script> tag (use "" for boolean attributes like async).

Props

PropTypeDefaultDescription
titlestring'We use cookies'Banner heading.
messagestring(see source)Banner body text.
acceptLabelstring'Accept all'Accept button text.
rejectLabelstring'Reject all'Reject button text.
saveLabelstring'Save preferences'Save button text (modal).
policyLabelstring'Read our policy'Policy link text.
policyUrlstringCookie policy URL.
privacyUrlstringPrivacy policy URL.
prefsTitlestring'Cookie preferences'Modal title.
prefsDescriptionstring(see source)Modal description.
showCustomizebooleantrueShow the Customize button in the banner.
categoriesCategory[]4 standardCategories shown in the modal — [{ id, label, description, required?, enabledByDefault? }].
storageKeystring'xAffiliate.consent'localStorage key for the consent state.
forceShowbooleanfalseForce the banner open (e.g. from a "Manage cookies" link in the footer).

Emits

EventPayloadWhen
acceptRecord<CategoryId, boolean>Visitor accepted all categories.
rejectRecord<CategoryId, boolean>Visitor rejected all non-required categories.
saveRecord<CategoryId, boolean>Visitor saved custom preferences.

Categories

The four standard categories — necessary (always-on, locked), analytics (GTM/GA4/Clarity), marketing (Meta Pixel/AdWords), preferences (UI settings) — are designed to map 1:1 to the scripts[].category field. Add or remove categories by passing a custom categories prop.

Mounting

The component is auto-teleported to <body> and renders a <UModal> for the preferences. Wire it into your layout once — usually in the default app/layouts/default.vue:

<template>
  <div>
    <slot />
    <XAFCookieConsent
      :policy-url="legalBase + '/cookie-policy'"
      :privacy-url="legalBase + '/privacy-policy'"
    />
  </div>
</template>

Use forceShow with a ref to re-open the banner from anywhere in your app:

<script setup>
const showConsent = ref(false)
</script>

<template>
  <footer>
    <UButton variant="link" @click="showConsent = true">Manage cookies</UButton>
  </footer>
  <XAFCookieConsent :force-show="showConsent" />
</template>

useXAFConsentTracking() composable

Underlying script-injection engine for <XAFCookieConsent />. Most sites don't need to call it directly — but if you want to programmatically grant consent (e.g. for a "preferences saved" toast that re-fires scripts) or read the current state:

const consent = useXAFConsentTracking()
consent.acceptAll()               // grant every category
consent.rejectAll()               // grant only necessary
consent.hasConsent('analytics')   // boolean — has analytics consent?
consent.state.value               // { necessary, analytics, marketing, preferences }
consent.setConsent({              // full state map
  necessary: true,
  analytics: true,
  marketing: false,
  preferences: true,
})

The composable listens for the consent:updated window event, so any banner UI that fires it stays in sync with the script loader.


Auto-registered by the layer. Bootstraps useXAFConsentTracking() on client mount:

  1. Reads stored consent from localStorage.
  2. If consent was previously granted, immediately injects the matching scripts.
  3. Registers a consent:updated listener for future banner interactions.

You don't need to install it manually — it ships in app/plugins/xaf-consent-tracking.client.ts and is picked up by Nuxt auto-import.


Storage key

The default storage key is xAffiliate.consent. Override via the storageKey prop if you ship multiple consent UIs (e.g. one for the affiliate layer, one for the marketing layer — they share a browser but use different keys).


Prior to v0.5.0, <XAFCookieConsent /> stored consent in a document.cookie value (default name xaf_consent). v0.5.0 switches to localStorage (default key xAffiliate.consent) to align with nuxt-x-marketing's XMarkPrivacyCookieConsent and to make the storage inspectable from dev tools.

If a consuming site had previously-deployed consent decisions under the old cookie-based storage, those users will see the banner again on their next visit (one-time regression; no manual migration needed). New visitors get the consent flow as expected.


AI Context

category: Privacy
package: "@xenterprises/nuxt-x-affiliate"
components:
  - XAFCookieConsent
composables:
  - useXAFConsentTracking
use-when: >
  Default consent flow for any site shipping GTM / GA4 / Clarity / Meta Pixel /
  Hotjar / Segment / LinkedIn Insight / etc. Drop tracking IDs into
  app.config.xAffiliate.tracking (gtmId / ga4Id / clarityId convenience
  fields, or scripts[] for anything else), mount <XAFCookieConsent /> once in
  your layout, and the banner + script injection handles itself.
typical-page-section: Bottom-anchored consent banner + preferences modal — mounted once per site, not per page.
scripts-fire-on: consent:updated window event + first revisit when storage key already set
storage-key: xAffiliate.consent (default)
Copyright © 2026