Ir al contenido

Integraciones de 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>

Si usas Server-Side Rendering (SSR) o Static Site Generation (SSG), cargar el script CMP exclusivamente en el cliente puede causar un leve retraso. Para asegurar la inclusión en el HTML, puedes usar @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>
)
}

En Next.js App Router, el CMP debe cargarse en un Client Component ya que accede a window y 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

En Nuxt 3, utiliza un plugin en el lado del cliente para cargar el SDK al inicio:

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>

Dado que Astro renderiza HTML estático por defecto, usa una etiqueta <script> en tu layout. Para islas interactivas, usa cualquier integración de framework anterior:

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>

Para componentes interactivos, usa Vue, React o Svelte con las directivas de cliente de Astro:

<CookieButton client:load />

Para todos los frameworks, añade estos tipos globales:

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