تخطَّ إلى المحتوى

تكاملات الأطر (Frameworks)

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>

إذا كنت تستخدم العرض من جانب الخادم (SSR) أو توليد المواقع الثابتة (SSG)، فإن تحميل سكريبت CMP حصريًا من جانب العميل قد يسبب تأخيرًا طفيفًا. لضمان تضمين السكريبت في ملف HTML المولد الأولي، يمكنك استخدام @unhead/vue لحقنه في <head> مباشرة:

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

في Next.js App Router، يجب تحميل CMP داخل Client Component لأنه يحتاج إلى window و 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

في Nuxt 3، استخدم إضافة على جانب العميل لتحميل SDK مرة واحدة عند تشغيل التطبيق:

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>

نظرًا لأن Astro يعرض HTML ثابتًا افتراضيًا، استخدم علامة <script> في التخطيط. للمكونات التفاعلية، استخدم أيًا من التكاملات أعلاه:

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>

للمكونات التفاعلية، استخدم Vue أو React أو Svelte مع توجيهات العميل لـ Astro:

<CookieButton client:load />

لجميع الأطر، أضف هذه الأنواع العامة:

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[]]>
}
}