Now with AI-powered page building via MCP Server

Third-party scripts

Analytics and tag scripts are app code, not content. Where to mount them, which loading strategy to pick, and why the editor's CSP matters.

Analytics, tag managers, chat widgets, consent banners - these belong in your app, not in the CMS.

The reason is ownership. A script tag is executable code on every page; it is reviewed, versioned and deployed like the rest of your app. Putting one in an editable field turns "change a heading" and "run arbitrary JavaScript on production" into the same permission.

Analytics: use the official components

For Google Analytics and Tag Manager, do not hand-roll a script tag. @next/third-parties ships components that load the script and the init snippet with the right strategy:

// app/[[...path]]/layout.tsx
import { GoogleAnalytics, GoogleTagManager } from "@next/third-parties/google";

const gaId = process.env.NEXT_PUBLIC_GA_ID?.trim();
const gtmId = process.env.NEXT_PUBLIC_GTM_ID?.trim();

// ...inside <body>
{gtmId ? <GoogleTagManager gtmId={gtmId} /> : null}
{gaId ? <GoogleAnalytics gaId={gaId} /> : null}

A raw <Script src="...gtag/js?id=..."> loads the library but never configures it - you also need the inline gtag('config', id) call. That is exactly the kind of half-working setup these components exist to prevent.

Guarding on the environment variable is not politeness. It keeps analytics out of preview deployments and local development, so your numbers are not your own clicking.

Everything else: next/script

Chat widgets, support bubbles, social embeds and one-off vendor tags have no official component. Those go through next/script:

import Script from "next/script";

<Script src="https://widget.example.com/embed.js" strategy="lazyOnload" />

Choosing a strategy

The default is rarely the right one:

  • afterInteractive - the default, and correct for analytics and tag managers. Loads once the page is interactive.
  • lazyOnload - for anything the first screen does not need: chat widgets, support bubbles, social embeds.
  • beforeInteractive - only for scripts that must run before hydration, like a consent gate that decides whether other scripts may load at all. It blocks, so treat it as a last resort.

A chat widget on afterInteractive competes with your own hydration for the main thread. That is the most common way a fast cmssy site becomes a slow one - the pages are static, but the third-party payload is not.

The CSP will block them

This is the part that catches people.

createCmssyProxy applies a Content Security Policy so the cmssy admin can frame your site in the editor. That policy governs every script the page loads - so a newly added third-party script can be blocked by a header you did not write and may not know exists.

The symptom is specific: the script never runs, nothing appears in the network tab as an error, and the browser console shows a CSP violation naming the blocked origin. If a tag "just doesn't fire" in production but works locally, check the console before checking the tag manager.

When you add a script from a new origin, extend the policy to allow it.

If you gate scripts behind consent, the gate is app code too, and it has to run first. Put the consent logic on beforeInteractive and load the gated scripts conditionally afterwards.

Do not gate by rendering the consent banner as a CMS block. An editor unpublishing that block would silently remove the mechanism, not the tracking - and the site would keep collecting data with nothing on screen asking permission.

What belongs in the CMS instead

Not the script - the configuration. A measurement ID, a campaign flag, a feature toggle are all values, and values are safe to edit. Read them from site config or a page's custom fields and pass them into a script your app owns.

That keeps the editable surface at "which ID" rather than "which code".

Next steps

  • Routes and pages - the middleware that sets the CSP.
  • SEO - metadata, which is content rather than code.
  • Branding - site-level values worth editing.