
Australian entrepreneur and founder of Rubix Studios. Vincent specialises in branding, multimedia, and web development with a focus on digital innovation and emerging technologies.
Table of contents
Centralising analytics and third-party script management is essential when developing with Payload CMS and Next.js. Embedding tracking scripts directly within the application can create compliance risks, affect site performance, and complicate maintenance. Leveraging Google Tag Manager (GTM) as the centralised platform for analytics, including GA4, Meta, TikTok, Reddit, Pinterest, and Microsoft Clarity, ensures a clear separation from application logic, enhances operational control, and streamlines consent management.
Google tools
Managing analytics with Payload CMS and Next.js requires coordinated use of several platforms. Each tool has a specific function within the data collection and reporting process, with Google Tag Manager (GTM) as the integration point.
Google Tag Manager (GTM)
GTM manages and injects analytics and tracking scripts into your application via a single container. This streamlines deployment, configuration, and consent management.
Google Analytics 4 (GA4)
GA4 records user interactions on the site, such as page views and events, and sends this data to Google’s analytics platform for analysis and reporting.
Google Search Console (GSC)
GSC provides insights on how the site is indexed and performs in Google Search. It tracks search queries, crawl activity, and indexing status.
GTM functions as the primary tool for deploying and managing GA4 tracking scripts. All custom events and analytics logic are loaded via GTM, ensuring a controlled and centralised implementation.
While GSC does not inject scripts or send real-time interaction data to GA4, integration between GSC and GA4 enables consolidated reporting within the GA4 dashboard. This allows search performance data from GSC to be accessible alongside behavioural analytics from GA4, supporting unified analysis and reporting.
Tag manager
Google Tag Manager (GTM) should be the only platform for deploying analytics and marketing scripts within Payload CMS and Next.js projects. Avoid embedding software development kits (SDKs) or tracking snippets directly into the application frontend or Payload administrative interface. All analytics requirements should be managed through GTM by configuring the necessary tags and establishing custom triggers for specific user actions, route changes, or content types.
For scenarios requiring dynamic contextual data, such as user identifiers or page classifications, utilise the GTM dataLayer. Push structured data objects to the dataLayer to enable GTM to activate tags with precise contextual information.
This method preserves the integrity of the frontend codebase by separating tracking concerns and preventing unnecessary code proliferation related to analytics logic.
Script control
Utilising Google Tag Manager (GTM) to manage the loading of third-party scripts grants precise control over resource execution and placement. GTM enables deferred execution, allowing tags to be triggered after specific user interactions or upon rendering defined views. Scripts can be selectively activated based on route changes or component-level criteria, ensuring they run only where necessary. Additionally, domain restrictions can be implemented to block tags from sending data to unapproved endpoints, enhancing data governance.
This centralised approach supports the development of lightweight application bundles, keeping the Next.js frontend clear of embedded marketing code. The resulting reduction in code weight and execution overhead delivers tangible improvements in application performance, particularly for server-side rendered and hydration-sensitive routes.
Integration
For compliant analytics integration in a Next.js application, tracking scripts and essential utilities are best managed within a centralised provider component, rather than injected directly into the main layout file (layout.tsx). By including the <GTM /> component within <Providers>, tracking and consent logic are consolidated, promoting maintainability and ensuring consistent regulatory compliance across the application.
This structure enables all global utilities, such as Google Tag Manager, consent management, and future providers, to be organised in a single location. Additional context providers, such as user context or marketing integrations, can be incorporated without disrupting the main layout structure.
@/app/(next)/layout.tsx
1import { Providers } from "@/context"234export default function RootLayout({5 children,6}: Readonly<{7 children: React.ReactNode8}>) {9 return (10 <html className={`antialiased`} lang="en">11 <body>12 <Providers>{children}</Providers>13 </body>14 </html>15 )16}17
@/context/index.tsx
1import { GTM } from "@/context/tag/google"2// import { Hubspot } from "@/context/tag/hubspot"3import { CookieConsent } from "@/components/cookies"4// import { UserProvider } from "@/context/users"56export const Providers = ({ children }: { children: React.ReactNode }) => {7 return (8 <>9 {/* <UserProvider> */}10 <GTM />11 {children}12 <CookieConsent />13 {/* <Hubspot /> */}14 {/* </UserProvider> */}15 </>16 )17}18
This pattern ensures that the Google Tag Manager script is conditionally loaded per user consent and provides a consistent, persistent consent state to all components via the application context. Integration with additional providers, such as user context, can further enrich tracking data as requirements evolve, with the flexibility to adjust provider scope as needed.
GTM
The <GTM /> component manages Google Tag Manager initialisation and maintains alignment between application cookie consent status and GTM consent mode. This approach ensures that analytics scripts operate only according to user preferences, supporting privacy compliance and regulatory requirements.
@/context/tag/google.tsx
1"use client"23import { useEffect } from "react"4import Script from "next/script"56import { useCookieStore } from "@/store/useCookieStore"78// Optional: Use Next.js official integration or a custom worker9// import { GoogleTagManager } from "@next/third-parties/google";1011export const GTM = () => {12 const { preferences, hasConsented, acceptCookies } = useCookieStore()1314 useEffect(() => {15 if (typeof window === "undefined" || hasConsented === null) return1617 function updateConsent() {18 if (typeof window.gtag === "function") {19 window.gtag("consent", "update", {20 functionality_storage: "granted",21 security_storage: "granted",22 analytics_storage: preferences.analytics ? "granted" : "denied",23 ad_storage: preferences.marketing ? "granted" : "denied",24 ad_user_data: preferences.userData ? "granted" : "denied",25 ad_personalization: preferences.adPersonalization26 ? "granted"27 : "denied",28 personalization_storage: preferences.contentPersonalization29 ? "granted"30 : "denied",31 })32 }33 }3435 updateConsent()3637 if (!window.gtag) {38 const scriptCheck = setInterval(() => {39 if (window.gtag) {40 updateConsent()41 clearInterval(scriptCheck)42 }43 }, 50)44 return () => clearInterval(scriptCheck)45 }46 }, [hasConsented, acceptCookies])4748 return (49 <>50 <Script id="gtm-inline-config" strategy="beforeInteractive">{`51 window.dataLayer = window.dataLayer || [];52 window.dataLayer.push({53 'gtm.start': new Date().getTime(),54 event: 'gtm.js'55 });56 function gtag(){dataLayer.push(arguments);}57 gtag('consent', 'default', {58 'functionality_storage': 'denied',59 'security_storage': 'denied',60 'analytics_storage': 'denied',61 'ad_storage': 'denied',62 'ad_user_data': 'denied',63 'ad_personalization': 'denied',64 'personalization_storage': 'denied'65 });66 `}</Script>67 <Script68 id="gtm-init"69 src="https://tag.domain.com/gtm.js" // Adjust to your worker70 strategy="beforeInteractive"71 />72 {/* https://tag.domain.com/gtm.js script ensures loading gtm as first-party */}73 {/* Example using official Next.js GTM integration: */}74 {/*75 <GoogleTagManager76 gtmId="GTM-XXXXXXX"77 gtmScriptUrl="https://tag.domain.com/gtm.js"78 />79 */}80 </>81 )82}83
This configuration loads GTM from a first-party-controlled endpoint and synchronises consent dynamically, ensuring that analytics activity always reflects the user's stated preferences.
Consent
The <CookieConsent /> component presents a persistent, accessible cookie consent banner to users, enabling them to accept or decline the use of cookies. It integrates with the application's cookie store, capturing and persisting the user's consent decision across the application session. On acceptance or rejection, the relevant consent state is updated, which can be used to control the activation of analytics, advertising, or personalisation features.
The component provides clear user feedback, accessible navigation, and links to the privacy policy for transparency. Consent choices are communicated throughout the application, ensuring regulatory compliance and respecting user preferences in downstream analytics and marketing integrations.
@/components/cookies/index.tsx
1"use client"23import { useEffect, useState } from "react"4import { HiOutlineCog } from "react-icons/hi"5import { LuCookie } from "react-icons/lu"67import { Link } from "@/lib/transition"8import { cn } from "@/lib/utils"9import { getClientSideURL } from "@/utils/getURL"10import { useCookieStore } from "@/store/useCookieStore"11import { Button } from "@/components/ui/button"12import { Preferences } from "@/components/cookies/preferences"1314export function CookieConsent() {15 const { hasConsented, acceptCookies, declineCookies, preferences } =16 useCookieStore()17 const [isOpen, setIsOpen] = useState(false)18 const [hide, setHide] = useState(false)19 const [showPreferences, setShowPreferences] = useState(false)2021 const accept = () => {22 setIsOpen(false)23 setTimeout(() => {24 setHide(true)25 }, 700)26 acceptCookies({27 ...preferences,28 analytics: true,29 marketing: true,30 userData: true,31 adPersonalization: true,32 contentPersonalization: true,33 })34 }3536 const decline = () => {37 setIsOpen(false)38 setTimeout(() => {39 setHide(true)40 }, 700)41 declineCookies()42 }4344 const manage = () => {45 setIsOpen(false)46 setTimeout(() => {47 setHide(true)48 setShowPreferences(true)49 }, 700)50 }5152 useEffect(() => {53 if (hasConsented === null) {54 setIsOpen(true)55 setHide(false)56 } else {57 setIsOpen(false)58 setHide(true)59 }60 }, [hasConsented])6162 return (63 <>64 <Preferences open={showPreferences} onOpenChange={setShowPreferences} />6566 <div67 role="dialog"68 aria-modal="true"69 aria-live="assertive"70 aria-labelledby="cookie-title-small"71 aria-describedby="cookie-description-small"72 className={cn(73 "fixed bottom-4 left-1/2 z-[200] w-full -translate-x-1/2 transform p-4 sm:mx-0 sm:max-w-md sm:p-0",74 !isOpen75 ? "scale-95 opacity-0 transition-[opacity,transform]"76 : "scale-100 opacity-100 transition-[opacity,transform]",77 hide && "hidden"78 )}79 >80 <div className="dark:bg-card bg-background border-border m-0 rounded-lg border shadow-lg sm:m-3">81 <header className="flex items-center justify-between p-3">82 <p83 id="cookie-title-small"84 className="text-base font-medium"85 role="heading"86 aria-level={2}87 >88 We use cookies89 </p>90 <LuCookie91 className="h-4 w-4 sm:h-[1.2rem] sm:w-[1.2rem]"92 aria-hidden="true"93 />94 </header>95 <main className="-mt-3 space-y-1 p-3" id="cookie-description-small">96 <p className="text-muted-foreground text-left text-xs">97 We use cookies to improve your experience, analyse traffic, and deliver98 personalised content and ads. Essential cookies for security and core99 functionality are always enabled. By clicking "Accept", you consent100 to our use of additional cookies.101 </p>102 <p className="text-muted-foreground text-left text-xs">103 For more information, see our{" "}104 <Link105 href={`${getClientSideURL()}/legal/privacy-policy`}106 className="text-primary relative before:absolute before:bottom-0 before:left-0 before:h-[1px] before:w-full before:origin-right before:scale-x-100 before:bg-current before:transition-transform before:duration-500 hover:before:scale-x-0"107 >108 privacy policy109 </Link>110 .111 </p>112 </main>113 <footer className="mt-2 flex flex-col items-center gap-2 border-t p-3 sm:flex-row">114 <Button115 onClick={manage}116 variant="outline"117 size="icon"118 className="h-8 w-full cursor-pointer text-xs transition-all duration-300 sm:h-9 sm:w-9"119 aria-label="Manage cookie consent"120 >121 <HiOutlineCog className="h-4 w-4" />122 </Button>123 <Button124 onClick={decline}125 className="h-8 w-full flex-1 cursor-pointer text-xs transition-all duration-300 sm:h-9"126 aria-label="Decline cookie consent"127 >128 Decline129 </Button>130 <Button131 onClick={accept}132 className="h-8 w-full flex-1 cursor-pointer text-xs transition-all duration-300 sm:h-9"133 aria-label="Accept cookie consent"134 >135 Accept136 </Button>137 </footer>138 </div>139 </div>140 </>141 )142}143
Preferences
@/components/cookies/preferences.tsx
1"use client"23import { Link } from "@/lib/transition"4import { useCookieStore } from "@/store/useCookieStore"5import { Button } from "@/components/ui/button"6import {7 Dialog,8 DialogContent,9 DialogHeader,10 DialogTitle,11} from "@/components/ui/dialog"12import { Switch } from "@/components/ui/switch"1314export function Preferences({15 open,16 onOpenChange,17}: {18 open: boolean19 onOpenChange: (open: boolean) => void20}) {21 const { preferences, setPreference, acceptCookies, declineCookies } = useCookieStore()2223 return (24 <Dialog open={open} modal={true}>25 <DialogContent>26 <DialogHeader>27 <DialogTitle>Preferences</DialogTitle>28 </DialogHeader>29 <div className="text-muted-foreground space-y-3 text-sm">30 <div className="flex flex-row items-center justify-between gap-2">31 <div>32 <p className="text-foreground font-semibold">Function</p>33 <p className="text-xs">34 Required for core site features such as language settings, login35 status, and user preferences.36 </p>37 </div>38 <Switch39 id="functional"40 checked={preferences.functional}41 disabled42 aria-label="Enable functional cookies"43 />44 </div>45 <div className="flex flex-row items-center justify-between gap-2">46 <div>47 <p className="text-foreground font-semibold">Security</p>48 <p className="text-xs">49 Protect against security threats and ensure user session50 integrity.51 </p>52 </div>53 <Switch54 id="security"55 checked={preferences.security}56 disabled57 aria-label="Enable security cookies"58 />59 </div>60 <div className="flex flex-row items-center justify-between gap-2">61 <div>62 <p className="text-foreground font-semibold">Analytics</p>63 <p className="text-xs">64 Help us understand how visitors interact with the website using65 aggregated data.66 </p>67 </div>68 <Switch69 id="analytics"70 checked={preferences.analytics}71 onCheckedChange={(checked) => setPreference("analytics", checked)}72 className="cursor-pointer"73 aria-label="Enable analytics cookies"74 />75 </div>76 <div className="flex flex-row items-center justify-between gap-2">77 <div>78 <p className="text-foreground font-semibold">Marketing</p>79 <p className="text-xs">80 Store data to serve ads that are more relevant to users.81 </p>82 </div>83 <Switch84 id="marketing"85 checked={preferences.marketing}86 onCheckedChange={(checked) => setPreference("marketing", checked)}87 className="cursor-pointer"88 aria-label="Enable marketing cookies"89 />90 </div>91 <div className="flex flex-row items-center justify-between gap-2">92 <div>93 <p className="text-foreground font-semibold">User</p>94 <p className="text-xs">95 Used to collect identifiable user data for targeted advertising.96 </p>97 </div>98 <Switch99 id="user-data"100 checked={preferences.userData}101 onCheckedChange={(checked) => setPreference("userData", checked)}102 className="cursor-pointer"103 aria-label="Enable user data cookies"104 />105 </div>106 <div className="flex flex-row items-center justify-between gap-2">107 <div>108 <p className="text-foreground font-semibold">Personalise</p>109 <p className="text-xs">110 Enables personalised ads based on browsing and usage history.111 </p>112 </div>113 <Switch114 id="ad-personalization"115 checked={preferences.adPersonalization}116 onCheckedChange={(checked) =>117 setPreference("adPersonalization", checked)118 }119 className="cursor-pointer"120 aria-label="Enable ad personalization cookies"121 />122 </div>123 <div className="flex flex-row items-center justify-between gap-2">124 <div>125 <p className="text-foreground font-semibold">Preference</p>126 <p className="text-xs">127 Adjusts content based on individual user behavior and128 preferences.129 </p>130 </div>131 <Switch132 id="content-personalization"133 checked={preferences.contentPersonalization}134 onCheckedChange={(checked) =>135 setPreference("contentPersonalization", checked)136 }137 className="cursor-pointer"138 aria-label="Enable content personalization cookies"139 />140 </div>141 </div>142 <div className="text-muted-foreground mt-2 text-xs">143 <p>144 By saving your preferences, you consent to the use of cookies as145 described in our{" "}146 <Link147 href="/legal/privacy-policy"148 className="text-primary underline hover:no-underline"149 aria-label="Privacy Policy"150 >151 Privacy Policy152 </Link>153 . You can change your preferences at any time by clicking the154 "Cookie settings" link in the footer.155 </p>156 </div>157 <div className="flex justify-end gap-2">158 <Button159 onClick={() => {160 declineCookies()161 onOpenChange(false)162 }}163 className="cursor-pointer rounded-3xl text-xs uppercase transition-all duration-300"164 aria-label="Decline cookie preferences"165 >166 Decline167 </Button>168 <Button169 onClick={() => {170 acceptCookies(preferences)171 onOpenChange(false)172 }}173 className="cursor-pointer rounded-3xl text-xs uppercase transition-all duration-300"174 aria-label="Accept cookie preferences"175 >176 Accept177 </Button>178 </div>179 </DialogContent>180 </Dialog>181 )182}183
Store
The useCookieStore hook manages the user's cookie consent state across the application using Zustand with persistence middleware. This store tracks whether the user has accepted or declined cookies and synchronises consent status with browser storage mechanisms for compliance and session persistence.
Upon acceptance, the consent state is written to browser cookies and localStorage, enabling long-term recognition of user preferences. If declined, the choice is stored in sessionStorage for the current session. The store's state is rehydrated on load to reflect any previously stored preferences, directly controlling the display of consent banners, analytics or marketing scripts.
This approach centralises consent management, ensuring a consistent user experience and regulatory alignment across all application components.
Zustand
To use Zustand for state management, add it to your project’s dependencies with your preferred package manager.
Using npm:
1npm install zustand
Using pnpm:
1pnpm add zustand
After installation, Zustand can be imported and used directly in your application code.
Store
@/store/useCookieStore.tsx
1import { create } from "zustand"2import { persist } from "zustand/middleware"34interface CookiePreferences {5 functional: boolean6 security: boolean7 analytics: boolean8 marketing: boolean9 userData: boolean10 adPersonalization: boolean11 contentPersonalization: boolean12}1314interface CookieStore {15 hasConsented: boolean | null16 preferences: CookiePreferences17 setPreference: (category: keyof CookiePreferences, value: boolean) => void18 acceptCookies: (preferences?: Partial<CookiePreferences>) => void19 declineCookies: () => void20}2122const date = new Date()23date.setFullYear(date.getFullYear() + 1)2425export const useCookieStore = create<CookieStore>()(26 persist(27 (set) => ({28 hasConsented: null,29 preferences: {30 functional: true,31 security: true,32 analytics: false,33 marketing: false,34 userData: false,35 adPersonalization: false,36 contentPersonalization: false,37 },38 setPreference: (category: keyof CookiePreferences, value: boolean) => {39 set((state) => ({40 preferences: {41 ...state.preferences,42 [category]:43 category === "functional" || category === "security"44 ? true45 : value,46 },47 }))48 },49 acceptCookies: (preferences?: Partial<CookiePreferences>) => {50 try {51 const newPreferences = {52 functional: true,53 security: true,54 analytics: true,55 marketing: true,56 userData: true,57 adPersonalization: true,58 contentPersonalization: true,59 ...preferences,60 }6162 document.cookie = `cookieConsent=true; expires=${date.toUTCString()}; path=/; SameSite=Lax; Secure`63 localStorage.setItem("cookieConsent", "true")64 localStorage.setItem(65 "cookiePreferences",66 JSON.stringify(newPreferences)67 )6869 {/* Remove this sessionStorage if you intend to make decline persist */}70 sessionStorage.removeItem("cookieConsent")7172 set({73 hasConsented: true,74 preferences: newPreferences,75 })76 } catch (error) {77 console.error("Error setting cookie consent:", error)78 set({ hasConsented: false })79 }80 },81 declineCookies: () => {82 try {83 const declinedPreferences = {84 functional: true,85 security: true,86 analytics: false,87 marketing: false,88 userData: false,89 adPersonalization: false,90 contentPersonalization: false,91 }9293 {/* Remove this localStorage and sessionStorage if you intend to make decline persist */}94 localStorage.removeItem("cookieConsent")95 localStorage.removeItem("cookiePreferences")96 sessionStorage.setItem("cookieConsent", "false")9798 {/* Remove commenting to persist declined cookieConsent */}99 {/* localStorage.setItem("cookieConsent", "false") */}100 {/* localStorage.setItem("cookiePreferences", JSON.stringify(declinedPreferences)) */}101102 set({103 hasConsented: false,104 preferences: declinedPreferences,105 })106 } catch (error) {107 console.error("Error declining cookie consent:", error)108 }109 },110 }),111 {112 name: "cookie-storage",113 onRehydrateStorage: () => (state) => {114 try {115 const local = localStorage.getItem("cookieConsent")116 const session = sessionStorage.getItem("cookieConsent")117118 if (session === "false") {119 return state?.declineCookies()120 }121 if (local === "true") {122 try {123 const storedPreferences =124 localStorage.getItem("cookiePreferences")125 const preferences = storedPreferences126 ? JSON.parse(storedPreferences)127 : undefined128 return state?.acceptCookies(preferences)129 } catch (error) {130 console.error("Error loading cookie preferences:", error)131 return state?.acceptCookies()132 }133 }134 } catch (error) {135 console.error("Error checking cookie consent:", error)136 return state?.declineCookies()137 }138 },139 }140 )141)142
Preference
The Preferences component manages user cookie consent configurations and must be rendered globally within the site's footer. This ensures users can access and modify their tracking preferences from any page. The CookieButton toggles the visibility of this modal interface, enabling compliance with privacy regulations such as GDPR and CCPA.
1"use client"23import { useState } from "react"45import { Preferences } from "@/components/cookies/preferences"67export function CookieButton() {8 const [showPreferences, setShowPreferences] = useState(false)910 return (11 <>12 <button13 onClick={() => setShowPreferences(true)}14 className="mt-2 cursor-pointer "15 >16 Cookie settings17 </button>18 <Preferences open={showPreferences} onOpenChange={setShowPreferences} />19 </>20 )21}22
Cloudflare
This Cloudflare Worker script proxies Google Tag Manager (gtm.js, ns.html) and Google Analytics (gtag/js) scripts through your domain, enabling these scripts to be served as first-party resources. This approach enhances privacy compliance, reduces the visibility of third-party network requests, and can help satisfy regulatory or client requirements regarding analytics loading.
How it works
- Path control: Only requests to /gtm.js, /gtag/js, or /ns.html are allowed and proxied. All other paths return a 404 response.
- ID injection: The script appends your GTM or GA ID as needed for each proxied resource.
- Caching: Results are cached on Cloudflare for 30 days (public, max-age=2592000, immutable), unless the gtm_debug parameter is present, improving performance and reliability.
- Header: Adjusts response headers to ensure correct content type and optimal cache settings.
1const GTM_ID = "GTM-XXXXXXX"; // Your Google Tag Manager ID2const GA_ID = "G-XXXXXXXXXX" // Your Google Analytics ID (Non Essential)3const GTM_BASE = "https://www.googletagmanager.com";45export default {6 async fetch(request) {7 const url = new URL(request.url);8 const { pathname, searchParams } = url;910 const allowedPaths = ["/gtm.js", "/gtag/js", "/ns.html"];11 if (!allowedPaths.includes(pathname)) {12 return new Response("Not found", { status: 404 });13 }1415// Start Comment: Adjust to suit your website url, or remove.16 const allowedReferers = [17 "https://yourdomain.com/",18 "https://www.yourdomain.com/"19 ]2021 const referer = request.headers.get("Referer");22 if (referer && !allowedReferers.some(domain => referer.startsWith(domain))) {23 return new Response("Forbidden: Invalid Referer", { status: 403 });24 }25// End Comment2627 const userAgent = request.headers.get("User-Agent");28 if (userAgent && userAgent.includes("Chrome-Lighthouse")) {29 return new Response("", {30 status: 200,31 headers: {32 "Content-Type": "application/javascript",33 "Cache-Control": "public, max-age=31536000, immutable",34 },35 });36 }3738 const upstreamUrl = new URL(`${GTM_BASE}${pathname}`);3940 if (pathname === "/gtm.js" || pathname === "/ns.html") {41 upstreamUrl.searchParams.set("id", GTM_ID);42 }4344 if (pathname === "/gtag/js") {45 upstreamUrl.searchParams.set("id", GA_ID);46 }4748 const cacheKey = new Request(upstreamUrl.toString(), request);49 const cache = caches.default;50 const isDebug = searchParams.has("gtm_debug");5152 if (!isDebug) {53 const cached = await cache.match(cacheKey);54 if (cached) return cached;55 }5657 try {58 const upstreamResponse = await fetch(upstreamUrl.toString(), {59 headers: request.headers,60 });6162 let body = await upstreamResponse.text();6364 const headers = new Headers(upstreamResponse.headers);65 headers.delete("Vary");66 if (pathname.endsWith(".js")) {67 headers.set("Content-Type", "application/javascript");68 } else {69 headers.set("Content-Type", "text/html; charset=utf-8");70 }71 headers.set("Cache-Control", "public, max-age=2592000, immutable");7273 const modified = new Response(body, {74 status: upstreamResponse.status,75 statusText: upstreamResponse.statusText,76 headers,77 });7879 if (!isDebug && upstreamResponse.ok) {80 await cache.put(cacheKey, modified.clone());81 }8283 return modified;84 } catch {85 return new Response("Upstream fetch failed", { status: 502 });86 }87 },88};89
Workers
Access workers in Cloudflare
Navigate to your account dashboard and go to:
Compute (Workers) → Workers & Pages → Get started

Create a new worker
Click “Start with Hello World!”, name the worker (e.g., tag), and click Deploy.

Replace the default code
Once the editor loads, replace the default "Hello World" code with your proxy logic.

Deploy the worker
Click Deploy in the top right corner to publish the latest version of your worker.
Set domain routes
Go to Settings tab of your Worker and define your public routes (e.g., https://tag.domain.com/gtag/js).
- Use exact path routes for /gtag/js, /gtm.js, and /ns.html.
- Make sure the workers.dev subdomain is disabled if not in use.

Google Tag
To run Google Analytics via GTM with full support for Consent Mode (v2), you need a minimal configuration that includes:
Enable consent overview
In your GTM container settings:
- Go to Admin > Container Settings
- Enable “Consent Overview (BETA)”
This enables Consent Mode UI for managing ad_storage, analytics_storage, and others.

Create Google Tag (GA4)
In your workspace:
- Add a Google Tag (GA4)
- Configure the Tag ID (your GA4 measurement ID)
- Enable Consent Settings with built-in checks:
- ad_storage
- ad_personalization
- ad_user_data
- analytics_storage
- Set the trigger to:
- Consent Initialization - All Pages
This ensures the tag initialises only after consent decisions are made.

Publish your changes
- Click Submit in the top right corner.
- Select “Publish and Create Version”
- Add a descriptive name and version summary (optional but recommended).
- Confirm the “All tags have been configured for consent” message.
- Click Publish.

Please be careful with cross-domain tracking, as this can become problematic with missing data in Google Analytics.
Centralising analytics and script management in Payload CMS and Next.js via Google Tag Manager (GTM) establishes a clear boundary between application logic and tracking infrastructure. This structured approach ensures regulatory compliance through consent synchronisation and first-party script hosting and significantly enhances site performance by reducing script bloat and preventing unnecessary execution.
Integrating providers such as <GTM /> and <CookieConsent /> within a unified context architecture, developers maintain consistent consent propagation and efficient analytics control. Further, leveraging a Cloudflare Worker to serve GTM and GA assets from a first-party domain improves trust, data control, and alignment with data privacy standards.