Framework Integrations
While the basic script tag integration works universally, modern frameworks benefit from deeper integration using composables, hooks, and services. This guide provides ready-to-use examples for popular frameworks.
Vue 3 (Composition API)
Section titled “Vue 3 (Composition API)”Composable: useCMP.ts
Section titled “Composable: useCMP.ts”import { ref, onMounted, onUnmounted, readonly } from 'vue'
interface ConsentCategories { necessary: boolean analytics: boolean marketing: boolean preferences: boolean}
let loadPromise: Promise<void> | null = null
function loadScript(src: string, siteKey: string): Promise<void> { if (loadPromise) return loadPromise loadPromise = new Promise((resolve, reject) => { if (document.querySelector(`script[data-site="${siteKey}"]`)) { resolve(); return } const el = document.createElement('script') el.src = src el.async = true el.setAttribute('data-site', siteKey) el.onload = () => resolve() el.onerror = () => reject(new Error(`CMP script failed: ${src}`)) document.head.appendChild(el) }) return loadPromise}
export function useCMP(options?: { cmpUrl?: string; siteKey?: string }) { const isReady = ref(false) const consent = ref<{ decision: string; categories: ConsentCategories } | null>(null) let cleanups: Array<() => void> = []
const sync = () => { consent.value = window.CMP?.getConsent() ?? null }
onMounted(async () => { const url = options?.cmpUrl ?? import.meta.env.VITE_CMP_URL const key = options?.siteKey ?? import.meta.env.VITE_CMP_SITE_KEY if (!url || !key) return
await loadScript(url, key) window.CMP?.ready(() => { isReady.value = true; sync() })
const onChange = () => sync() window.CMP?.on('consent:accepted', onChange) window.CMP?.on('consent:rejected', onChange) window.CMP?.on('consent:updated', onChange) cleanups = [ () => window.CMP?.off('consent:accepted', onChange), () => window.CMP?.off('consent:rejected', onChange), () => window.CMP?.off('consent:updated', onChange), ] })
onUnmounted(() => { cleanups.forEach(fn => fn()); cleanups = [] })
return { isReady: readonly(isReady), consent: readonly(consent), show: () => window.CMP?.show(), hide: () => window.CMP?.hide(), accept: (cats?: Partial<ConsentCategories>) => window.CMP?.accept(cats), reject: () => window.CMP?.reject(), hasConsent: (cat: string) => window.CMP?.hasConsent(cat) ?? false, setLanguage: (lang: string) => window.CMP?.setLanguage(lang), }}Usage in App.vue
Section titled “Usage in App.vue”<script setup lang="ts">import { useCMP } from '@/composables/useCMP'import { useI18n } from 'vue-i18n'import { watch } from 'vue'
const { isReady, consent, setLanguage } = useCMP()const { locale } = useI18n()
// Sync language changes to CMPwatch(locale, (lang) => { if (isReady.value) setLanguage(lang)})</script>SSR / SSG Optimization (Vite SSG / Nuxt)
Section titled “SSR / SSG Optimization (Vite SSG / Nuxt)”If you are using Server-Side Rendering (SSR) or Static Site Generation (SSG), loading the CMP script exclusively on the client side with loadScript might cause a slight delay or flicker. To ensure the script is included in the initially generated HTML, you can use @unhead/vue (or Nuxt’s useHead) to inject it immediately to the <head> during the build:
<script setup lang="ts">import { useCMP } from '@/composables/useCMP'import { useHead } from '@unhead/vue' // Veya Nuxt için doğrudan useHead()
const { isReady, setLanguage } = useCMP()
if (import.meta.env.VITE_CMP_URL && import.meta.env.VITE_CMP_SITE_KEY) { useHead({ script: [ { src: import.meta.env.VITE_CMP_URL, async: true, 'data-site': import.meta.env.VITE_CMP_SITE_KEY } ] })}
// ... sync logic</script>Usage in a Component
Section titled “Usage in a Component”<script setup lang="ts">import { useCMP } from '@/composables/useCMP'
const { consent, hasConsent, show } = useCMP()</script>
<template> <div v-if="consent"> <p>Analytics: {{ hasConsent('analytics') ? '✅' : '❌' }}</p> </div> <button @click="show()">Cookie Preferences</button></template>Environment Variables
Section titled “Environment Variables”VITE_CMP_URL=https://tool.hashentry.com/cmp.jsVITE_CMP_SITE_KEY=he_live_xxxxxxxxxxxxxReact (Hooks)
Section titled “React (Hooks)”Hook: useCMP.ts
Section titled “Hook: useCMP.ts”import { useState, useEffect, useCallback, useRef } from 'react'
interface ConsentCategories { necessary: boolean analytics: boolean marketing: boolean preferences: boolean}
let loadPromise: Promise<void> | null = null
function loadScript(src: string, siteKey: string): Promise<void> { if (loadPromise) return loadPromise loadPromise = new Promise((resolve, reject) => { if (document.querySelector(`script[data-site="${siteKey}"]`)) { resolve(); return } const el = document.createElement('script') el.src = src el.async = true el.setAttribute('data-site', siteKey) el.onload = () => resolve() el.onerror = () => reject(new Error(`CMP script failed: ${src}`)) document.head.appendChild(el) }) return loadPromise}
export function useCMP(options?: { cmpUrl?: string; siteKey?: string }) { const [isReady, setIsReady] = useState(false) const [consent, setConsent] = useState<{ decision: string; categories: ConsentCategories } | null>(null) const cleanups = useRef<Array<() => void>>([])
const sync = useCallback(() => { setConsent(window.CMP?.getConsent() ?? null) }, [])
useEffect(() => { const url = options?.cmpUrl ?? import.meta.env.VITE_CMP_URL const key = options?.siteKey ?? import.meta.env.VITE_CMP_SITE_KEY if (!url || !key) return
loadScript(url, key).then(() => { window.CMP?.ready(() => { setIsReady(true); sync() }) const onChange = () => sync() window.CMP?.on('consent:accepted', onChange) window.CMP?.on('consent:rejected', onChange) window.CMP?.on('consent:updated', onChange) cleanups.current = [ () => window.CMP?.off('consent:accepted', onChange), () => window.CMP?.off('consent:rejected', onChange), () => window.CMP?.off('consent:updated', onChange), ] })
return () => { cleanups.current.forEach(fn => fn()); cleanups.current = [] } }, [])
return { isReady, consent, show: useCallback(() => window.CMP?.show(), []), hide: useCallback(() => window.CMP?.hide(), []), accept: useCallback((cats?: Partial<ConsentCategories>) => window.CMP?.accept(cats), []), reject: useCallback(() => window.CMP?.reject(), []), hasConsent: useCallback((cat: string) => window.CMP?.hasConsent(cat) ?? false, []), setLanguage: useCallback((lang: string) => window.CMP?.setLanguage(lang), []), }}import { useCMP } from './hooks/useCMP'
function CookieBanner() { const { isReady, consent, show, hasConsent } = useCMP()
return ( <div> {isReady && consent && ( <p>Analytics: {hasConsent('analytics') ? '✅' : '❌'}</p> )} <button onClick={show}>Cookie Preferences</button> </div> )}Next.js (App Router)
Section titled “Next.js (App Router)”Client Component Hook
Section titled “Client Component Hook”In Next.js App Router, CMP must be loaded in a Client Component since it accesses window and document.
'use client'// Same hook as the React version above.// Import and use it in Client Components only.Layout Integration
Section titled “Layout Integration”import { CmpProvider } from '@/components/CmpProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <CmpProvider /> {children} </body> </html> )}'use client'import { useCMP } from '@/hooks/useCMP'
export function CmpProvider() { useCMP({ cmpUrl: process.env.NEXT_PUBLIC_CMP_URL!, siteKey: process.env.NEXT_PUBLIC_CMP_SITE_KEY!, }) return null}Environment Variables
Section titled “Environment Variables”NEXT_PUBLIC_CMP_URL=https://tool.hashentry.com/cmp.jsNEXT_PUBLIC_CMP_SITE_KEY=he_live_xxxxxxxxxxxxxNuxt 3
Section titled “Nuxt 3”Plugin: cmp.client.ts
Section titled “Plugin: cmp.client.ts”In Nuxt 3, use a client-side plugin to load the SDK once on app startup:
export default defineNuxtPlugin(() => { const config = useRuntimeConfig() const src = config.public.cmpUrl as string const siteKey = config.public.cmpSiteKey as string
if (!src || !siteKey) return
const script = document.createElement('script') script.src = src script.async = true script.setAttribute('data-site', siteKey) document.head.appendChild(script)})Composable: useCMP.ts
Section titled “Composable: useCMP.ts”export function useCMP() { const isReady = ref(false) const consent = ref<Record<string, any> | null>(null)
const sync = () => { consent.value = window.CMP?.getConsent() ?? null }
onMounted(() => { window.CMP?.ready(() => { isReady.value = true; sync() }) const onChange = () => sync() window.CMP?.on('consent:accepted', onChange) window.CMP?.on('consent:rejected', onChange) window.CMP?.on('consent:updated', onChange) })
return { isReady: readonly(isReady), consent: readonly(consent), show: () => window.CMP?.show(), hide: () => window.CMP?.hide(), accept: (cats?: Record<string, boolean>) => window.CMP?.accept(cats), reject: () => window.CMP?.reject(), hasConsent: (cat: string) => window.CMP?.hasConsent(cat) ?? false, setLanguage: (lang: string) => window.CMP?.setLanguage(lang), }}Configuration
Section titled “Configuration”export default defineNuxtConfig({ runtimeConfig: { public: { cmpUrl: 'https://tool.hashentry.com/cmp.js', cmpSiteKey: '', // Set via NUXT_PUBLIC_CMP_SITE_KEY env } }})Angular
Section titled “Angular”Service: cmp.service.ts
Section titled “Service: cmp.service.ts”import { Injectable, signal } from '@angular/core'
@Injectable({ providedIn: 'root' })export class CmpService { isReady = signal(false) consent = signal<Record<string, any> | null>(null)
private loaded = false
init(cmpUrl: string, siteKey: string): void { if (this.loaded) return this.loaded = true
const el = document.createElement('script') el.src = cmpUrl el.async = true el.setAttribute('data-site', siteKey) el.onload = () => { (window as any).CMP?.ready(() => { this.isReady.set(true) this.sync() }) const onChange = () => this.sync() ;(window as any).CMP?.on('consent:accepted', onChange) ;(window as any).CMP?.on('consent:rejected', onChange) ;(window as any).CMP?.on('consent:updated', onChange) } document.head.appendChild(el) }
private sync(): void { this.consent.set((window as any).CMP?.getConsent() ?? null) }
show = () => (window as any).CMP?.show() hide = () => (window as any).CMP?.hide() accept = (cats?: Record<string, boolean>) => (window as any).CMP?.accept(cats) reject = () => (window as any).CMP?.reject() hasConsent = (cat: string) => (window as any).CMP?.hasConsent(cat) ?? false setLanguage = (lang: string) => (window as any).CMP?.setLanguage(lang)}App Component
Section titled “App Component”import { Component, OnInit, inject } from '@angular/core'import { CmpService } from './services/cmp.service'import { environment } from '../environments/environment'
@Component({ selector: 'app-root', template: '<router-outlet />' })export class AppComponent implements OnInit { private cmp = inject(CmpService)
ngOnInit() { this.cmp.init(environment.cmpUrl, environment.cmpSiteKey) }}Usage in a Component
Section titled “Usage in a Component”import { Component, inject } from '@angular/core'import { CmpService } from '../services/cmp.service'
@Component({ selector: 'app-footer', template: ` <footer> <button (click)="cmp.show()">Cookie Preferences</button> @if (cmp.isReady()) { <span>Analytics: {{ cmp.hasConsent('analytics') ? '✅' : '❌' }}</span> } </footer> `})export class FooterComponent { cmp = inject(CmpService)}Svelte (5 / Runes)
Section titled “Svelte (5 / Runes)”Store: cmp.svelte.ts
Section titled “Store: cmp.svelte.ts”let loadPromise: Promise<void> | null = null
function loadScript(src: string, siteKey: string): Promise<void> { if (loadPromise) return loadPromise loadPromise = new Promise((resolve, reject) => { if (document.querySelector(`script[data-site="${siteKey}"]`)) { resolve(); return } const el = document.createElement('script') el.src = src; el.async = true el.setAttribute('data-site', siteKey) el.onload = () => resolve() el.onerror = () => reject() document.head.appendChild(el) }) return loadPromise}
export function createCMP(cmpUrl: string, siteKey: string) { let isReady = $state(false) let consent = $state<Record<string, any> | null>(null)
const sync = () => { consent = (window as any).CMP?.getConsent() ?? null }
loadScript(cmpUrl, siteKey).then(() => { (window as any).CMP?.ready(() => { isReady = true; sync() }) ;(window as any).CMP?.on('consent:accepted', sync) ;(window as any).CMP?.on('consent:rejected', sync) ;(window as any).CMP?.on('consent:updated', sync) })
return { get isReady() { return isReady }, get consent() { return consent }, show: () => (window as any).CMP?.show(), hide: () => (window as any).CMP?.hide(), accept: (cats?: Record<string, boolean>) => (window as any).CMP?.accept(cats), reject: () => (window as any).CMP?.reject(), hasConsent: (cat: string) => (window as any).CMP?.hasConsent(cat) ?? false, setLanguage: (lang: string) => (window as any).CMP?.setLanguage(lang), }}<script lang="ts"> import { createCMP } from '$lib/cmp.svelte' import { PUBLIC_CMP_URL, PUBLIC_CMP_SITE_KEY } from '$env/static/public'
const cmp = createCMP(PUBLIC_CMP_URL, PUBLIC_CMP_SITE_KEY)</script>
<slot />
<footer> <button onclick={() => cmp.show()}>Cookie Preferences</button> {#if cmp.isReady} <span>Analytics: {cmp.hasConsent('analytics') ? '✅' : '❌'}</span> {/if}</footer>Since Astro renders static HTML by default, use a <script> tag in your layout. For interactive islands, use any of the framework integrations above within your chosen UI framework:
<html> <head> <script src={import.meta.env.PUBLIC_CMP_URL} data-site={import.meta.env.PUBLIC_CMP_SITE_KEY} async /> </head> <body> <slot /> </body></html>For interactive components, use the Vue, React, or Svelte composables with Astro’s client directives:
<CookieButton client:load />TypeScript Types
Section titled “TypeScript Types”For all frameworks, add these global types:
interface ConsentCategories { necessary: boolean analytics: boolean marketing: boolean preferences: boolean}
interface ConsentState { decision: 'accept' | 'reject' | 'partial' categories: ConsentCategories timestamp: string consent_token?: string}
interface ConsentResult { success: boolean consent_token: string proof_hash: string created_at: string}
interface CMPAPI { show(): void hide(): void accept(categories?: Partial<ConsentCategories>): Promise<ConsentResult> reject(): Promise<ConsentResult> getConsent(): ConsentState | null hasConsent(category: string): boolean setLanguage(lang: string): void getLanguage(): string setMetadata(key: string, value: string): void setMetadata(obj: Record<string, string>): void setRegion(region: string): void on(event: string, callback: (...args: unknown[]) => void): void off(event: string, callback: (...args: unknown[]) => void): void ready(callback: () => void): void}
declare global { interface Window { CMP?: CMPAPI __CMP_QUEUE?: Array<[string, ...unknown[]]> }}