İçeriğe geç

Framework Entegrasyonları

Temel script tag entegrasyonu evrensel olarak çalışırken, modern framework’ler composable, hook ve servis yapılarıyla daha derin entegrasyon sağlar. Bu rehber, popüler framework’ler için hazır kullanılabilir örnekler sunar.


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 yüklenemedi: ${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()
// Dil değişikliklerini CMP SDK'ya senkronize et
watch(locale, (lang) => {
if (isReady.value) setLanguage(lang)
})
</script>

Eğer uygulamanızda Server-Side Rendering (SSR) veya Static Site Generation (SSG) kullanıyorsanız, CMP betiğini yalnızca istemcide (loadScript ile) yüklemek hafif bir gecikmeye veya titreşime neden olabilir. Betiğin doğrudan üretilen HTML içerisine erken işlenmesi için @unhead/vue veya Nuxt useHead kullanarak build aşamasında <head> etiketine oturtabilirsiniz:

<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>Analitik: {{ hasConsent('analytics') ? '✅' : '❌' }}</p>
</div>
<button @click="show()">Çerez Tercihleri</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 yüklenemedi: ${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 CerezBanneri() {
const { isReady, consent, show, hasConsent } = useCMP()
return (
<div>
{isReady && consent && (
<p>Analitik: {hasConsent('analytics') ? '' : ''}</p>
)}
<button onClick={show}>Çerez Tercihleri</button>
</div>
)
}

Next.js App Router’da CMP, window ve document eriştiği için Client Component içinde yüklenmelidir.

hooks/useCMP.ts
'use client'
// Yukarıdaki React hook'u ile aynı.
// Yalnızca Client Component'lerde kullanın.
app/layout.tsx
import { CmpProvider } from '@/components/CmpProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="tr">
<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’te, SDK’yı uygulama başlangıcında yalnızca bir kez yüklemek için client-side plugin kullanın:

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: '', // NUXT_PUBLIC_CMP_SITE_KEY env ile ayarlayın
}
}
})

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)
}
import { Component, inject } from '@angular/core'
import { CmpService } from '../services/cmp.service'
@Component({
selector: 'app-footer',
template: `
<footer>
<button (click)="cmp.show()">Çerez Tercihleri</button>
@if (cmp.isReady()) {
<span>Analitik: {{ 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()}>Çerez Tercihleri</button>
{#if cmp.isReady}
<span>Analitik: {cmp.hasConsent('analytics') ? '' : ''}</span>
{/if}
</footer>

Astro varsayılan olarak statik HTML render ettiğinden, layout’unuzda <script> tag’i kullanın. İnteraktif adacıklar için yukarıdaki framework entegrasyonlarından birini tercih ettiğiniz UI framework’ü ile kullanın:

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>

Tüm framework’ler için şu global tipleri ekleyin:

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