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.
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.
Reading consent on the server
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}>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.