Skip to content

fix(nextjs-ssr): Enable SSR rendering tests#344

Draft
David Nalchevanidze (nalchevanidze) wants to merge 21 commits into
mainfrom
fix-ssr
Draft

fix(nextjs-ssr): Enable SSR rendering tests#344
David Nalchevanidze (nalchevanidze) wants to merge 21 commits into
mainfrom
fix-ssr

Conversation

@nalchevanidze

@nalchevanidze David Nalchevanidze (nalchevanidze) commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Purpose

This PR sets up the Next.js SSR reference implementation and the E2E test suite that validates SSR rendering behaviour. It is intentionally free of any react-web-sdk changes — those are being explored in parallel experiment branches (see below).

The E2E tests in this PR are the acceptance criteria. The experiment PRs are candidate solutions; whichever approach is chosen will need to pass the same suite.

Note: E2E tests are expected to fail on this PR until one of the experiment PRs below is merged in. The tests require a react-web-sdk fix to pass — that fix is deliberately kept separate so the three approaches can be evaluated independently.

What's in this PR

nextjs-sdk_ssr implementation

  • getOptimizationData (React.cache) — single request-scoped server entry point for optimization state, shared across pages
  • loadPageData / ResolvedPageData in lib/resolution.ts — fetches entries, resolves variants, applies merge tags, recursively resolves linked entry links
  • EntryCard / EntryCard.client.tsx — unified server + client render path (replaces old LiveEntryCard)
  • Consent helpers extracted to lib/consent.ts; lib/util.ts expanded with entry/link type guards and resolveEntryLinks
  • buildEntryRegistry / extendEntryRegistry in contentful.ts for deep Contentful link resolution

E2E suite (lib/e2e-web/e2e/)

  • ssr.spec.ts — SSR first-paint state tests (consent + identified status, JS disabled) and Hydration test (no client Experience request after consented SSR)
  • utils.tsCONSENT_COOKIE, PROFILE_COOKIE, seedAnonymousProfile, seedIdentifiedProfile shared helpers
  • variant-resolution.spec.ts — SSR beforeEach hooks use the shared seed helpers; no inline cookie setup

Experiment PRs (react-web-sdk solutions)

The three PRs below each solve the same problem — how useOptimization behaves before useLayoutEffect fires (SSR + first client render) — in a different way. They all target the same E2E suite in this PR as their acceptance test.

PR Approach Trade-off
#349 experiment/ssr-stub Return a no-op stub from useOptimization No call-site changes; adds stub object to bundle
#346 experiment/undefined-sdk-path Return undefined from useOptimization Clean API; every hook call site needs a guard
#347 experiment/ssr-sdk-init Guard LocalStore + sync SDK init on server No stub needed; server initialises real SDK

Test plan

  • pnpm typecheck — no type errors
  • pnpm lint / pnpm implementation:lint
  • Spin up nextjs-sdk_ssr and verify SSR pages render with and without consent cookie
  • Run E2E suite with E2E_FLAGS=CSR,HYDRATION,SSR against the implementation — will pass once a solution from above is merged in

🤖 Generated with Claude Code

…rver-resolved optimization data

- 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 <noreply@anthropic.com>
- Remove unused `useManualViewTracking` hook and its `useOptimization` import
- Move `setAppConsent`/`getAppConsent` back into `util.ts`, delete `consent.ts`
- Fix double `resolveOptimizedEntry` call in `buildEntry` — cache result in variable
- Pass pre-resolved variant into `buildEntry` from `loadPageData` to avoid resolving twice
- Rename `applyMergeTags` → `resolveMergeTags`, flatten closure in merge tag walker into top-level `resolveMergeTagNode`
- Remove redundant `substituteMergeTags` wrapper
- Simplify `ResolvedPageData.resolve` from `flatMap` to `map + filter`
- Update AGENTS.md E2E flags note to match current `.env.example` (CSR,HYDRATION,SSR)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Delete app/globals.css (only contained @import 'tailwindcss', not imported anywhere)
- Delete postcss.config.mjs (only configured @tailwindcss/postcss plugin)
- Remove @tailwindcss/postcss, postcss, tailwindcss from devDependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

<CustomViewTracker componentId="page-two-hero" />
<ControlPanel demoCTA />
<NextjsOptimizationState data={optimizationData} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the reason you deleted this? Do you know what its purpose is?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure. was it probably not used? fist should check what it does.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component is required to pass state from the SSR context to the client-side context, and it must be a component because client hooks are not allowed in server components. Please try to understand what you are doing in the course of "fixing" an issue.

@nalchevanidze David Nalchevanidze (nalchevanidze) Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first of all, it was excluded by claude while fixing e2e tests and not having it did not broke anything. app functioned as it is(if it desired behavior that is required, lets add e2e test for it, and what it does).

after manual investigation: passing state is done by <OptimizationRoot defauls={}> and NextjsOptimizationState is just component to call hook hydrateOptimizationData -> await getRequiredBridge(sdk).hydrateOptimizationData(data) . interesting is what does it and why? why its not provider wraping whole app? and do we really need it if it does not affect apps functionality? and just naming does not give enough clarity to understand its necessity. if it ware provider to provide backend context. name would make perfect sence but then questions is why we have two places for that : OptimizationRoot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always happy to collaborate and discuss — I'd appreciate keeping the feedback constructive though.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Charles Hudson (@phobetron) David Nalchevanidze (@nalchevanidze) How can we make sure that we do not rely on knowing everything upfront? One would expect a test or a the compiler to let you know when you have broke something. If this is not needed for today's use case, my instinct would be to get rid of it.

Concretely, we need to invest in adding automated tests that ensure key functionality is not unintentionally changed. Claude will cover more ground than us as humans, and we need the safeguards to ensure we catch changes like that.

…logic

- PreviewPanel: replace module-level mutable flag with useRef — scoped to component instance, safe across HMR
- GlobalLiveUpdatesProvider: remove useMemo — context value object was recreated every render anyway so memo had no effect
- Inline buildEntryRegistry into loadPageData — it was a one-call wrapper; two extendEntryRegistry calls collapsed into one by combining baseline and variant entries into a single pass
- Remove buildEntryRegistry export and toIdMap import from contentful.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…etchEntry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…imization class

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…public/private boundary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion count to ControlPanel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lPanel and pass serverState as single prop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rverState from defaults shape

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After reset(), useProfileState() returns null intentionally, but null ?? serverState.profile
fell back to the SSR-resolved identified profile, keeping isIdentified true and hiding the
identify-button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ient

The client card rendered entry-card inside entry-card; collapse to a single
content div since the outer section.entry-card comes from EntryCard already.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
defaults was passing profile/selectedOptimizations but omitting
consent/persistenceConsent, so the SDK had no consent on startup
and blocked all tracking events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ardClient

The double-wrapper removal dropped data-testid="content-${testId}" which
live-updates E2E tests rely on for locating cards and reading data-test-entry-id.
Restore both testids with a minimal two-div structure, keeping entry-card class
removed (that was the actual duplicate).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ld to util

Collapse dual server/client interfaces into a single EntryCardProps, extract
isRichTextField into util.ts so both EntryCard and ServerOptimization share it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…okie utils

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nalchevanidze David Nalchevanidze (nalchevanidze) marked this pull request as draft July 1, 2026 11:55
…t/ssr-stub

The stub approach is now isolated in PR #349. fix-ssr contains only
the nextjs-sdk implementation and surrounding changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nalchevanidze David Nalchevanidze (nalchevanidze) changed the title fix(nextjs-ssr): Fix SSR rendering — always render children, use server-resolved optimization data fix(nextjs-ssr): Fix SSR rendering Jul 1, 2026
@nalchevanidze David Nalchevanidze (nalchevanidze) changed the title fix(nextjs-ssr): Fix SSR rendering fix(nextjs-ssr): Enable SSR rendering tests Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants