Webclat / GTM Practice

What's the correct way to load GTM in a Next.js app using next/script (or @next/third-parties) without breaking CSP?

Quick answer

Use next/script's afterInteractive strategy (or the @next/third-parties GoogleTagManager component, which wraps the same pattern) rather than a plain script tag in _document or a useEffect injection - both of those either load too late for accurate pageview timing or bypass Next's own nonce propagation, which is what breaks under a strict CSP.

Why this happens

Next.js's next/script component integrates with Next's script-loading lifecycle and, critically, with its CSP nonce handling when you use the nonce prop or Next's built-in CSP support. A raw script tag dropped into _document.tsx or injected imperatively from useEffect does not get that integration, so it either loads at the wrong point in the page lifecycle (after hydration, missing early interactions) or gets blocked by a nonce-based CSP the same way any unmanaged inline script would.

Fix it

  1. For most cases, install @next/third-parties and use its GoogleTagManager component in your root layout, passing your container ID - it handles the loader script, the dataLayer bootstrap, and the noscript iframe fallback for you.
  2. If you need more control (for example a nonce from your own CSP middleware), use next/script directly with strategy="afterInteractive" and pass the same nonce your middleware generated for that request to the Script component's nonce prop.
  3. Do not use strategy="beforeInteractive" for GTM unless you specifically need tags to fire before hydration - it can delay Time to Interactive, and most tracking tags do not need to run that early.
  4. Pair this with the virtual pageview pattern (see the SPA route-change question) rather than assuming next/script's placement alone solves pageview tracking - it only solves script loading, not navigation events.
  5. If your CSP is nonce-based, confirm the nonce is generated once per request and applied consistently to both the CSP header and every nonce prop - a mismatch fails silently, the same as having no nonce at all.

How to verify it worked

View page source (not DevTools Elements, which shows the post-hydration DOM) on a fresh server-rendered load and confirm the GTM script tag carries the same nonce value present in the Content-Security-Policy response header. Check the Network tab for gtm.js loading with a 200 and no console CSP violation, and confirm in GTM Preview that the container connects on both a hard reload and a client-side route change.

Still stuck after working through this?

Send us what you are seeing in Preview and the Network tab. We trace GTM containers for a living and can usually tell you what is actually happening in one look.

Ask a GTM Engineer