Replies: 3 comments
-
I'm interested in this as well. |
Beta Was this translation helpful? Give feedback.
-
I am sorry for being late, but I had the same question and finally I have reached with this solution, maybe its not the way but its working perfectly and debugging with google tag assistant shows correct consent config and page view events. First I have created a hook to handle the user consent, it can store the consent in a cookie or in localStorage, it doesnt matter. It has a function where you update the consent by using the import { setCookie, getCookie } from 'cookies-next'
import { useEffect, useState } from 'react'
const COOKIE_NAME = 'YOUR_COOKIE_NAME'
export enum CookieState {
PENDING = 'pending',
ACCEPTED = 'accepted',
REJECTED = 'rejected',
}
export const useCookiesPolicy = () => {
const [cookieState, setCookieState] = useState<string | undefined>(
getCookie(COOKIE_NAME)
)
const updateConsent = (state: CookieState) => {
const consent = state === CookieState.ACCEPTED ? 'granted' : 'denied'
// @ts-expect-error
window.gtag('consent', 'update', {
analytics_storage: consent,
ad_storage: consent,
ad_personalization: consent,
ad_user_data: consent,
fb_pixel: consent,
})
}
const acceptCookies = () => {
setCookieState(CookieState.ACCEPTED)
setCookie(COOKIE_NAME, CookieState.ACCEPTED, { maxAge: 24 * 60 * 60 * 365 })
updateConsent(CookieState.ACCEPTED)
}
const rejectCookies = () => {
setCookieState(CookieState.REJECTED)
setCookie(COOKIE_NAME, CookieState.REJECTED, { maxAge: 24 * 60 * 60 * 365 })
updateConsent(CookieState.REJECTED)
}
useEffect(() => {
if (!cookieState) setCookieState(CookieState.PENDING)
}, [cookieState])
return {
cookieState,
onAcceptCookies: acceptCookies,
onRejectCookies: rejectCookies,
}
} Then I have created a cookie banner, its very simple with just an accept and reject buttons, and conditionally rendered depending on previous hook state: import Link from 'next/link'
import { useTranslations } from 'next-intl'
import { useCookiesPolicy, CookieState } from 'hooks/useCookiesPolicy'
import { Button } from 'components/Primitives/Button'
export default function CookieBanner() {
const t = useTranslations('landing.cookiesBanner')
const { cookieState, onAcceptCookies, onRejectCookies } = useCookiesPolicy()
if (!cookieState || cookieState !== CookieState.PENDING) return null
return (
<div
id="cookies-banner"
className="z-20 max-w-xs md:max-w-xl mx-auto fixed bottom-4 left-0 right-0 bg-white rounded-lg border-2 shadow-lg"
>
<div className="flex flex-col justify-between items-center px-3 md:flex-row">
<div className="mt-3 md:mt-0">
{t('message')}
<Link
href="/cookies"
className="text-blue-500 whitespace-nowrap hover:underline"
>
{t('readMore')}
</Link>
</div>
<div className="flex space-x-4 items-center p-5">
<Button onClick={onRejectCookies} variant="link">
{t('reject')}
</Button>
<Button
onClick={onAcceptCookies}
className="bg-fuel-purple text-white"
>
{t('accept')}
</Button>
</div>
</div>
</div>
)
} And now here is the most important part, the component that renders the script. By default, consent mode should be denied, and only granted if the user explicitly accepts it. So you just need to do string interpolation to always render the script but set the consent value depending on the hook state (the first script is for Umami Analytics, you can ignore it): import Script from 'next/script'
import { useCookiesPolicy, CookieState } from 'hooks/useCookiesPolicy'
import { GTM_ID, UMAMI_ID } from 'lib/analytics'
export default function Analytics() {
const { cookieState } = useCookiesPolicy()
// Note: If pending it will be denied
const consent = cookieState === CookieState.ACCEPTED ? 'granted' : 'denied'
return (
<>
<Script src="/stats/script.js" data-website-id={UMAMI_ID} />
<Script
id="gtm"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
const consent = '${consent}';
gtag('consent', 'default', {
'ad_storage': consent,
'analytics_storage': consent,
'ad_personalization': consent,
'ad_user_data': consent,
'fb_pixel': consent
});
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${GTM_ID}');`,
}}
/>
</>
)
} Then you just import this component in your And thats my current solution, it can be improved by allowing the user to customize the script, but for my use case its fine like that. I hope it helps and if you have any suggestions just write me! |
Beta Was this translation helpful? Give feedback.
-
I applied one very similar to that of @angelhodar // components/GoogleTagManager.tsx
'use client';
import Script from 'next/script';
import { useEffect, useState } from 'react';
// Consent configuration types
type ConsentStatus = 'granted' | 'denied';
// Component props
interface GoogleTagManagerProps {
gtmId: string;
}
export function GoogleTagManager({ gtmId }: GoogleTagManagerProps) {
// Consent state based on localStorage
const [consent, setConsent] = useState<ConsentStatus>('denied');
useEffect(() => {
// Update consent from localStorage
const updateConsentFromStorage = () => {
const cookieState = localStorage.getItem('cookieConsent');
setConsent(cookieState === 'all' ? 'granted' : 'denied');
};
// Get current state on load
updateConsentFromStorage();
// Listen for localStorage changes from other tabs/windows
const handleStorageChange = (event: StorageEvent) => {
if (event.key === 'cookieConsent') {
updateConsentFromStorage();
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// GTM initialization script with consent config
const gtmScript = `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Configure consent mode
gtag('consent', 'default', {
'ad_storage': '${consent}',
'analytics_storage': '${consent}',
'ad_personalization': '${consent}',
'ad_user_data': '${consent}',
'functionality_storage': 'granted',
'security_storage': 'granted'
});
// Load GTM
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${gtmId}');
`;
return (
<>
<Script
id="gtm-consent-script"
strategy="afterInteractive"
dangerouslySetInnerHTML={{ __html: gtmScript }}
/>
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`}
height="0"
width="0"
style={{ display: 'none', visibility: 'hidden' }}
/>
</noscript>
</>
);
} // components/CookieConsent.tsx
'use client';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { Link, usePathname } from '@/i18n/navigation';
// Window object typing
declare global {
interface Window {
dataLayer?: Array<Record<string, unknown> | unknown[]>;
gtag?: (...arguments_: unknown[]) => void;
}
}
// Consent constants
const CONSENT_ALL = 'all'; // Full consent (granted)
const CONSENT_ESSENTIAL = 'essential'; // Basic consent (denied)
// Cookie consent banner component
export default function CookieConsent() {
const [isVisible, setIsVisible] = useState(false);
const pathname = usePathname();
const t = useTranslations('Cookie');
// Apply permissions to GTM
const applyConsent = (hasFullConsent: boolean) => {
if (typeof window === 'undefined') return;
try {
// Ensure dataLayer exists
window.dataLayer = window.dataLayer || [];
// Define gtag if it doesn't exist
if (typeof window.gtag !== 'function') {
window.gtag = function(...arguments_) {
window.dataLayer!.push(arguments_);
};
}
// Update consent in GTM
const consent = hasFullConsent ? 'granted' : 'denied';
// Update consent configuration
window.gtag('consent', 'update', {
'ad_storage': consent,
'analytics_storage': consent,
'ad_personalization': consent,
'ad_user_data': consent,
'functionality_storage': 'granted', // Always allowed for functionality
'security_storage': 'granted' // Always allowed for security
});
// Remove analytics cookies if rejected
if (!hasFullConsent) {
const cookiesToRemove = ['_ga', '_gat', '_gid'];
cookiesToRemove.forEach(cookieName => {
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
});
}
} catch (error) {
console.error('Error updating consent:', error);
}
};
useEffect(() => {
// Check for existing consent
const hasConsent = localStorage.getItem('cookieConsent');
// If consent exists, apply it
if (hasConsent) {
applyConsent(hasConsent === CONSENT_ALL);
} else {
// Default config (everything denied except functionality)
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || [];
if (typeof window.gtag !== 'function') {
window.gtag = function(...arguments_) {
window.dataLayer!.push(arguments_);
};
}
// Default config (denied)
window.gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied',
'ad_personalization': 'denied',
'ad_user_data': 'denied',
'functionality_storage': 'granted',
'security_storage': 'granted'
});
}
// Show banner after delay
const timer = setTimeout(() => {
setIsVisible(true);
}, 1000);
return () => clearTimeout(timer);
}
}, []);
// Handle user choices
const acceptAll = () => {
localStorage.setItem('cookieConsent', CONSENT_ALL);
applyConsent(true);
setIsVisible(false);
};
const acceptEssential = () => {
localStorage.setItem('cookieConsent', CONSENT_ESSENTIAL);
applyConsent(false);
setIsVisible(false);
};
return (
<div
className={`fixed right-0 bottom-0 left-0 z-50 p-4 ${isVisible ? 'animate-in' : ''}`}
aria-labelledby="cookie-consent-title"
role="dialog"
>
<div className="container-md">
<div className="glass rounded-lg p-4 shadow-md md:flex md:items-center md:justify-between">
<div className="mb-4 md:mr-4 md:mb-0">
<h2 id="cookie-consent-title" className="mb-2 text-lg font-medium">
{t('title')}
</h2>
<p className="text-muted-foreground text-sm">
{t('description')}{' '}
<Link
href="/privacy"
className="text-primary font-medium hover:underline"
>
{t('privacyLink')}
</Link>
</p>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-2">
<button
onClick={acceptEssential}
className="btn btn-outline text-sm"
>
{t('essentialOnly')}
</button>
<button onClick={acceptAll} className="btn btn-primary text-sm">
{t('acceptAll')}
</button>
</div>
</div>
</div>
</div>
);
} // layout.tsx
<GoogleTagManager gtmId="GTM-******" /> |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
We are adding GoogleTagManager from @next/third-parties and wonder what is the best approach to deal with consent in a good way? Ideally we do want to collect page views directly, but they shouldn't be sent until a user has given a consent, is that possible with GoogleTagManager? Or do I have to write some own logic to queue up events until we have a decision around consent and now what to do with the events?
Additional information
No response
Example
No response
Beta Was this translation helpful? Give feedback.
All reactions