Aller au contenu

Intégrations Framework

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.


composables/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),
}
}
<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 CMP
watch(locale, (lang) => {
if (isReady.value) setLanguage(lang)
})
</script>

Si vous utilisez le rendu côté serveur (SSR) ou la génération de sites statiques (SSG), le chargement exclusif côté client peut provoquer un léger retard. Pour assurer l’inclusion du script dans le HTML, vous pouvez utiliser @unhead/vue :

<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>
<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>
.env
VITE_CMP_URL=https://tool.hashentry.com/cmp.js
VITE_CMP_SITE_KEY=he_live_xxxxxxxxxxxxx

hooks/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>
)
}

Dans Next.js App Router, le CMP doit être chargé dans un Client Component car il accède à window et document.

hooks/useCMP.ts
'use client'
// Same hook as the React version above.
// Import and use it in Client Components only.
app/layout.tsx
import { CmpProvider } from '@/components/CmpProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<CmpProvider />
{children}
</body>
</html>
)
}
components/CmpProvider.tsx
'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
}
.env.local
NEXT_PUBLIC_CMP_URL=https://tool.hashentry.com/cmp.js
NEXT_PUBLIC_CMP_SITE_KEY=he_live_xxxxxxxxxxxxx

Dans Nuxt 3, utilisez un plugin côté client pour charger le SDK au démarrage :

plugins/cmp.client.ts
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)
})
composables/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),
}
}
nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
cmpUrl: 'https://tool.hashentry.com/cmp.js',
cmpSiteKey: '', // Set via NUXT_PUBLIC_CMP_SITE_KEY env
}
}
})

services/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.ts
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)
}
}
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)
}

lib/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),
}
}
+layout.svelte
<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>

Puisqu’Astro génère du HTML statique par défaut, utilisez une balise <script> dans votre layout. Pour les îlots interactifs, utilisez les intégrations ci-dessus :

layouts/BaseLayout.astro
<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>

Pour les composants interactifs, utilisez Vue, React ou Svelte avec les directives client d’Astro :

<CookieButton client:load />

Pour tous les frameworks, ajoutez ces types globaux :

types/cmp.d.ts
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[]]>
}
}