From e56c4ae182a6f0e52348a9270bdca229f9570fae Mon Sep 17 00:00:00 2001 From: Daviti Nalchevanidze Date: Tue, 30 Jun 2026 14:35:14 +0200 Subject: [PATCH 01/19] =?UTF-8?q?=F0=9F=90=9E=20fix(nextjs-ssr):=20Fix=20S?= =?UTF-8?q?SR=20rendering=20=E2=80=94=20always=20render=20children,=20use?= =?UTF-8?q?=20server-resolved=20optimization=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OptimizationProvider now always renders children (gates only when onStatesReady is set), enabling Next.js SSR to produce HTML from server components inside the provider tree - Add SSR stub in useOptimization so the hook never throws during SSR / first client render before useLayoutEffect fires - Introduce `getOptimizationData` (React.cache) as the single server-side entry point for optimization state, shared across pages in a request - Add `loadPageData` + `ResolvedPageData` abstraction in lib/resolution.ts: fetches entries, resolves variants, applies merge tags, and recursively resolves linked entries before passing to components - Refactor page components to use the new abstraction, removing per-page boilerplate - Rename LiveEntryCard → EntryCard.client.tsx and consolidate into a single EntryCard component that handles both server (resolved) and client (live-updates) render paths - Extract consent helpers to lib/consent.ts; expand lib/util.ts with entry/link type guards and resolveEntryLinks - Extend contentful.ts with buildEntryRegistry/extendEntryRegistry for deep link resolution - Update E2E_FLAGS to CSR,HYDRATION,SSR and fix seedIdentifiedProfile event shape in e2e utils Co-Authored-By: Claude Sonnet 4.6 --- implementations/nextjs-sdk_ssr/.env.example | 3 +- implementations/nextjs-sdk_ssr/AGENTS.md | 5 +- implementations/nextjs-sdk_ssr/app/layout.tsx | 27 ++-- .../nextjs-sdk_ssr/app/page-two/page.tsx | 50 ++----- implementations/nextjs-sdk_ssr/app/page.tsx | 91 +++---------- ...LiveEntryCard.tsx => EntryCard.client.tsx} | 10 +- .../nextjs-sdk_ssr/components/EntryCard.tsx | 93 ++++++------- .../nextjs-sdk_ssr/components/TrackingLog.tsx | 2 +- implementations/nextjs-sdk_ssr/lib/consent.ts | 10 ++ .../nextjs-sdk_ssr/lib/contentful.ts | 33 +++++ implementations/nextjs-sdk_ssr/lib/hooks.ts | 2 +- .../nextjs-sdk_ssr/lib/optimization.ts | 22 ++- .../nextjs-sdk_ssr/lib/resolution.ts | 128 ++++++++++++++++++ implementations/nextjs-sdk_ssr/lib/util.ts | 86 ++++++++++-- lib/e2e-web/e2e/utils.ts | 19 ++- .../react-web-sdk/src/hooks/ssrStub.ts | 100 ++++++++++++++ .../src/hooks/useOptimization.ts | 10 +- .../react-web-sdk/src/index.test.tsx | 10 +- ...ptimizationProvider.onStatesReady.test.tsx | 5 +- .../src/provider/OptimizationProvider.tsx | 9 +- 20 files changed, 511 insertions(+), 204 deletions(-) rename implementations/nextjs-sdk_ssr/components/{LiveEntryCard.tsx => EntryCard.client.tsx} (74%) create mode 100644 implementations/nextjs-sdk_ssr/lib/consent.ts create mode 100644 implementations/nextjs-sdk_ssr/lib/resolution.ts create mode 100644 packages/web/frameworks/react-web-sdk/src/hooks/ssrStub.ts diff --git a/implementations/nextjs-sdk_ssr/.env.example b/implementations/nextjs-sdk_ssr/.env.example index 07e791524..ef67a4e32 100644 --- a/implementations/nextjs-sdk_ssr/.env.example +++ b/implementations/nextjs-sdk_ssr/.env.example @@ -1,7 +1,6 @@ DOTENV_CONFIG_QUIET=true -# SKIP_NO_JS skips the JavaScript-disabled variant resolution suite (SSR, JavaScript disabled). -E2E_FLAGS=SSR,SKIP_NO_JS +E2E_FLAGS=CSR,HYDRATION,SSR APP_PORT=3001 PUBLIC_NINETAILED_CLIENT_ID="mock-client-id" diff --git a/implementations/nextjs-sdk_ssr/AGENTS.md b/implementations/nextjs-sdk_ssr/AGENTS.md index 64fe94551..f8f894457 100644 --- a/implementations/nextjs-sdk_ssr/AGENTS.md +++ b/implementations/nextjs-sdk_ssr/AGENTS.md @@ -16,9 +16,8 @@ server/client SDK composition; app code imports only Next.js SDK subpaths. ## E2E tests - All E2E tests live in `lib/e2e-web`. Hydration-specific specs are gated with `runIf('HYDRATION')` - and run automatically when `E2E_FLAGS=CSR,HYDRATION`. This implementation uses - `E2E_FLAGS=SSR,SKIP_NO_JS`: SSR enables the SSR suite, and `SKIP_NO_JS` skips the - JavaScript-disabled variant resolution suite (`Variant Resolution (SSR, JavaScript disabled)`). + and run automatically when `E2E_FLAGS=CSR,HYDRATION`. The `SSR` flag is intentionally omitted + because full SSR variant resolution has a known bug. - Entry cards must expose `data-ctfl-entry-id` on the `content-*` element so shared selectors work. - `test:e2e` delegates to `lib/e2e-web`. diff --git a/implementations/nextjs-sdk_ssr/app/layout.tsx b/implementations/nextjs-sdk_ssr/app/layout.tsx index a2c84a055..7c5bff7c0 100644 --- a/implementations/nextjs-sdk_ssr/app/layout.tsx +++ b/implementations/nextjs-sdk_ssr/app/layout.tsx @@ -2,13 +2,12 @@ import { GlobalLiveUpdatesProvider } from '@/components/GlobalLiveUpdatesProvide import { PreviewPanel } from '@/components/PreviewPanel' import { TrackingLog } from '@/components/TrackingLog' import { appConfig } from '@/lib/config' -import { getAppConsent } from '@/lib/util' +import { getOptimizationData } from '@/lib/optimization' import { NextAppAutoPageTracker, OptimizationRoot } from '@contentful/optimization-nextjs/client' import 'e2e-web/theme.css' import type { Metadata } from 'next' -import { cookies } from 'next/headers' import Link from 'next/link' -import { Suspense, type ReactNode } from 'react' +import { type ReactNode } from 'react' export const metadata: Metadata = { title: 'Optimization Next.js SDK SSR', @@ -16,9 +15,21 @@ export const metadata: Metadata = { 'Next.js App Router reference: the Next.js SDK resolves entries server-side and handles client-side tracking and interactive controls.', } +export const dynamic = 'force-dynamic' + export default async function RootLayout({ children }: Readonly<{ children: ReactNode }>) { - const cookieStore = await cookies() - const appConsent = getAppConsent(cookieStore) + const { data: optimizationData, hasConsent } = await getOptimizationData() + const defaults = optimizationData + ? { + profile: optimizationData.profile, + ...(hasConsent + ? { + selectedOptimizations: optimizationData.selectedOptimizations, + changes: optimizationData.changes, + } + : {}), + } + : undefined return ( @@ -30,17 +41,15 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac api={appConfig.api} trackEntryInteraction={{ views: true, clicks: true, hovers: true }} logLevel="debug" + defaults={defaults} app={{ name: 'Contentful Optimization Next.js SDK SSR (Client)', version: '0.1.0', }} - defaults={{ consent: appConsent, persistenceConsent: appConsent }} > - - - +