From a7bf0e3083adfbbdc50ed8fac5fbc620fd784db4 Mon Sep 17 00:00:00 2001 From: Matteo Date: Thu, 28 May 2026 12:10:42 +0200 Subject: [PATCH] cloud: force-dynamic root layout so GTM_ID is read at runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, Next.js statically prerenders the layout during `next build` and bakes in whatever GTM_ID was set in CI (empty). The cloud droplet's runtime env never gets a chance to set it, so the GTM script never renders. Forcing dynamic rendering keeps a single Docker image usable for both flavors: GTM_ID on the cloud droplet activates the banner, self-hosted containers leave it unset and ship nothing. Tradeoff: static optimization is lost for /, /login, /forgot-password, etc. Acceptable for an admin app — most pages already SSR per request. --- packages/frontend/src/app/layout.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/frontend/src/app/layout.tsx b/packages/frontend/src/app/layout.tsx index b617197..7b3c980 100644 --- a/packages/frontend/src/app/layout.tsx +++ b/packages/frontend/src/app/layout.tsx @@ -13,6 +13,15 @@ export const metadata: Metadata = { icons: { icon: '/icon.svg', apple: '/apple-icon.svg' }, }; +// process.env.GTM_ID is read by GoogleTagManager() during the layout +// render. Without `force-dynamic`, Next.js prerenders the layout at +// build time and bakes in whatever the env was during `next build` — +// which is empty in CI, so cloud builds would never get GTM. Forcing +// dynamic keeps a single Docker image working for both flavors: +// the runtime env on the cloud droplet enables GTM, and self-hosted +// containers leave it empty and ship nothing. +export const dynamic = 'force-dynamic'; + // Inline script to prevent FOUC — runs before React hydrates const themeScript = `(function(){try{var t=localStorage.getItem('theme');var d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme:dark)').matches);if(d)document.documentElement.classList.add('dark')}catch(e){}})()`;