libconsent

Next.js

App Router, @next/third-parties, and reading consent on the server.

Setup

<ConsentProvider> is a client component. Mount it once in the root layout; everything under it stays a server component unless it reads consent itself.

app/layout.tsx
import { ConsentProvider } from "@libconsent/core";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ConsentProvider>{children}</ConsentProvider>
      </body>
    </html>
  );
}

@next/third-parties

@next/third-parties components go straight inside a gate:

import { GoogleAnalytics, GoogleTagManager } from "@next/third-parties/google";
import { CookieService } from "@libconsent/core";

<CookieService id="google-analytics">
  <GoogleAnalytics gaId="G-XXXXXXX" />
</CookieService>

<CookieService id="google-tag-manager" consentMode>
  <GoogleTagManager gtmId="GTM-XXXX" />
</CookieService>;

The first mounts once analytics consent exists. The second always mounts and lets Consent Mode gate the data, which is how Google recommends running GTM.

next/script

Load the CMP yourself if you want it in the document rather than injected:

<Script
  id="usercentrics-cmp"
  src="https://web.cmp.usercentrics.eu/ui/loader.js"
  id="AbCdEf123"
  strategy="beforeInteractive"
/>
<ConsentProvider adapter={usercentrics({ settingsId: "AbCdEf123", manual: true })}>

manual: true tells the adapter the script is already on the page, so it waits for the API instead of adding a second tag. Every adapter takes it.

Consent lives in the browser, so localStorage is no use to middleware or an RSC. Put it in a cookie instead:

import { local, cookieStorage } from "@libconsent/core";

const adapter = local({ storage: cookieStorage() });

<ConsentProvider adapter={adapter}>
middleware.ts
import { cookies } from "next/headers";

const raw = (await cookies()).get("libconsent")?.value;
const consent = raw ? JSON.parse(decodeURIComponent(raw)) : null;
const analytics = consent?.categories.analytics === true;

Build the adapter outside your component, or with useMemo. A new adapter object means a new connection.

Hydration

Gated components render their fallback on the server and swap once consent is known in the browser, so server and client agree on the first render. There is nothing to suppress.

Static export

Everything here works with output: "export"; this documentation site is one.

On this page