From 88a5c7178ef26de03345b33b57fe332e9b5743dc Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:04:10 +0200 Subject: [PATCH 01/17] init nextjs app --- documentation/spike-nextjs-support.md | 152 ++++++ .../react-web-sdk_nextjs/.gitignore | 41 ++ .../react-web-sdk_nextjs/AGENTS.md | 5 + .../react-web-sdk_nextjs/CLAUDE.md | 1 + .../react-web-sdk_nextjs/README.md | 36 ++ .../react-web-sdk_nextjs/app/favicon.ico | Bin 0 -> 25931 bytes .../react-web-sdk_nextjs/app/globals.css | 26 + .../react-web-sdk_nextjs/app/layout.tsx | 33 ++ .../react-web-sdk_nextjs/app/page.tsx | 65 +++ .../react-web-sdk_nextjs/eslint.config.mjs | 18 + .../react-web-sdk_nextjs/next.config.ts | 7 + .../react-web-sdk_nextjs/package.json | 26 + .../react-web-sdk_nextjs/pnpm-workspace.yaml | 3 + .../react-web-sdk_nextjs/postcss.config.mjs | 7 + .../react-web-sdk_nextjs/public/file.svg | 1 + .../react-web-sdk_nextjs/public/globe.svg | 1 + .../react-web-sdk_nextjs/public/next.svg | 1 + .../react-web-sdk_nextjs/public/vercel.svg | 1 + .../react-web-sdk_nextjs/public/window.svg | 1 + .../react-web-sdk_nextjs/tsconfig.json | 34 ++ session-ses_22be.md | 447 ++++++++++++++++++ 21 files changed, 906 insertions(+) create mode 100644 documentation/spike-nextjs-support.md create mode 100644 implementations/react-web-sdk_nextjs/.gitignore create mode 100644 implementations/react-web-sdk_nextjs/AGENTS.md create mode 100644 implementations/react-web-sdk_nextjs/CLAUDE.md create mode 100644 implementations/react-web-sdk_nextjs/README.md create mode 100644 implementations/react-web-sdk_nextjs/app/favicon.ico create mode 100644 implementations/react-web-sdk_nextjs/app/globals.css create mode 100644 implementations/react-web-sdk_nextjs/app/layout.tsx create mode 100644 implementations/react-web-sdk_nextjs/app/page.tsx create mode 100644 implementations/react-web-sdk_nextjs/eslint.config.mjs create mode 100644 implementations/react-web-sdk_nextjs/next.config.ts create mode 100644 implementations/react-web-sdk_nextjs/package.json create mode 100644 implementations/react-web-sdk_nextjs/pnpm-workspace.yaml create mode 100644 implementations/react-web-sdk_nextjs/postcss.config.mjs create mode 100644 implementations/react-web-sdk_nextjs/public/file.svg create mode 100644 implementations/react-web-sdk_nextjs/public/globe.svg create mode 100644 implementations/react-web-sdk_nextjs/public/next.svg create mode 100644 implementations/react-web-sdk_nextjs/public/vercel.svg create mode 100644 implementations/react-web-sdk_nextjs/public/window.svg create mode 100644 implementations/react-web-sdk_nextjs/tsconfig.json create mode 100644 session-ses_22be.md diff --git a/documentation/spike-nextjs-support.md b/documentation/spike-nextjs-support.md new file mode 100644 index 00000000..6eb379ed --- /dev/null +++ b/documentation/spike-nextjs-support.md @@ -0,0 +1,152 @@ +# [SPIKE] Next.js Support — React Web SDK / Next.js Implementation + +## Document Status + +Draft + +## Document Author / Owner + +Lotfi Arif + +## Contributor(s) + +(none yet) + +## PRD Links + +- PRD: Optimization React Web SDK + +## Reference Links + +- Confluence Project Page +- GitHub Repo + +--- + +## 1. Purpose + +Determine what it takes to fully support a Next.js application (SSR, SSG, bonus ESR) with the +Optimization SDK. Evaluate whether Node SDK + React Web SDK composition is sufficient or whether a +dedicated Next.js SDK is warranted. Produce a working reference implementation. + +## 2. Approaches + +### Approach A: Node SDK + React Web SDK (Composition) + +Customer manually wires Node SDK on the server and React Web SDK on the client — similar to the +existing `node-sdk+web-sdk` reference implementation but in a Next.js context. + +- Server: Node SDK calls Experience API with `preflight: true`, passes data as props +- Client: React Web SDK hydrates with server-provided data, takes over from there +- Cookie: `ctfl-opt-aid` shared between server and client for profile continuity + +| Dimension | Assessment | +| ----------------------- | ------------------------------------------------------------------------------------------------ | +| Complexity for customer | High — must manage cookies, Node SDK setup, `preflight`, data passing, `'use client'` boundaries | +| Maintenance for us | Low — no new package | +| Consistency | Low — every customer wires it differently | +| Error surface | High — easy to forget `preflight`, double-count events, or leak state | + +### Approach B: Dedicated Next.js SDK (`@contentful/optimization-next`) + +A thin package providing the standard glue code, composed from Node SDK + React Web SDK internals. +Would provide things like: + +- `getOptimizationData()` — async helper for Server Components / `getServerSideProps` +- `` / `` — server/client boundary pair +- `optimizationMiddleware()` — cookie management, optionally edge variant selection +- `withOptimization(gssp)` — Pages Router HOC + +| Dimension | Assessment | +| ----------------------- | ------------------------------------------------------------- | +| Complexity for customer | Low — single provider, familiar Next.js patterns | +| Maintenance for us | Medium — new package, but thin wrapper over existing SDKs | +| Consistency | High — every customer uses the same pattern | +| Error surface | Low — `preflight`, cookies, and boundaries handled internally | + +### Recommendation + +**Start with Approach A as a reference implementation** to prove the architecture works. If that +reveals significant boilerplate (which we expect it will), extract it into a dedicated Next.js SDK +(Approach B). The dedicated SDK is only warranted if there is standard glue code that most/all +integrations would need. + +## 3. What We Need to Know + +### Rendering Mode Feasibility + +**SSR — Feasible.** Node SDK (`CoreStateless`) is fully request-scoped and platform-agnostic. The +pattern is: server fetches optimization data with `preflight: true`, serializes it into page props, +client hydrates with that data. The existing `node-sdk+web-sdk` implementation already proves this +flow with Express; Next.js just changes the transport (props instead of EJS templates). + +**SSG — Partially feasible.** No real user at build time, so only baseline content can be resolved. +Personalization happens client-side after hydration (causes a content shift). ISR has the same +limitation unless the regeneration is request-triggered (effectively SSR). + +**ESR — Research-only.** `CoreStateless` is likely Edge-compatible (no Node-specific APIs, just +global `fetch`), but needs verification. The value would be running variant selection in Next.js +Middleware at the edge. Risk: cold start + API round-trip may negate latency benefits. + +### Key Technical Pieces + +1. **Dehydration/rehydration:** Server fetches `OptimizationData`, passes it to a client provider + that seeds the Web SDK state without making a fresh API call. In App Router this is natural + (Server Component props serialize automatically). In Pages Router, `getServerSideProps` returns + it as page props. + +2. **`'use client'` / `'use server'` boundaries:** The Web SDK and all React hooks must remain in + Client Components. Server-side data fetching (Node SDK) lives in Server Components or + `getServerSideProps`. + +3. **Cookie continuity:** `ctfl-opt-aid` cookie bridges server and client identity. Server reads it + from the request, passes it to Node SDK; Web SDK picks it up on hydration. + +4. **`preflight: true`:** Already exists in the API client. Server calls should use preflight so the + profile state is previewed but not persisted — client persists on hydration. + +### What Already Exists + +- `CoreStateless` / Node SDK — fully server-safe, request-scoped, no browser APIs +- `preflight` flag on Experience API client +- `ctfl-opt-aid` cookie constant exported from both Node and Web SDKs +- Next.js router adapters (`./router/next-app`, `./router/next-pages`) in React Web SDK +- `node-sdk+web-sdk` reference implementation demonstrating the SSR-to-client handoff pattern +- `typeof window === 'undefined'` guards throughout the Web SDK + +### What Is Missing + +- A way for `OptimizationProvider` to accept pre-fetched server data (currently always creates a + fresh Web SDK instance that calls the API) +- Server-side entry resolution without signals (the resolution logic in `CoreBase` is pure, but + `OptimizedEntry` reads from signals) +- Next.js-specific cookie reading (via `next/headers` `cookies()`) +- A reference implementation proving the full Next.js flow end-to-end + +## 4. Open Questions + +1. **App Router, Pages Router, or both?** Architecture differs significantly. Recommend App Router + primary, Pages Router secondary. + +2. **Should `preflight: true` be the default for server-side calls?** Probably yes (server previews, + client persists), but some customers may want server-side persistence for bots/crawlers. + +3. **Is Edge Runtime compatibility a hard requirement or nice-to-have?** Determines whether we need + to verify/fix transitive dependencies. + +4. **Is there appetite for Next.js Middleware?** Enables edge-based variant selection and A/B + routing but adds complexity. + +5. **What is the anonymous ID lifecycle for SSG?** No user at build time — always render baseline + and defer personalization to client? + +6. **How should the reference implementation handle Contentful entries?** Use `contentful.js`, or + mock/fixture approach like existing implementations? + +## 5. Next Steps + +1. Build a Next.js App Router reference implementation (`implementations/nextjs-react-web-sdk/`) + using Approach A (Node SDK + React Web SDK composition) +2. Evaluate how much boilerplate the reference implementation requires +3. Decide whether to extract into a dedicated Next.js SDK (Approach B) +4. Bonus: verify `CoreStateless` Edge Runtime compatibility diff --git a/implementations/react-web-sdk_nextjs/.gitignore b/implementations/react-web-sdk_nextjs/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/implementations/react-web-sdk_nextjs/AGENTS.md b/implementations/react-web-sdk_nextjs/AGENTS.md new file mode 100644 index 00000000..8bd0e390 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/implementations/react-web-sdk_nextjs/CLAUDE.md b/implementations/react-web-sdk_nextjs/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/implementations/react-web-sdk_nextjs/README.md b/implementations/react-web-sdk_nextjs/README.md new file mode 100644 index 00000000..e215bc4c --- /dev/null +++ b/implementations/react-web-sdk_nextjs/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/implementations/react-web-sdk_nextjs/app/favicon.ico b/implementations/react-web-sdk_nextjs/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/implementations/react-web-sdk_nextjs/app/globals.css b/implementations/react-web-sdk_nextjs/app/globals.css new file mode 100644 index 00000000..a2dc41ec --- /dev/null +++ b/implementations/react-web-sdk_nextjs/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/implementations/react-web-sdk_nextjs/app/layout.tsx b/implementations/react-web-sdk_nextjs/app/layout.tsx new file mode 100644 index 00000000..976eb90d --- /dev/null +++ b/implementations/react-web-sdk_nextjs/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/implementations/react-web-sdk_nextjs/app/page.tsx b/implementations/react-web-sdk_nextjs/app/page.tsx new file mode 100644 index 00000000..3f36f7c5 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/app/page.tsx @@ -0,0 +1,65 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
+

+ To get started, edit the page.tsx file. +

+

+ Looking for a starting point or more instructions? Head over to{" "} + + Templates + {" "} + or the{" "} + + Learning + {" "} + center. +

+
+ +
+
+ ); +} diff --git a/implementations/react-web-sdk_nextjs/eslint.config.mjs b/implementations/react-web-sdk_nextjs/eslint.config.mjs new file mode 100644 index 00000000..05e726d1 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/implementations/react-web-sdk_nextjs/next.config.ts b/implementations/react-web-sdk_nextjs/next.config.ts new file mode 100644 index 00000000..e9ffa308 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/implementations/react-web-sdk_nextjs/package.json b/implementations/react-web-sdk_nextjs/package.json new file mode 100644 index 00000000..d61abbc6 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/package.json @@ -0,0 +1,26 @@ +{ + "name": "react-web-sdk_nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "next": "16.2.4", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.4", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/implementations/react-web-sdk_nextjs/pnpm-workspace.yaml b/implementations/react-web-sdk_nextjs/pnpm-workspace.yaml new file mode 100644 index 00000000..581a9d5b --- /dev/null +++ b/implementations/react-web-sdk_nextjs/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +ignoredBuiltDependencies: + - sharp + - unrs-resolver diff --git a/implementations/react-web-sdk_nextjs/postcss.config.mjs b/implementations/react-web-sdk_nextjs/postcss.config.mjs new file mode 100644 index 00000000..61e36849 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/implementations/react-web-sdk_nextjs/public/file.svg b/implementations/react-web-sdk_nextjs/public/file.svg new file mode 100644 index 00000000..004145cd --- /dev/null +++ b/implementations/react-web-sdk_nextjs/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/public/globe.svg b/implementations/react-web-sdk_nextjs/public/globe.svg new file mode 100644 index 00000000..567f17b0 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/public/next.svg b/implementations/react-web-sdk_nextjs/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/implementations/react-web-sdk_nextjs/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/public/vercel.svg b/implementations/react-web-sdk_nextjs/public/vercel.svg new file mode 100644 index 00000000..77053960 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/public/window.svg b/implementations/react-web-sdk_nextjs/public/window.svg new file mode 100644 index 00000000..b2b2a44f --- /dev/null +++ b/implementations/react-web-sdk_nextjs/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/tsconfig.json b/implementations/react-web-sdk_nextjs/tsconfig.json new file mode 100644 index 00000000..3a13f90a --- /dev/null +++ b/implementations/react-web-sdk_nextjs/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/session-ses_22be.md b/session-ses_22be.md new file mode 100644 index 00000000..25bf0464 --- /dev/null +++ b/session-ses_22be.md @@ -0,0 +1,447 @@ +# SSR vs SSG in Next.js with edge personalization + +**Session ID:** ses_22be2983affe0ibj2AEPG9G17s +**Created:** 4/28/2026, 2:43:10 PM +**Updated:** 4/28/2026, 5:03:35 PM + +--- + +## User + +I need to discuss some things and it is best to go one by own here. +the first thing that i need to learn about is SSR in nextjs and what are its use cases and is it possible to always have pure SSR components? I would like to know the same case for SSG in nextJs as well +There was previously a module called Middleware (now proxy.js) that helps execute HTTP requests before they hit Next.js servers. Middleware functions are deployed at the cloud edge (e.g., CDNs or Edge workers). +I recommend including this in the reference implementation to show how the customer can use SSG while also serving personalized content by proxying requests to our Optimization API to fetch variants before NextJS renders the components, which could be useful. +I got the idea to research this from Sebastian, who mentioned that we had a customer (Home Depot) with a slow, static React frontend (cached) that did not work well with our personalization. That caused flicker on first paint, which was not ideal for them. The solution they came up with was to use an edge worker that receives the request and knows what content to serve before first paint. +Looking through the documentation, I can see that NextJS recommends avoiding its use unless there are no other options available. but would serve personalization with SSG and proxy actually work in theory if we are using something like the experience API that we have? + +--- + +## Assistant (Debug · nt-opus · 27.4s) + + + +This is a great architectural discussion. Let me address each point: + +## SSR in Next.js + +**Server-Side Rendering** generates HTML on every request at the server. Use cases: +- Pages needing real-time data (dashboards, user profiles) +- Personalized content per request +- SEO-critical pages with dynamic data + +**Can you have pure SSR components?** Yes, in the App Router (Next.js 13+), all components are Server Components by default. You only need `"use client"` when you need browser APIs, event handlers, or React state/effects. In Pages Router, `getServerSideProps` achieves SSR per-page. + +## SSG in Next.js + +**Static Site Generation** pre-renders pages at build time. Use cases: +- Marketing pages, blogs, docs +- Product listing pages with infrequent changes +- Any content that's the same for all users + +**Can you have pure SSG?** Yes, but the limitation is obvious: **SSG serves the same HTML to everyone**. There's no per-user personalization at render time. You can use ISR (Incremental Static Revalidation) to periodically rebuild, but it's still not per-user. + +## The Edge Middleware / Proxy Approach for SSG + Personalization + +This is the key architectural question, and **yes, it works in theory**. Here's how: + +1. **User request hits the edge** (Cloudflare Worker, Vercel Edge Middleware, etc.) +2. **Edge calls the Experience/Optimization API** with the user's context (cookies, geo, device, etc.) to resolve which variant to serve +3. **Edge rewrites/proxies the request** to the correct pre-built static page variant (e.g., `/homepage?variant=B` -> serves `/homepage-b.html`) +4. **Next.js serves the pre-rendered static page** -- zero flicker, fully cached, fast TTFB + +### Why this solves the Home Depot problem + +The flicker happened because: static HTML renders -> client JS hydrates -> JS calls personalization API -> DOM updates with new content (flash). With edge resolution, the correct content is served **before first paint**. + +### Practical considerations + +- **You need to pre-build all variants as static pages** (SSG with dynamic routes or params), which means a finite, known set of variants +- **The Experience API call adds latency at the edge**, but edge functions are typically 1-50ms from the user, so the round-trip to the Optimization API is the bottleneck. If the API is fast (~50-100ms), total overhead is acceptable +- **Caching at the edge** can help -- you can cache variant resolution by segment/audience key, not just per-user +- **Cookie-based routing** is common: edge sets a cookie with the variant, subsequent requests skip the API call entirely + +### Why Next.js discourages Middleware overuse + +Next.js warns against heavy Middleware because: +- It runs on **every matched request** (including static assets if not filtered) +- It has a limited runtime (Edge Runtime, no Node.js APIs) +- Complex logic there is harder to debug/test +- They'd prefer you use Server Components or Route Handlers + +**But personalization variant resolution is exactly the right use case for it** -- it's a lightweight, stateless decision that must happen before rendering. + +## Recommendation for the Reference Implementation + +Including a `proxy.js` / Middleware example that: + +1. Intercepts the request at the edge +2. Calls the Optimization/Experience API to resolve the variant +3. Rewrites to the correct pre-rendered SSG page + +...would be a strong demonstration. It shows customers they can have **both** the performance of SSG **and** personalized content without flicker. This is particularly compelling for large retail customers like Home Depot where TTFP and CLS are critical metrics. + +The main trade-off is that variant count must be bounded and pages pre-built, but for most A/B testing and personalization scenarios (hero banners, CTAs, layouts), that's entirely feasible. + +Want me to look at the current reference implementation in this repo to see how this could be integrated? + +--- + +## User + +with the practical considerations, we would need to build all the variants for this right? with the way that we do it now i am sure there is a contentful component that we can use to feed in the entry IDs that we get from the experience API which renders those entries to a degree right? since its coming from contentful anyways. + +--- + +## Assistant (Debug · nt-opus · 13.1s) + + + +You're right, and that's an important distinction that simplifies things significantly. + +## You don't need to pre-build every variant + +The flow would be: + +1. **Edge receives request** -> calls Experience API -> gets back the winning variant (which is a Contentful entry ID) +2. **Edge passes that entry ID** to Next.js (via header, cookie, or rewrite param) +3. **Next.js page (SSG with ISR or SSR)** receives the entry ID and fetches that entry from Contentful at render time +4. **The Contentful component renders whatever entry it gets** -- it doesn't care which variant it is + +So the page template is generic -- it's just "render this Contentful entry." The personalization decision (which entry) is made at the edge, but the actual content comes from Contentful's API as it always does. + +### This means: + +- **No need to pre-build N variants per page** -- you have one page/template that accepts an entry ID +- **Content authors can add/remove variants in Contentful** without triggering rebuilds +- **The Contentful SDK components already handle rendering entries** -- you just need to pass the right entry ID to them +- **SSG still works** for the page shell/layout, with the personalized content block being the dynamic part resolved at the edge + +### The realistic flow becomes: + +``` +User -> Edge/Proxy -> Experience API -> "serve entry xyz123" + -> Next.js (cached template) + Contentful fetch(xyz123) -> Full HTML +``` + +The only latency cost is the Experience API call at the edge + the Contentful fetch for that specific entry. And if you're already using Contentful's CDN, that fetch is fast. + +This is much more practical than pre-building all variant combinations. The page is essentially a shell that says "render whatever Contentful entry I'm told to," and the edge decides which entry that is. + +--- + +## User + +lets not start giving recommendations but its more me trying to learn about SSR and SSG and how I can implement it +how the HTML looks like in SSR if i were to implement react web sdk with the node SDK inside of a nextjs app +what are the benefits and some detailed technical components of how it works under the hood and how i can be an expert on it. + +--- + +## Assistant (Debug · nt-opus · 49.5s) + + + +## SSR Under the Hood in Next.js + +### What actually happens on a request + +``` +Browser sends GET /page + ↓ +Next.js server receives request + ↓ +Server executes your React component tree as JavaScript + ↓ +React's renderToString() / renderToReadableStream() converts +the component tree into an HTML string + ↓ +Server sends complete HTML to browser + ↓ +Browser paints immediately (no blank screen) + ↓ +React JS bundle downloads + ↓ +React "hydrates" — attaches event listeners to existing DOM +``` + +### The HTML output + +When the server renders a page using SSR, the browser receives **fully formed HTML**. If you had a component like: + +```jsx +// Server Component in App Router (SSR by default) +export default async function Page() { + // This runs on the server — Node SDK call + const experience = await fetchExperience(experienceId); + + return ( +
+

Welcome back, John

+ +

Special offer just for you

+
+ ); +} +``` + +The browser receives something like: + +```html + + + + + + + +
+
+

Welcome back, John

+ +

Special offer just for you

+
+
+ + + +``` + +Key things to notice: +- **The content is in the HTML** — not injected later by JS +- **`__NEXT_DATA__`** is a JSON blob that Next.js injects so React on the client can hydrate without re-fetching the same data +- **Scripts are `defer`** — they load after HTML parsing, so paint happens first + +### Hydration — the critical concept + +Once the HTML is painted, React needs to "take over" the DOM to make it interactive. This is hydration: + +1. React downloads and executes in the browser +2. It rebuilds the virtual DOM tree in memory +3. It walks the existing server-rendered DOM and **attaches event listeners** to matching nodes +4. It does NOT re-render — it assumes the server HTML matches what React would produce client-side + +**If there's a mismatch** (server rendered "Welcome back, John" but client-side state says "Guest"), React throws a hydration error and may re-render, causing flicker. This is exactly what happens with client-side personalization on static pages — the server HTML doesn't match what the client wants to show. + +### Server Components vs Client Components (App Router) + +``` +Server Component (default) +├── Runs ONLY on the server +├── Can use async/await, Node APIs, Node SDK +├── Output is HTML + React Server Component Payload (RSC Payload) +├── NEVER ships JS to the browser +└── Cannot use useState, useEffect, onClick, etc. + +Client Component ("use client") +├── Renders on server first (for initial HTML) AND on client +├── Ships JS bundle to browser for hydration +├── Can use hooks, browser APIs, event handlers +└── React Web SDK components would be this if they need interactivity +``` + +The RSC Payload is a binary format that describes the server component tree. It's streamed alongside HTML and lets React on the client understand the component boundaries without needing the server component code. + +### How Node SDK + React Web SDK would fit + +``` +Server Component (Node SDK) +│ +│ Runs on server: fetches experiences, resolves variants, +│ gets Contentful entries — all in Node.js +│ +├── Passes resolved data as props to: +│ +│ ├── Server Components (no interactivity needed) +│ │ → Pure HTML, zero JS shipped +│ │ → Contentful rich text, images, static content +│ │ +│ └── Client Components ("use client", React Web SDK) +│ → Interactive elements (carousels, forms, analytics) +│ → Hydrates on client with event listeners +│ → The Web SDK JS ships in the bundle +``` + +In practice: + +```jsx +// app/page.tsx — Server Component +import { getExperience } from '@ninetailed/node-sdk'; +import { HeroBanner } from './HeroBanner'; // client component + +export default async function Page() { + const experience = await getExperience({ + experienceId: 'xxx', + profile: getProfileFromCookies(), + }); + + const entry = await fetchContentfulEntry(experience.variantId); + + // Data resolved on server, passed as serialized props + return ; +} +``` + +```jsx +// app/HeroBanner.tsx — Client Component +"use client"; + +export function HeroBanner({ content }) { + // This component hydrates on the client + // content was resolved on the server — no flicker + return ( +
+

{content.fields.headline}

+
+ ); +} +``` + +### What the network looks like + +**First request (SSR):** +``` +GET /page → 200 HTML (~50-200ms TTFB) +GET /_next/static/chunks/main.js → JS bundle (cached) +GET /_next/static/chunks/page.js → Page-specific JS (cached) +``` + +**The HTML response is complete.** The user sees content immediately. JS loads in background for interactivity. + +### Benefits — the technical specifics + +**1. TTFB to First Contentful Paint** +- CSR: browser gets empty `
` → downloads JS → executes → fetches data → renders. Could be 2-4s on slow connections. +- SSR: browser gets full HTML → paints. TTFB is slower (server has to render) but FCP is much faster because there's no JS execution in the critical path. + +**2. No layout shift from personalization** +- The server already resolved the variant. The HTML contains the correct content. There's no "swap" after hydration. +- CLS (Cumulative Layout Shift) stays at 0 for personalized content. + +**3. SEO** +- Crawlers get complete HTML. Googlebot can execute JS, but other crawlers can't, and even Google prefers server-rendered content. + +**4. Streaming SSR (React 18+)** +- Next.js App Router uses `renderToReadableStream()` which streams HTML chunks as they resolve +- If one component is slow (e.g., waiting on Experience API), the rest of the page streams first, and a `` fallback shows until it resolves +- The browser starts painting before the full response is complete + +```jsx +export default async function Page() { + return ( +
+
{/* streams immediately */} + }> + {/* streams when Experience API responds */} + +
{/* streams immediately */} +
+ ); +} +``` + +**5. The `__NEXT_DATA__` / RSC Payload contract** +- In Pages Router: server serializes props into `__NEXT_DATA__` JSON, client React reads it during hydration so it doesn't re-fetch +- In App Router: the RSC Payload is a streaming binary format that describes the server component output, more efficient than JSON serialization +- Either way, data fetched on the server is passed to the client without a second network request + +### What you need to understand to be expert-level + +1. **The rendering pipeline**: request → server render → HTML stream → client JS download → hydration → interactive +2. **Hydration mismatches**: the #1 source of bugs. Server and client must produce identical DOM for the initial render +3. **Serialization boundary**: data passing from server to client components must be JSON-serializable (no functions, no classes, no circular refs) +4. **The cost of "use client"**: every client component's JS ships to the browser. Pushing the "use client" boundary as deep as possible in the tree keeps bundle size small +5. **Caching layers**: Next.js has multiple caches (Full Route Cache, Data Cache, Router Cache) that interact with SSR. Understanding when a page is dynamically rendered vs statically cached is critical +6. **`dynamic = 'force-dynamic'` vs `revalidate`**: these control whether Next.js treats a route as SSR (every request) or ISR (cached, revalidated periodically) + +--- + +## User + +i have this written about SSG +is ther esomething i can write about SSR that is a similar style? +NextJS Personalization with SSG +There was previously a module called Middleware (now proxy.js) that helps execute HTTP requests before they hit Next.js servers. Middleware functions are deployed at the cloud edge (e.g., CDNs or Edge workers). +I recommend including this in the reference implementation to show how the customer can use SSG while also serving personalized content by proxying requests to our Optimization API to fetch variants before NextJS renders the components, which could be useful. +I got the idea to research this from Sebastian, who mentioned that we had a customer (Home Depot) with a slow, static React frontend (cached) that did not work well with our personalization. That caused a flicker on the first paint, which was not ideal for them. The solution they came up with was to use an edge worker that receives the request and knows what content to serve before first paint. +Looking through the documentation, I can see that NextJS recommends avoiding its use unless there are no other options available. + [Image 1] + +--- + +## Assistant (Debug · nt-opus · 19.5s) + + + +Here's an SSR counterpart in the same style: + +--- + +**NextJS Personalization with SSR** + +With SSR, personalization happens on the server at request time — no edge proxy or middleware needed. When a request comes in, the server executes the Node SDK to call the Experience API, resolves the winning variant, fetches the corresponding Contentful entry, and returns fully rendered HTML with the personalized content already in place. The browser paints the correct variant on first load with zero flicker and zero layout shift. + +This is the most straightforward approach to personalization in Next.js because the rendering pipeline naturally supports it — there's no architectural workaround required. The server already has access to request context (cookies, headers, geo) and can pass that to the Experience API before React ever touches the component tree. The resolved content flows down as props, and the HTML the browser receives is complete and correct. + +The trade-off compared to SSG is latency. Every request hits the server, which must call the Experience API and Contentful before responding. There's no cached static page to fall back on. For high-traffic pages where the variant set is small and doesn't change per-user, SSG with edge resolution may perform better. But for pages where personalization is highly dynamic (per-user profile, real-time segmentation, many variant combinations), SSR is simpler to implement and avoids the complexity of pre-building variants or maintaining an edge proxy layer. + +In the App Router, this is the default behavior — components are Server Components and can call the Node SDK directly with async/await. In the Pages Router, `getServerSideProps` achieves the same thing. Either way, the client never sees a loading state or content swap for the personalized block. + +I'd recommend including this in the reference implementation alongside the SSG + proxy approach so customers can compare both patterns and choose based on their traffic profile and performance requirements. + +--- + +Want me to adjust the tone or add/remove anything? + +--- + +## User + +but with calling the experience API it would not use the SDK which is what we would want to use to have further personalization with useing the SDK methods like consent and everything not just the personalization right? +so what is a better way of doing this in a full way? with supporting SSR on nextJs + +--- + +## Assistant (Debug · nt-opus · 14.5s) + + + +You're right. Calling the Experience API directly only gives you variant resolution — you lose the full SDK surface (consent management, event tracking, profile merging, `identify`, `track`, `page`, etc.). + +## The full approach for SSR with the SDK + +You need **both** SDKs working together: + +**Server side (Node SDK)** handles the initial render: +- Resolves experiences/variants at request time using profile context +- Fetches the correct Contentful entries +- Returns fully rendered HTML — no flicker + +**Client side (React Web SDK)** hydrates and takes over: +- Manages the ongoing user session (profile, consent, traits) +- Tracks events (`track`, `page`) +- Handles `identify` for known users +- Manages consent state +- Listens for profile changes and can re-evaluate experiences client-side if the profile updates mid-session (e.g., user logs in, consent changes, new traits added) + +The flow looks like: + +``` +Request → Server (Node SDK resolves variant, renders HTML) + → Browser paints correct content immediately + → React hydrates + → Web SDK initializes with the same profile state + → Web SDK handles all ongoing interactions: + consent, track, identify, page, etc. + → If profile changes mid-session, Web SDK + re-evaluates and can swap variants client-side +``` + +The key is that the **Node SDK handles the first render** (solving flicker) and the **Web SDK handles everything after** (solving the full personalization lifecycle). The server passes the initial profile/variant state to the client so the Web SDK doesn't start from scratch — it picks up where the Node SDK left off, avoiding a hydration mismatch. + +This is the pattern worth documenting in the reference implementation: not just "how to get the right variant on first paint" but "how to run the full personalization lifecycle with SSR as the foundation." + +--- + From 69b673fbc45df264219e33211779e6b5441fcdf8 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Mon, 4 May 2026 11:46:15 +0200 Subject: [PATCH 02/17] scaffold Next.js reference implementation with monorepo conventions Restructure the bare create-next-app scaffold into a proper monorepo implementation with both React Web SDK and Node SDK dependencies, PM2-managed mock/serve scripts, env config, and AGENTS.md aligned with existing implementation patterns. --- .../react-web-sdk_nextjs/.env.example | 16 ++++ .../react-web-sdk_nextjs/.gitignore | 9 +- implementations/react-web-sdk_nextjs/.npmrc | 1 + .../react-web-sdk_nextjs/AGENTS.md | 66 ++++++++++++++- .../react-web-sdk_nextjs/CLAUDE.md | 1 - .../react-web-sdk_nextjs/README.md | 65 +++++++++----- .../react-web-sdk_nextjs/app/globals.css | 27 +----- .../react-web-sdk_nextjs/app/layout.tsx | 31 ++----- .../react-web-sdk_nextjs/app/page.tsx | 84 +++++-------------- .../react-web-sdk_nextjs/next.config.ts | 21 ++++- .../react-web-sdk_nextjs/package.json | 68 ++++++++++++--- .../react-web-sdk_nextjs/public/file.svg | 1 - .../react-web-sdk_nextjs/public/globe.svg | 1 - .../react-web-sdk_nextjs/public/next.svg | 1 - .../react-web-sdk_nextjs/public/vercel.svg | 1 - .../react-web-sdk_nextjs/public/window.svg | 1 - .../react-web-sdk_nextjs/tsconfig.json | 10 ++- package.json | 1 + 18 files changed, 238 insertions(+), 167 deletions(-) create mode 100644 implementations/react-web-sdk_nextjs/.env.example create mode 100644 implementations/react-web-sdk_nextjs/.npmrc delete mode 100644 implementations/react-web-sdk_nextjs/CLAUDE.md delete mode 100644 implementations/react-web-sdk_nextjs/public/file.svg delete mode 100644 implementations/react-web-sdk_nextjs/public/globe.svg delete mode 100644 implementations/react-web-sdk_nextjs/public/next.svg delete mode 100644 implementations/react-web-sdk_nextjs/public/vercel.svg delete mode 100644 implementations/react-web-sdk_nextjs/public/window.svg diff --git a/implementations/react-web-sdk_nextjs/.env.example b/implementations/react-web-sdk_nextjs/.env.example new file mode 100644 index 00000000..9273d7d6 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/.env.example @@ -0,0 +1,16 @@ +DOTENV_CONFIG_QUIET=true + +PUBLIC_NINETAILED_CLIENT_ID="mock-client-id" +PUBLIC_NINETAILED_ENVIRONMENT="main" + +PUBLIC_EXPERIENCE_API_BASE_URL="http://localhost:8000/experience/" +PUBLIC_INSIGHTS_API_BASE_URL="http://localhost:8000/insights/" + +PUBLIC_CONTENTFUL_TOKEN="mock-token" +PUBLIC_CONTENTFUL_PREVIEW_TOKEN="mock-preview-token" +PUBLIC_CONTENTFUL_ENVIRONMENT="master" +PUBLIC_CONTENTFUL_SPACE_ID="mock-space-id" + +PUBLIC_CONTENTFUL_CDA_HOST="localhost:8000" +PUBLIC_CONTENTFUL_BASE_PATH="contentful" +PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL="false" diff --git a/implementations/react-web-sdk_nextjs/.gitignore b/implementations/react-web-sdk_nextjs/.gitignore index 5ef6a520..818f88a7 100644 --- a/implementations/react-web-sdk_nextjs/.gitignore +++ b/implementations/react-web-sdk_nextjs/.gitignore @@ -1,5 +1,3 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - # dependencies /node_modules /.pnp @@ -12,6 +10,8 @@ # testing /coverage +/playwright-report +/test-results # next.js /.next/ @@ -30,8 +30,9 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* +# env files +.env +.env*.local # vercel .vercel diff --git a/implementations/react-web-sdk_nextjs/.npmrc b/implementations/react-web-sdk_nextjs/.npmrc new file mode 100644 index 00000000..135f7a0d --- /dev/null +++ b/implementations/react-web-sdk_nextjs/.npmrc @@ -0,0 +1 @@ +shared-workspace-lockfile=false diff --git a/implementations/react-web-sdk_nextjs/AGENTS.md b/implementations/react-web-sdk_nextjs/AGENTS.md index 8bd0e390..e92cd376 100644 --- a/implementations/react-web-sdk_nextjs/AGENTS.md +++ b/implementations/react-web-sdk_nextjs/AGENTS.md @@ -1,5 +1,63 @@ - -# This is NOT the Next.js you know +# AGENTS.md -This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. - +Read the repository root `AGENTS.md` first. + +## Scope + +This is the Next.js (App Router) reference implementation demonstrating +`@contentful/optimization-react-web` for client-side optimization and +`@contentful/optimization-node` for server-side optimization in an SSR context. + +It shows two integration patterns: + +1. **Client-resolved**: entries are resolved entirely on the client via the React SDK +2. **Server-pre-resolved**: entries are resolved on the server via the Node SDK and hydrated on the + client + +## Key Paths + +- `app/` — Next.js App Router pages, layouts, and route handlers +- `lib/` — shared utilities (SDK config, Contentful client, server-side helpers) +- `components/` — React components (client and server) +- `.env.example` +- `middleware.ts` — Next.js middleware for cookie-based profile management + +## Local Rules + +- This implementation uses the Next.js App Router exclusively. Do not add Pages Router files. +- All SDK usage on the client must live inside `"use client"` components. Server Components must not + import from `@contentful/optimization-react-web` or `@contentful/optimization-web`. +- Server-side SDK usage must import from `@contentful/optimization-node` only. +- The React SDK (`OptimizationRoot`) must be initialized in a single client component wrapper in the + root layout. Do not create multiple provider instances. +- Do not add a `src/optimization/` directory. Follow the same direct-import pattern as + `implementations/react-web-sdk`. +- If you changed a consumed package, run `pnpm build:pkgs` and reinstall this implementation before + trusting local results. +- `serve` uses PM2-managed processes. Use `serve:stop` when done. + +## Common Failure Modes + +- Package changes are not reflected here: rerun `pnpm build:pkgs`, then + `pnpm implementation:run -- react-web-sdk_nextjs implementation:install`. +- The app or mocks fail to bind local ports such as `3000` or `8000`: stop only this + implementation's local processes with + `pnpm implementation:run -- react-web-sdk_nextjs serve:stop`. +- Behavior differs from the documented mock setup: compare `.env` with `.env.example` before + changing code. +- Next.js caching stale data: run `pnpm clean` to clear the `.next` directory. + +## Commands + +- `pnpm implementation:run -- react-web-sdk_nextjs implementation:install` +- `pnpm implementation:run -- react-web-sdk_nextjs typecheck` +- `pnpm implementation:run -- react-web-sdk_nextjs build` +- `pnpm implementation:run -- react-web-sdk_nextjs dev` +- `pnpm implementation:run -- react-web-sdk_nextjs serve` +- `pnpm implementation:run -- react-web-sdk_nextjs serve:stop` + +## Usually Validate + +- Run `typecheck` for local code changes. +- Run `build` when changing production bundling behavior. +- There are no meaningful unit tests here. diff --git a/implementations/react-web-sdk_nextjs/CLAUDE.md b/implementations/react-web-sdk_nextjs/CLAUDE.md deleted file mode 100644 index 43c994c2..00000000 --- a/implementations/react-web-sdk_nextjs/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/implementations/react-web-sdk_nextjs/README.md b/implementations/react-web-sdk_nextjs/README.md index e215bc4c..159484bc 100644 --- a/implementations/react-web-sdk_nextjs/README.md +++ b/implementations/react-web-sdk_nextjs/README.md @@ -1,36 +1,55 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Next.js React Web SDK Reference Implementation -## Getting Started +Next.js App Router reference implementation demonstrating `@contentful/optimization-react-web` +(client-side) and `@contentful/optimization-node` (server-side SSR) working together. -First, run the development server: +## Architecture -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +This implementation shows two integration patterns: -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +- **Client-Resolved** (`/client-resolved`): Entries are resolved entirely in the browser via the + React SDK. The server renders an HTML shell; the Web SDK resolves optimizations client-side. +- **Server-Resolved** (`/server-resolved`): Entries are pre-resolved on the server via the Node SDK + and passed as props to client components for hydration with full interactivity. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +Both patterns share a cookie-based profile (`ctfl-opt-aid`) managed by Next.js middleware, ensuring +the same anonymous profile is used across server and client. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +## Setup -## Learn More +```bash +# From the repository root: +pnpm build:pkgs +pnpm implementation:run -- react-web-sdk_nextjs implementation:install -To learn more about Next.js, take a look at the following resources: +# Copy env: +cp implementations/react-web-sdk_nextjs/.env.example implementations/react-web-sdk_nextjs/.env +``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## Development -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +```bash +# Start mock server + dev server: +pnpm implementation:run -- react-web-sdk_nextjs dev -## Deploy on Vercel +# Or with PM2-managed processes: +pnpm implementation:run -- react-web-sdk_nextjs serve +pnpm implementation:run -- react-web-sdk_nextjs serve:stop +``` -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## Key Files -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +``` +app/ + layout.tsx # Root layout (Server Component) wraps children in client provider + page.tsx # Home page with navigation to both patterns + client-resolved/ # Client-side optimization resolution demo + server-resolved/ # Server-side pre-resolution + client hydration demo +lib/ + config.ts # Shared SDK configuration from env vars + contentful-client.ts # Contentful CDA client + optimization-server.ts # Node SDK singleton for server-side use +components/ + OptimizationProvider.tsx # "use client" wrapper for +middleware.ts # Cookie-based profile management +``` diff --git a/implementations/react-web-sdk_nextjs/app/globals.css b/implementations/react-web-sdk_nextjs/app/globals.css index a2dc41ec..d4b50785 100644 --- a/implementations/react-web-sdk_nextjs/app/globals.css +++ b/implementations/react-web-sdk_nextjs/app/globals.css @@ -1,26 +1 @@ -@import "tailwindcss"; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} +@import 'tailwindcss'; diff --git a/implementations/react-web-sdk_nextjs/app/layout.tsx b/implementations/react-web-sdk_nextjs/app/layout.tsx index 976eb90d..8835a4e0 100644 --- a/implementations/react-web-sdk_nextjs/app/layout.tsx +++ b/implementations/react-web-sdk_nextjs/app/layout.tsx @@ -1,33 +1,20 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +import type { Metadata } from 'next' +import './globals.css' export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; + title: 'Optimization Next.js Reference Implementation', + description: + 'Next.js App Router reference implementation using @contentful/optimization-react-web and @contentful/optimization-node', +} export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode }>) { return ( - + {children} - ); + ) } diff --git a/implementations/react-web-sdk_nextjs/app/page.tsx b/implementations/react-web-sdk_nextjs/app/page.tsx index 3f36f7c5..65e73685 100644 --- a/implementations/react-web-sdk_nextjs/app/page.tsx +++ b/implementations/react-web-sdk_nextjs/app/page.tsx @@ -1,65 +1,25 @@ -import Image from "next/image"; - export default function Home() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
- -
-
- ); +
+

Optimization Next.js Reference Implementation

+

+ This implementation demonstrates two integration patterns with the Contentful Optimization + SDKs in a Next.js App Router application. +

+ +
+ ) } diff --git a/implementations/react-web-sdk_nextjs/next.config.ts b/implementations/react-web-sdk_nextjs/next.config.ts index e9ffa308..9333b88a 100644 --- a/implementations/react-web-sdk_nextjs/next.config.ts +++ b/implementations/react-web-sdk_nextjs/next.config.ts @@ -1,7 +1,20 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next' const nextConfig: NextConfig = { - /* config options here */ -}; + serverExternalPackages: ['@contentful/optimization-node'], + env: { + PUBLIC_NINETAILED_CLIENT_ID: process.env.PUBLIC_NINETAILED_CLIENT_ID, + PUBLIC_NINETAILED_ENVIRONMENT: process.env.PUBLIC_NINETAILED_ENVIRONMENT, + PUBLIC_EXPERIENCE_API_BASE_URL: process.env.PUBLIC_EXPERIENCE_API_BASE_URL, + PUBLIC_INSIGHTS_API_BASE_URL: process.env.PUBLIC_INSIGHTS_API_BASE_URL, + PUBLIC_CONTENTFUL_TOKEN: process.env.PUBLIC_CONTENTFUL_TOKEN, + PUBLIC_CONTENTFUL_PREVIEW_TOKEN: process.env.PUBLIC_CONTENTFUL_PREVIEW_TOKEN, + PUBLIC_CONTENTFUL_ENVIRONMENT: process.env.PUBLIC_CONTENTFUL_ENVIRONMENT, + PUBLIC_CONTENTFUL_SPACE_ID: process.env.PUBLIC_CONTENTFUL_SPACE_ID, + PUBLIC_CONTENTFUL_CDA_HOST: process.env.PUBLIC_CONTENTFUL_CDA_HOST, + PUBLIC_CONTENTFUL_BASE_PATH: process.env.PUBLIC_CONTENTFUL_BASE_PATH, + PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL: process.env.PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL, + }, +} -export default nextConfig; +export default nextConfig diff --git a/implementations/react-web-sdk_nextjs/package.json b/implementations/react-web-sdk_nextjs/package.json index d61abbc6..cb4e8eb1 100644 --- a/implementations/react-web-sdk_nextjs/package.json +++ b/implementations/react-web-sdk_nextjs/package.json @@ -1,26 +1,68 @@ { - "name": "react-web-sdk_nextjs", - "version": "0.1.0", + "name": "@implementation/react-web-sdk_nextjs", "private": true, + "version": "0.0.0", + "description": "Reference implementation for Next.js (App Router) using @contentful/optimization-react-web and @contentful/optimization-node for SSR", + "license": "MIT", + "type": "module", "scripts": { - "dev": "next dev", + "dev": "next dev --port 3000", "build": "next build", - "start": "next start", + "start": "next start --port 3000", + "clean": "rimraf .next coverage playwright-report test-results .tsbuildinfo", + "preview": "pnpm serve:mocks && pnpm start", + "serve": "pnpm serve:mocks && pnpm serve:app", + "serve:app": "pnpm build && pm2 start --name nextjs-react-web-sdk-app \"pnpm start\"", + "serve:app:stop": "pm2 stop nextjs-react-web-sdk-app && pm2 delete nextjs-react-web-sdk-app", + "serve:mocks": "pm2 start --name nextjs-react-web-sdk-mocks \"pnpm --dir ../../lib/mocks serve\"", + "serve:mocks:stop": "pm2 stop nextjs-react-web-sdk-mocks && pm2 delete nextjs-react-web-sdk-mocks", + "serve:stop": "pnpm serve:app:stop && pnpm serve:mocks:stop", + "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", + "test:e2e:codegen": "playwright codegen", + "test:e2e:report": "playwright show-report", + "test:e2e:ui": "playwright test --ui", + "implementation:playwright:install": "playwright install", + "implementation:playwright:install-deps": "playwright install-deps", + "implementation:setup:e2e": "pnpm implementation:playwright:install && pnpm implementation:playwright:install-deps", + "test:unit": "echo \"No unit tests necessary\"", + "typecheck": "tsc --noEmit", "lint": "eslint" }, "dependencies": { + "@contentful/optimization-node": "0.0.0", + "@contentful/optimization-react-web": "0.0.0", + "@contentful/optimization-web-preview-panel": "0.0.0", + "@contentful/rich-text-react-renderer": "16.1.6", + "@contentful/rich-text-types": "17.2.5", + "contentful": "11.10.5", "next": "16.2.4", - "react": "19.2.4", - "react-dom": "19.2.4" + "react": "19.2.5", + "react-dom": "19.2.5" }, "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", + "@playwright/test": "1.58.2", + "@tailwindcss/postcss": "4.1.11", + "@types/node": "24.11.0", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "dotenv": "17.3.1", + "eslint": "9.29.0", "eslint-config-next": "16.2.4", - "tailwindcss": "^4", - "typescript": "^5" + "pm2": "6.0.14", + "postcss": "8.5.6", + "rimraf": "6.1.3", + "tailwindcss": "4.1.11", + "typescript": "5.9.3" + }, + "pnpm": { + "overrides": { + "@contentful/optimization-api-client": "file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz", + "@contentful/optimization-api-schemas": "file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz", + "@contentful/optimization-core": "file:../../pkgs/contentful-optimization-core-0.0.0.tgz", + "@contentful/optimization-node": "file:../../pkgs/contentful-optimization-node-0.0.0.tgz", + "@contentful/optimization-web": "file:../../pkgs/contentful-optimization-web-0.0.0.tgz", + "@contentful/optimization-react-web": "file:../../pkgs/contentful-optimization-react-web-0.0.0.tgz", + "@contentful/optimization-web-preview-panel": "file:../../pkgs/contentful-optimization-web-preview-panel-0.0.0.tgz" + } } } diff --git a/implementations/react-web-sdk_nextjs/public/file.svg b/implementations/react-web-sdk_nextjs/public/file.svg deleted file mode 100644 index 004145cd..00000000 --- a/implementations/react-web-sdk_nextjs/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/public/globe.svg b/implementations/react-web-sdk_nextjs/public/globe.svg deleted file mode 100644 index 567f17b0..00000000 --- a/implementations/react-web-sdk_nextjs/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/public/next.svg b/implementations/react-web-sdk_nextjs/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/implementations/react-web-sdk_nextjs/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/public/vercel.svg b/implementations/react-web-sdk_nextjs/public/vercel.svg deleted file mode 100644 index 77053960..00000000 --- a/implementations/react-web-sdk_nextjs/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/public/window.svg b/implementations/react-web-sdk_nextjs/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/implementations/react-web-sdk_nextjs/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/implementations/react-web-sdk_nextjs/tsconfig.json b/implementations/react-web-sdk_nextjs/tsconfig.json index 3a13f90a..2cc12770 100644 --- a/implementations/react-web-sdk_nextjs/tsconfig.json +++ b/implementations/react-web-sdk_nextjs/tsconfig.json @@ -1,18 +1,22 @@ { "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, - "module": "esnext", + "module": "ESNext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, "plugins": [ { "name": "next" diff --git a/package.json b/package.json index a3802b29..8414ab57 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "implementation:run": "tsx ./scripts/run-implementation-script.ts", "implementation:react-web-sdk": "pnpm run implementation:run -- react-web-sdk", "implementation:web-sdk_react": "pnpm run implementation:run -- web-sdk_react", + "implementation:react-web-sdk_nextjs": "pnpm run implementation:run -- react-web-sdk_nextjs", "implementation:web-sdk": "pnpm run implementation:run -- web-sdk", "implementation:lint": "eslint implementations --cache --cache-location .cache/eslint/implementations", "implementation:lint:fix": "eslint implementations --fix", From eaf177e38621a7a0703fc525552a2ac5c58c652c Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Mon, 4 May 2026 12:21:31 +0200 Subject: [PATCH 03/17] feat: add client-side React SDK integration with OptimizationProvider and client-resolved demo --- .../app/client-resolved/page.tsx | 166 ++++++++++++++++++ .../react-web-sdk_nextjs/app/layout.tsx | 5 +- .../react-web-sdk_nextjs/app/page.tsx | 10 +- .../app/server-resolved/page.tsx | 19 ++ .../components/OptimizationProvider.tsx | 42 +++++ .../react-web-sdk_nextjs/config/entries.ts | 17 ++ .../react-web-sdk_nextjs/lib/config.ts | 23 +++ .../lib/contentful-client.ts | 34 ++++ .../react-web-sdk_nextjs/types/contentful.ts | 11 ++ 9 files changed, 322 insertions(+), 5 deletions(-) create mode 100644 implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx create mode 100644 implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx create mode 100644 implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx create mode 100644 implementations/react-web-sdk_nextjs/config/entries.ts create mode 100644 implementations/react-web-sdk_nextjs/lib/config.ts create mode 100644 implementations/react-web-sdk_nextjs/lib/contentful-client.ts create mode 100644 implementations/react-web-sdk_nextjs/types/contentful.ts diff --git a/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx b/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx new file mode 100644 index 00000000..c6974039 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx @@ -0,0 +1,166 @@ +'use client' + +import { ENTRY_IDS } from '@/config/entries' +import { fetchEntries } from '@/lib/contentful-client' +import type { ContentEntry } from '@/types/contentful' +import { useOptimization, useOptimizationContext } from '@contentful/optimization-react-web' +import type { Profile } from '@contentful/optimization-react-web/api-schemas' +import { type JSX, useEffect, useMemo, useState } from 'react' + +function getEntryText(entry: ContentEntry): string { + return typeof entry.fields.text === 'string' ? entry.fields.text : 'No content' +} + +function ResolvedEntry({ entry }: { entry: ContentEntry }): JSX.Element { + const { resolveEntry, resolveEntryData } = useOptimization() + const resolvedEntry = resolveEntry(entry) as ContentEntry + const { selectedOptimization } = resolveEntryData(entry) + const experienceId = + selectedOptimization && + typeof selectedOptimization === 'object' && + 'experienceId' in selectedOptimization + ? (selectedOptimization as { experienceId: string }).experienceId + : undefined + + return ( +
+

{getEntryText(resolvedEntry)}

+

{`[Entry: ${entry.sys.id}]`}

+
+ ) +} + +function EntryList(): JSX.Element { + const { sdk, isReady } = useOptimizationContext() + const [entries, setEntries] = useState([]) + const [consent, setConsent] = useState(undefined) + const [profile, setProfile] = useState(undefined) + const [selectedOptimizationCount, setSelectedOptimizationCount] = useState(0) + + useEffect(() => { + if (!sdk || !isReady) { + return + } + + const consentSub = sdk.states.consent.subscribe((value: boolean | undefined) => { + setConsent(value) + }) + + const profileSub = sdk.states.profile.subscribe((value: Profile | undefined) => { + setProfile(value) + }) + + const selectedOptSub = sdk.states.selectedOptimizations.subscribe((value) => { + setSelectedOptimizationCount(Array.isArray(value) ? value.length : 0) + }) + + return () => { + consentSub.unsubscribe() + profileSub.unsubscribe() + selectedOptSub.unsubscribe() + } + }, [isReady, sdk]) + + useEffect(() => { + if (!sdk || !isReady) { + return + } + + void fetchEntries(ENTRY_IDS).then((nextEntries) => { + setEntries(nextEntries) + }) + }, [isReady, sdk]) + + const isIdentified = useMemo( + () => profile !== undefined && Boolean(profile.traits.identified), + [profile], + ) + + if (!sdk || !isReady) { + return

Loading SDK...

+ } + + return ( +
+
+

Controls

+
+ + + {!isIdentified ? ( + + ) : ( + + )} +
+ +
+

Consent: {String(consent)}

+

+ Selected Optimizations: {selectedOptimizationCount} +

+

Identified: {isIdentified ? 'Yes' : 'No'}

+
+
+ +
+

Entries (Client-Resolved)

+ {entries.length === 0 ? ( +

Loading entries...

+ ) : ( +
+ {entries.map((entry) => ( + + ))} +
+ )} +
+
+ ) +} + +export default function ClientResolvedPage(): JSX.Element { + return ( +
+

Client-Resolved Pattern

+

+ Entries are fetched from Contentful and resolved entirely on the client via the React SDK. + The server renders an HTML shell; the Web SDK resolves optimizations in the browser. +

+ +
+ ) +} diff --git a/implementations/react-web-sdk_nextjs/app/layout.tsx b/implementations/react-web-sdk_nextjs/app/layout.tsx index 8835a4e0..486108a1 100644 --- a/implementations/react-web-sdk_nextjs/app/layout.tsx +++ b/implementations/react-web-sdk_nextjs/app/layout.tsx @@ -1,3 +1,4 @@ +import { OptimizationProvider } from '@/components/OptimizationProvider' import type { Metadata } from 'next' import './globals.css' @@ -14,7 +15,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ) } diff --git a/implementations/react-web-sdk_nextjs/app/page.tsx b/implementations/react-web-sdk_nextjs/app/page.tsx index 65e73685..a4ff86e7 100644 --- a/implementations/react-web-sdk_nextjs/app/page.tsx +++ b/implementations/react-web-sdk_nextjs/app/page.tsx @@ -1,3 +1,5 @@ +import Link from 'next/link' + export default function Home() { return (
@@ -7,18 +9,18 @@ export default function Home() { SDKs in a Next.js App Router application.

) diff --git a/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx b/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx new file mode 100644 index 00000000..c0d0c50a --- /dev/null +++ b/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link' + +export default function ServerResolvedPage() { + return ( +
+

Server-Resolved Pattern

+

+ Entries are pre-resolved on the server via the Node SDK and passed as props to client + components for hydration. This pattern will be implemented in Phase 3. +

+ + Back to Home + +
+ ) +} diff --git a/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx b/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx new file mode 100644 index 00000000..742f30a5 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx @@ -0,0 +1,42 @@ +'use client' + +import { optimizationConfig } from '@/lib/config' +import { OptimizationRoot } from '@contentful/optimization-react-web' +import { NextAppAutoPageTracker } from '@contentful/optimization-react-web/router/next-app' +import { Suspense, useEffect, useState, type ReactNode } from 'react' + +interface OptimizationProviderProps { + readonly children: ReactNode +} + +export function OptimizationProvider({ children }: OptimizationProviderProps): ReactNode { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + // + return null + } + + return ( + + + + + {children} + + ) +} diff --git a/implementations/react-web-sdk_nextjs/config/entries.ts b/implementations/react-web-sdk_nextjs/config/entries.ts new file mode 100644 index 00000000..72962fff --- /dev/null +++ b/implementations/react-web-sdk_nextjs/config/entries.ts @@ -0,0 +1,17 @@ +export const AUTO_OBSERVED_ENTRY_IDS = [ + '1JAU028vQ7v6nB2swl3NBo', + '1MwiFl4z7gkwqGYdvCmr8c', + '4ib0hsHWoSOnCVdDkizE8d', + 'xFwgG3oNaOcjzWiGe4vXo', + '2Z2WLOx07InSewC3LUB3eX', +] as const + +export const MANUALLY_OBSERVED_ENTRY_IDS = [ + '5XHssysWUDECHzKLzoIsg1', + '6zqoWXyiSrf0ja7I2WGtYj', + '7pa5bOx8Z9NmNcr7mISvD', +] as const + +export const ENTRY_IDS = [...AUTO_OBSERVED_ENTRY_IDS, ...MANUALLY_OBSERVED_ENTRY_IDS] as const + +export const LIVE_UPDATES_ENTRY_ID = '2Z2WLOx07InSewC3LUB3eX' as const diff --git a/implementations/react-web-sdk_nextjs/lib/config.ts b/implementations/react-web-sdk_nextjs/lib/config.ts new file mode 100644 index 00000000..5abfe222 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/lib/config.ts @@ -0,0 +1,23 @@ +const CLIENT_ID = process.env.PUBLIC_NINETAILED_CLIENT_ID?.trim() ?? 'mock-client-id' +const ENVIRONMENT = process.env.PUBLIC_NINETAILED_ENVIRONMENT?.trim() ?? 'main' +const INSIGHTS_BASE_URL = + process.env.PUBLIC_INSIGHTS_API_BASE_URL?.trim() ?? 'http://localhost:8000/insights/' +const EXPERIENCE_BASE_URL = + process.env.PUBLIC_EXPERIENCE_API_BASE_URL?.trim() ?? 'http://localhost:8000/experience/' + +export const optimizationConfig = { + clientId: CLIENT_ID, + environment: ENVIRONMENT, + api: { + insightsBaseUrl: INSIGHTS_BASE_URL, + experienceBaseUrl: EXPERIENCE_BASE_URL, + }, +} as const + +export const contentfulConfig = { + accessToken: process.env.PUBLIC_CONTENTFUL_TOKEN?.trim() ?? '', + environment: process.env.PUBLIC_CONTENTFUL_ENVIRONMENT?.trim() ?? '', + host: process.env.PUBLIC_CONTENTFUL_CDA_HOST?.trim() ?? '', + space: process.env.PUBLIC_CONTENTFUL_SPACE_ID?.trim() ?? '', + basePath: process.env.PUBLIC_CONTENTFUL_BASE_PATH?.trim(), +} as const diff --git a/implementations/react-web-sdk_nextjs/lib/contentful-client.ts b/implementations/react-web-sdk_nextjs/lib/contentful-client.ts new file mode 100644 index 00000000..aa0b99c4 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/lib/contentful-client.ts @@ -0,0 +1,34 @@ +import type { ContentEntry, ContentEntrySkeleton } from '@/types/contentful' +import { createClient } from 'contentful' +import { contentfulConfig } from './config' + +const INCLUDE_DEPTH = 10 + +function createContentfulClient(): ReturnType { + return createClient({ + accessToken: contentfulConfig.accessToken, + environment: contentfulConfig.environment, + host: contentfulConfig.host, + insecure: contentfulConfig.host.includes('localhost'), + space: contentfulConfig.space, + ...(contentfulConfig.basePath ? { basePath: contentfulConfig.basePath } : {}), + }) +} + +const contentfulClient = createContentfulClient() + +export async function fetchEntry(entryId: string): Promise { + try { + return await contentfulClient.getEntry(entryId, { + include: INCLUDE_DEPTH, + }) + } catch { + return undefined + } +} + +export async function fetchEntries(entryIds: readonly string[]): Promise { + const fetchedEntries = await Promise.all(entryIds.map(fetchEntry)) + + return fetchedEntries.filter((entry): entry is ContentEntry => entry !== undefined) +} diff --git a/implementations/react-web-sdk_nextjs/types/contentful.ts b/implementations/react-web-sdk_nextjs/types/contentful.ts new file mode 100644 index 00000000..2efb9ad7 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/types/contentful.ts @@ -0,0 +1,11 @@ +import type { Document } from '@contentful/rich-text-types' +import type { Entry, EntryFieldTypes, EntrySkeletonType } from 'contentful' + +export interface ContentEntryFields { + text?: EntryFieldTypes.Text | EntryFieldTypes.RichText + nested?: EntryFieldTypes.Array> +} + +export type ContentEntrySkeleton = EntrySkeletonType +export type ContentEntry = Entry +export type RichTextDocument = Document From 0273a04e0b901547d8347d9d1015832dfcccf5be Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Mon, 4 May 2026 12:29:41 +0200 Subject: [PATCH 04/17] docs: comment OptimizationProvider mount to client to avoid SSR localStorage crash --- .../components/OptimizationProvider.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx b/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx index 742f30a5..79874626 100644 --- a/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx +++ b/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx @@ -16,8 +16,13 @@ export function OptimizationProvider({ children }: OptimizationProviderProps): R setMounted(true) }, []) + // The Web SDK requires browser APIs (localStorage, document.cookie) and cannot + // be instantiated during SSR. We must return `null` rather than rendering + // children without , because child components that call + // useOptimization() / useOptimizationContext() will throw if rendered outside + // the provider. This causes a brief flash of empty content before the SDK + // mounts on the client. if (!mounted) { - // return null } From fedd3b3621e2851413c3f0e85e54b34efcce6549 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Mon, 4 May 2026 13:15:08 +0200 Subject: [PATCH 05/17] refactor: use next/dynamic with ssr:false for client-only SDK provider Replace the useState/useEffect mount guard with a dedicated ClientProviderWrapper that uses next/dynamic({ ssr: false }). This is the standard Next.js pattern for browser-only third-party SDKs and keeps OptimizationProvider clean without SSR workarounds. --- .../react-web-sdk_nextjs/app/layout.tsx | 4 +-- .../components/ClientProviderWrapper.tsx | 27 +++++++++++++++++++ .../components/OptimizationProvider.tsx | 20 ++------------ 3 files changed, 31 insertions(+), 20 deletions(-) create mode 100644 implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx diff --git a/implementations/react-web-sdk_nextjs/app/layout.tsx b/implementations/react-web-sdk_nextjs/app/layout.tsx index 486108a1..c4f9e87f 100644 --- a/implementations/react-web-sdk_nextjs/app/layout.tsx +++ b/implementations/react-web-sdk_nextjs/app/layout.tsx @@ -1,4 +1,4 @@ -import { OptimizationProvider } from '@/components/OptimizationProvider' +import { ClientProviderWrapper } from '@/components/ClientProviderWrapper' import type { Metadata } from 'next' import './globals.css' @@ -16,7 +16,7 @@ export default function RootLayout({ return ( - {children} + {children} ) diff --git a/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx b/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx new file mode 100644 index 00000000..5d64d04c --- /dev/null +++ b/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx @@ -0,0 +1,27 @@ +'use client' + +// The Web SDK requires browser APIs (localStorage, document.cookie) and cannot +// be instantiated during SSR. Using next/dynamic with ssr:false ensures the +// OptimizationProvider (and its OptimizationRoot) is only loaded and rendered +// on the client. Without this, child components that call useOptimization() +// would throw when rendered outside the provider during server rendering. +// This causes a brief flash of empty content before the SDK mounts. + +import dynamic from 'next/dynamic' +import type { ReactNode } from 'react' + +const OptimizationProvider = dynamic( + () => + import('./OptimizationProvider').then((mod) => ({ + default: mod.OptimizationProvider, + })), + { ssr: false }, +) + +interface ClientProviderWrapperProps { + readonly children: ReactNode +} + +export function ClientProviderWrapper({ children }: ClientProviderWrapperProps) { + return {children} +} diff --git a/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx b/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx index 79874626..a687cfcc 100644 --- a/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx +++ b/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx @@ -3,29 +3,13 @@ import { optimizationConfig } from '@/lib/config' import { OptimizationRoot } from '@contentful/optimization-react-web' import { NextAppAutoPageTracker } from '@contentful/optimization-react-web/router/next-app' -import { Suspense, useEffect, useState, type ReactNode } from 'react' +import { Suspense, type ReactElement, type ReactNode } from 'react' interface OptimizationProviderProps { readonly children: ReactNode } -export function OptimizationProvider({ children }: OptimizationProviderProps): ReactNode { - const [mounted, setMounted] = useState(false) - - useEffect(() => { - setMounted(true) - }, []) - - // The Web SDK requires browser APIs (localStorage, document.cookie) and cannot - // be instantiated during SSR. We must return `null` rather than rendering - // children without , because child components that call - // useOptimization() / useOptimizationContext() will throw if rendered outside - // the provider. This causes a brief flash of empty content before the SDK - // mounts on the client. - if (!mounted) { - return null - } - +export function OptimizationProvider({ children }: OptimizationProviderProps): ReactElement { return ( Date: Mon, 4 May 2026 18:39:08 +0200 Subject: [PATCH 06/17] feat: add server-resolved pattern and isolate route SDK boundaries - Add Node SDK server singleton and Next.js middleware for cookie lifecycle (ctfl-opt-aid profile management). - Add server-resolved page: Server Component fetches entries and resolves them via Node SDK, renders pure server HTML with zero client JS for content. - Add InteractiveControls as a small 'use client' component for consent/identify on the server-resolved route. - Isolate SDK setup per route: each route has its own layout with its own ClientProviderWrapper, root layout is neutral. - Remove custom OptimizationProvider wrapper; use SDK's OptimizationRoot directly via dynamic import. --- .../app/client-resolved/layout.tsx | 9 ++ .../app/client-resolved/page.tsx | 2 +- .../react-web-sdk_nextjs/app/layout.tsx | 5 +- .../server-resolved/InteractiveControls.tsx | 90 +++++++++++++++++++ .../app/server-resolved/layout.tsx | 9 ++ .../app/server-resolved/page.tsx | 87 +++++++++++++++--- .../components/ClientProviderWrapper.tsx | 45 +++++++--- .../components/OptimizationProvider.tsx | 31 ------- .../lib/optimization-server.ts | 15 ++++ .../react-web-sdk_nextjs/middleware.ts | 37 ++++++++ 10 files changed, 274 insertions(+), 56 deletions(-) create mode 100644 implementations/react-web-sdk_nextjs/app/client-resolved/layout.tsx create mode 100644 implementations/react-web-sdk_nextjs/app/server-resolved/InteractiveControls.tsx create mode 100644 implementations/react-web-sdk_nextjs/app/server-resolved/layout.tsx delete mode 100644 implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx create mode 100644 implementations/react-web-sdk_nextjs/lib/optimization-server.ts create mode 100644 implementations/react-web-sdk_nextjs/middleware.ts diff --git a/implementations/react-web-sdk_nextjs/app/client-resolved/layout.tsx b/implementations/react-web-sdk_nextjs/app/client-resolved/layout.tsx new file mode 100644 index 00000000..559e42a1 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/app/client-resolved/layout.tsx @@ -0,0 +1,9 @@ +import { ClientProviderWrapper } from '@/components/ClientProviderWrapper' + +export default function ClientResolvedLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return {children} +} diff --git a/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx b/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx index c6974039..f93dc500 100644 --- a/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx +++ b/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx @@ -13,7 +13,7 @@ function getEntryText(entry: ContentEntry): string { function ResolvedEntry({ entry }: { entry: ContentEntry }): JSX.Element { const { resolveEntry, resolveEntryData } = useOptimization() - const resolvedEntry = resolveEntry(entry) as ContentEntry + const resolvedEntry = resolveEntry(entry) const { selectedOptimization } = resolveEntryData(entry) const experienceId = selectedOptimization && diff --git a/implementations/react-web-sdk_nextjs/app/layout.tsx b/implementations/react-web-sdk_nextjs/app/layout.tsx index c4f9e87f..8835a4e0 100644 --- a/implementations/react-web-sdk_nextjs/app/layout.tsx +++ b/implementations/react-web-sdk_nextjs/app/layout.tsx @@ -1,4 +1,3 @@ -import { ClientProviderWrapper } from '@/components/ClientProviderWrapper' import type { Metadata } from 'next' import './globals.css' @@ -15,9 +14,7 @@ export default function RootLayout({ }>) { return ( - - {children} - + {children} ) } diff --git a/implementations/react-web-sdk_nextjs/app/server-resolved/InteractiveControls.tsx b/implementations/react-web-sdk_nextjs/app/server-resolved/InteractiveControls.tsx new file mode 100644 index 00000000..d6f1c4f3 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/app/server-resolved/InteractiveControls.tsx @@ -0,0 +1,90 @@ +'use client' + +import { useOptimizationContext } from '@contentful/optimization-react-web' +import type { Profile } from '@contentful/optimization-react-web/api-schemas' +import { type JSX, useEffect, useMemo, useState } from 'react' + +export function InteractiveControls(): JSX.Element { + const { sdk, isReady } = useOptimizationContext() + const [consent, setConsent] = useState(undefined) + const [profile, setProfile] = useState(undefined) + + useEffect(() => { + if (!sdk || !isReady) { + return + } + + const consentSub = sdk.states.consent.subscribe((value: boolean | undefined) => { + setConsent(value) + }) + + const profileSub = sdk.states.profile.subscribe((value: Profile | undefined) => { + setProfile(value) + }) + + return () => { + consentSub.unsubscribe() + profileSub.unsubscribe() + } + }, [isReady, sdk]) + + const isIdentified = useMemo( + () => profile !== undefined && Boolean(profile.traits.identified), + [profile], + ) + + if (!sdk || !isReady) { + return ( +
+

SDK loading...

+
+ ) + } + + return ( +
+

Controls

+
+ + + {!isIdentified ? ( + + ) : ( + + )} +
+ +
+

Consent: {String(consent)}

+

Identified: {isIdentified ? 'Yes' : 'No'}

+
+
+ ) +} diff --git a/implementations/react-web-sdk_nextjs/app/server-resolved/layout.tsx b/implementations/react-web-sdk_nextjs/app/server-resolved/layout.tsx new file mode 100644 index 00000000..af161672 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/app/server-resolved/layout.tsx @@ -0,0 +1,9 @@ +import { ClientProviderWrapper } from '@/components/ClientProviderWrapper' + +export default function ServerResolvedLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return {children} +} diff --git a/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx b/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx index c0d0c50a..4abd3255 100644 --- a/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx +++ b/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx @@ -1,19 +1,86 @@ -import Link from 'next/link' +import { ENTRY_IDS } from '@/config/entries' +import { fetchEntries } from '@/lib/contentful-client' +import { sdk } from '@/lib/optimization-server' +import type { ContentEntry } from '@/types/contentful' +import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants' +import { cookies, headers } from 'next/headers' +import { InteractiveControls } from './InteractiveControls' + +function getEntryText(entry: ContentEntry): string { + return typeof entry.fields.text === 'string' ? entry.fields.text : 'No content' +} + +function ServerRenderedEntry({ + baselineEntry, + resolvedEntry, +}: { + baselineEntry: ContentEntry + resolvedEntry: ContentEntry +}) { + return ( +
+

{getEntryText(resolvedEntry)}

+

{`[Entry: ${baselineEntry.sys.id}]`}

+
+ ) +} + +export default async function ServerResolvedPage() { + const cookieStore = await cookies() + const headerStore = await headers() + + const anonymousId = cookieStore.get(ANONYMOUS_ID_COOKIE)?.value + const profile = anonymousId ? { id: anonymousId } : undefined + + const [baselineEntries, optimizationData] = await Promise.all([ + fetchEntries(ENTRY_IDS), + sdk.page({ + locale: headerStore.get('accept-language')?.split(',')[0] ?? 'en-US', + userAgent: headerStore.get('user-agent') ?? 'next-js-server', + profile, + }), + ]) + + const resolvedEntries = baselineEntries.map((entry) => { + const { entry: resolved } = sdk.resolveOptimizedEntry( + entry, + optimizationData.selectedOptimizations, + ) + return resolved + }) -export default function ServerResolvedPage() { return (

Server-Resolved Pattern

- Entries are pre-resolved on the server via the Node SDK and passed as props to client - components for hydration. This pattern will be implemented in Phase 3. + Entries are pre-resolved on the server via the Node SDK. The HTML contains resolved content + with zero client-side JavaScript for rendering. Interactive features (consent, identify) + hydrate separately.

- - Back to Home - + + + +
+

Entries (Server-Resolved)

+ {baselineEntries.length === 0 ? ( +

No entries found.

+ ) : ( +
+ {baselineEntries.map((entry, index) => ( + + ))} +
+ )} +
) } diff --git a/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx b/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx index 5d64d04c..83eb04ae 100644 --- a/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx +++ b/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx @@ -1,19 +1,27 @@ 'use client' // The Web SDK requires browser APIs (localStorage, document.cookie) and cannot -// be instantiated during SSR. Using next/dynamic with ssr:false ensures the -// OptimizationProvider (and its OptimizationRoot) is only loaded and rendered -// on the client. Without this, child components that call useOptimization() -// would throw when rendered outside the provider during server rendering. -// This causes a brief flash of empty content before the SDK mounts. +// be instantiated during SSR. Using next/dynamic with ssr:false ensures +// OptimizationRoot is only loaded and rendered on the client. Without this, +// child components that call useOptimization() would throw when rendered +// outside the provider during server rendering. +import { optimizationConfig } from '@/lib/config' import dynamic from 'next/dynamic' -import type { ReactNode } from 'react' +import { Suspense, type ReactNode } from 'react' -const OptimizationProvider = dynamic( +const OptimizationRoot = dynamic( () => - import('./OptimizationProvider').then((mod) => ({ - default: mod.OptimizationProvider, + import('@contentful/optimization-react-web').then((mod) => ({ + default: mod.OptimizationRoot, + })), + { ssr: false }, +) + +const NextAppAutoPageTracker = dynamic( + () => + import('@contentful/optimization-react-web/router/next-app').then((mod) => ({ + default: mod.NextAppAutoPageTracker, })), { ssr: false }, ) @@ -23,5 +31,22 @@ interface ClientProviderWrapperProps { } export function ClientProviderWrapper({ children }: ClientProviderWrapperProps) { - return {children} + return ( + + + + + {children} + + ) } diff --git a/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx b/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx deleted file mode 100644 index a687cfcc..00000000 --- a/implementations/react-web-sdk_nextjs/components/OptimizationProvider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client' - -import { optimizationConfig } from '@/lib/config' -import { OptimizationRoot } from '@contentful/optimization-react-web' -import { NextAppAutoPageTracker } from '@contentful/optimization-react-web/router/next-app' -import { Suspense, type ReactElement, type ReactNode } from 'react' - -interface OptimizationProviderProps { - readonly children: ReactNode -} - -export function OptimizationProvider({ children }: OptimizationProviderProps): ReactElement { - return ( - - - - - {children} - - ) -} diff --git a/implementations/react-web-sdk_nextjs/lib/optimization-server.ts b/implementations/react-web-sdk_nextjs/lib/optimization-server.ts new file mode 100644 index 00000000..377062b6 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/lib/optimization-server.ts @@ -0,0 +1,15 @@ +import ContentfulOptimization from '@contentful/optimization-node' +import { optimizationConfig } from './config' + +const sdk = new ContentfulOptimization({ + clientId: optimizationConfig.clientId, + environment: optimizationConfig.environment, + logLevel: 'debug', + api: optimizationConfig.api, + app: { + name: 'ContentfulOptimization SDK - Next.js Reference (Server)', + version: '0.1.0', + }, +}) + +export { sdk } diff --git a/implementations/react-web-sdk_nextjs/middleware.ts b/implementations/react-web-sdk_nextjs/middleware.ts new file mode 100644 index 00000000..3e37b251 --- /dev/null +++ b/implementations/react-web-sdk_nextjs/middleware.ts @@ -0,0 +1,37 @@ +import { sdk } from '@/lib/optimization-server' +import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants' +import { type NextRequest, NextResponse } from 'next/server' + +export async function middleware(request: NextRequest): Promise { + const anonymousId = request.cookies.get(ANONYMOUS_ID_COOKIE)?.value + const profile = anonymousId ? { id: anonymousId } : undefined + + const url = new URL(request.url) + const data = await sdk.page({ + locale: request.headers.get('accept-language')?.split(',')[0] ?? 'en-US', + userAgent: request.headers.get('user-agent') ?? 'next-js-server', + page: { + path: url.pathname, + query: Object.fromEntries(url.searchParams), + referrer: request.headers.get('referer') ?? '', + search: url.search, + url: request.url, + }, + profile, + }) + + const response = NextResponse.next() + + if (data.profile.id) { + response.cookies.set(ANONYMOUS_ID_COOKIE, data.profile.id, { + path: '/', + sameSite: 'lax', + }) + } + + return response +} + +export const config = { + matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'], +} From 0878cc46d309e11af90df6c68c0021364ca23008 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Tue, 5 May 2026 13:42:18 +0200 Subject: [PATCH 07/17] docs: update README and AGENTS.md to reflect current architecture Document isolated route layouts, server-resolved vs client-resolved patterns, design decisions, cookie-based profile management, and the singleton dev-mode recovery tip. --- .../react-web-sdk_nextjs/AGENTS.md | 25 ++-- .../react-web-sdk_nextjs/README.md | 130 +++++++++++++++--- 2 files changed, 127 insertions(+), 28 deletions(-) diff --git a/implementations/react-web-sdk_nextjs/AGENTS.md b/implementations/react-web-sdk_nextjs/AGENTS.md index e92cd376..c5120a64 100644 --- a/implementations/react-web-sdk_nextjs/AGENTS.md +++ b/implementations/react-web-sdk_nextjs/AGENTS.md @@ -6,30 +6,33 @@ Read the repository root `AGENTS.md` first. This is the Next.js (App Router) reference implementation demonstrating `@contentful/optimization-react-web` for client-side optimization and -`@contentful/optimization-node` for server-side optimization in an SSR context. +`@contentful/optimization-node` for server-side optimization. -It shows two integration patterns: +It shows two independent integration patterns, each with its own route layout and SDK setup: -1. **Client-resolved**: entries are resolved entirely on the client via the React SDK -2. **Server-pre-resolved**: entries are resolved on the server via the Node SDK and hydrated on the - client +1. **Client-resolved** (`/client-resolved`): React SDK does everything in the browser — entry + resolution, event tracking, consent, identify. No Node SDK. +2. **Server-resolved** (`/server-resolved`): Node SDK resolves entries on the server as pure HTML. + React SDK hydrates only for interactive controls (consent, identify) and event tracking. ## Key Paths -- `app/` — Next.js App Router pages, layouts, and route handlers +- `app/` — Next.js App Router pages and route-scoped layouts +- `app/client-resolved/` — client-only pattern (React SDK) +- `app/server-resolved/` — server pattern (Node SDK + React SDK for tracking) - `lib/` — shared utilities (SDK config, Contentful client, server-side helpers) -- `components/` — React components (client and server) +- `components/` — shared client wrapper (`ClientProviderWrapper`) +- `middleware.ts` — cookie lifecycle management - `.env.example` -- `middleware.ts` — Next.js middleware for cookie-based profile management ## Local Rules - This implementation uses the Next.js App Router exclusively. Do not add Pages Router files. +- The root layout is neutral — no SDK provider. Each route owns its own SDK setup via route layouts. - All SDK usage on the client must live inside `"use client"` components. Server Components must not import from `@contentful/optimization-react-web` or `@contentful/optimization-web`. - Server-side SDK usage must import from `@contentful/optimization-node` only. -- The React SDK (`OptimizationRoot`) must be initialized in a single client component wrapper in the - root layout. Do not create multiple provider instances. +- Do not create a custom `OptimizationProvider` wrapper — use the SDK's `OptimizationRoot` directly. - Do not add a `src/optimization/` directory. Follow the same direct-import pattern as `implementations/react-web-sdk`. - If you changed a consumed package, run `pnpm build:pkgs` and reinstall this implementation before @@ -46,6 +49,8 @@ It shows two integration patterns: - Behavior differs from the documented mock setup: compare `.env` with `.env.example` before changing code. - Next.js caching stale data: run `pnpm clean` to clear the `.next` directory. +- `ContentfulOptimization is already initialized`: stale singleton from a previous dev session or + HMR. Run `window.contentfulOptimization?.destroy()` in the browser console and refresh. ## Commands diff --git a/implementations/react-web-sdk_nextjs/README.md b/implementations/react-web-sdk_nextjs/README.md index 159484bc..30b066f1 100644 --- a/implementations/react-web-sdk_nextjs/README.md +++ b/implementations/react-web-sdk_nextjs/README.md @@ -1,19 +1,71 @@ # Next.js React Web SDK Reference Implementation Next.js App Router reference implementation demonstrating `@contentful/optimization-react-web` -(client-side) and `@contentful/optimization-node` (server-side SSR) working together. +(client-side) and `@contentful/optimization-node` (server-side SSR) as two independent integration +patterns. ## Architecture -This implementation shows two integration patterns: +Each route is fully isolated with its own SDK setup. The root layout is neutral — no SDK +initialization at the app level. -- **Client-Resolved** (`/client-resolved`): Entries are resolved entirely in the browser via the - React SDK. The server renders an HTML shell; the Web SDK resolves optimizations client-side. -- **Server-Resolved** (`/server-resolved`): Entries are pre-resolved on the server via the Node SDK - and passed as props to client components for hydration with full interactivity. +### Client-Resolved (`/client-resolved`) -Both patterns share a cookie-based profile (`ctfl-opt-aid`) managed by Next.js middleware, ensuring -the same anonymous profile is used across server and client. +The React SDK does everything in the browser. No Node SDK involvement. + +``` +Browser loads page + → JavaScript downloads and executes + → React SDK initializes (browser-only singleton via dynamic import, ssr:false) + → SDK calls Experience API from the browser + → Component fetches entries from Contentful CDA + → SDK resolves entries client-side + → React renders content +``` + +- **First paint**: empty/loading state until JS loads and SDK resolves +- **Reactivity**: immediate — identify, consent, reset all re-resolve entries instantly +- **Client JS**: required for all content rendering + +### Server-Resolved (`/server-resolved`) + +The Node SDK resolves entries on the server. Client JS is only needed for interactive controls +(consent, identify) and event tracking — not for content rendering. + +``` +Request hits server + → Middleware reads ctfl-opt-aid cookie, calls Node SDK sdk.page(), sets/refreshes cookie + → Server Component reads cookie, calls Node SDK sdk.page() for selectedOptimizations + → Server Component fetches entries from Contentful CDA + → Node SDK resolves entries with sdk.resolveOptimizedEntry() + → Server renders final HTML with correct personalized content + +Browser receives HTML + → Content is visible immediately (static server HTML, zero JS) + → InteractiveControls component hydrates for consent/identify buttons + → Entry content does NOT re-render on hydration +``` + +- **First paint**: correct resolved content immediately, no loading flash +- **Reactivity**: profile changes (identify/reset) require a page refresh to re-resolve entries +- **Client JS**: only for interactive controls, not for content + +### Key Differences + +| | Client-Resolved | Server-Resolved | +| -------------------------- | --------------------- | ----------------------------- | +| Who resolves entries | Browser (React SDK) | Server (Node SDK) | +| First paint | Loading state | Correct content | +| Client JS for content | Required | None | +| Experience API called from | Browser | Server (middleware + page) | +| Profile changes | Instant re-resolution | Requires page refresh | +| `"use client"` boundary | Entire page | Small controls component only | + +### Cookie-Based Profile Consistency + +Both patterns share the `ctfl-opt-aid` cookie managed by Next.js middleware. The middleware runs on +every request and ensures an anonymous profile exists before any page renders. This is the bridge +between server and client — both SDKs use the same profile identity. ## Setup @@ -41,15 +93,57 @@ pnpm implementation:run -- react-web-sdk_nextjs serve:stop ``` app/ - layout.tsx # Root layout (Server Component) wraps children in client provider - page.tsx # Home page with navigation to both patterns - client-resolved/ # Client-side optimization resolution demo - server-resolved/ # Server-side pre-resolution + client hydration demo -lib/ - config.ts # Shared SDK configuration from env vars - contentful-client.ts # Contentful CDA client - optimization-server.ts # Node SDK singleton for server-side use + layout.tsx # Root layout — neutral, no SDK + page.tsx # Home page with navigation to both patterns + + client-resolved/ + layout.tsx # Route layout — ClientProviderWrapper (React SDK) + page.tsx # "use client" — React SDK resolves entries, full interactivity + + server-resolved/ + layout.tsx # Route layout — ClientProviderWrapper (React SDK for tracking) + page.tsx # Server Component — Node SDK resolves entries, pure HTML + InteractiveControls.tsx # "use client" — consent/identify buttons only + components/ - OptimizationProvider.tsx # "use client" wrapper for -middleware.ts # Cookie-based profile management + ClientProviderWrapper.tsx # "use client" — dynamic import of OptimizationRoot (ssr:false) + +lib/ + config.ts # Shared SDK configuration from env vars + contentful-client.ts # Contentful CDA client + optimization-server.ts # Node SDK singleton for server-side use + +middleware.ts # Cookie lifecycle — ensures ctfl-opt-aid exists on every request +config/entries.ts # Entry IDs shared across both patterns +types/contentful.ts # Contentful entry type definitions ``` + +## Design Decisions + +### Why `next/dynamic` with `ssr: false`? + +The Web SDK requires browser APIs (`localStorage`, `document.cookie`) and cannot be instantiated +during server rendering. Next.js server-renders `"use client"` components too — the directive only +marks the hydration boundary, not the execution boundary. `dynamic({ ssr: false })` is the standard +Next.js pattern for browser-only third-party SDKs. + +### Why isolated route layouts instead of a root provider? + +Each integration pattern is independent. A customer would choose one approach, not mix them. Keeping +them isolated means: + +- No shared singleton conflicts between routes +- Each pattern is self-contained and testable +- Clear demonstration of what each approach requires + +### Why does the middleware also call `sdk.page()`? + +The middleware runs on every route. Its job is cookie lifecycle — ensuring `ctfl-opt-aid` exists for +first-time visitors regardless of which page they land on. The server-resolved page makes its own +`sdk.page()` call for data fetching. They serve different purposes. + +### Why don't server-resolved entries update on identify/reset? + +Server Components render once on the server. They are not reactive. To see updated entries after a +profile change, the page needs a new server render (page refresh or `router.refresh()`). This is the +fundamental SSR tradeoff: fast first paint at the cost of immediate reactivity. From 4a8771d447a4a29cdb9c2f0a86cb9af9c2ca1986 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Tue, 5 May 2026 13:51:36 +0200 Subject: [PATCH 08/17] docs: document singleton conflict, customer setup context, and future direction Add context on why this reference implementation exists, the known singleton conflict when navigating between routes, and plans to gather customer architecture patterns from pre/post-sales to inform which setups to prioritize. --- .../react-web-sdk_nextjs/README.md | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/implementations/react-web-sdk_nextjs/README.md b/implementations/react-web-sdk_nextjs/README.md index 30b066f1..0597f9f6 100644 --- a/implementations/react-web-sdk_nextjs/README.md +++ b/implementations/react-web-sdk_nextjs/README.md @@ -1,9 +1,55 @@ # Next.js React Web SDK Reference Implementation Next.js App Router reference implementation demonstrating `@contentful/optimization-react-web` -(client-side) and `@contentful/optimization-node` (server-side SSR) as two independent integration +(client-side) and `@contentful/optimization-node` (server-side SSR) as independent integration patterns. +## Purpose and Context + +This reference implementation exists to explore and document the most common customer setups for +personalization in Next.js. In practice, customer architectures vary significantly: + +- **Pure CSR**: SPA-style, React SDK handles everything in the browser +- **Pure SSR with client-side tracking**: Node SDK resolves personalized content on the server, + client JS is limited to event tracking (page views, clicks, consent) +- **Hybrid**: Server resolves entries for fast first paint, client takes over for live reactivity + +The right pattern depends on how the customer's solution is engineered — information typically +gathered during pre-sales and post-sales. This implementation aims to demonstrate the range of +setups so the team and customers can evaluate which approach fits their architecture. + +### Current Status + +This implementation currently demonstrates two patterns independently. They are **not designed to +coexist in the same running application** — navigating between them will cause a singleton conflict +because the Web SDK only allows one instance per runtime. A customer would choose one pattern for +their application, not switch between them at runtime. + +### Known Limitation: Singleton Conflict Between Routes + +The `ContentfulOptimization` Web SDK is a singleton. Both route layouts (`/client-resolved` and +`/server-resolved`) initialize their own `OptimizationRoot` via `ClientProviderWrapper`. Navigating +between routes without a full page reload causes: + +``` +ContentfulOptimization SDK failed to initialize: ContentfulOptimization is already initialized +``` + +This is expected given the current setup — these patterns are meant to be viewed independently +(e.g., direct navigation or separate browser tabs), not as a multi-page app with client-side routing +between them. In a real customer deployment, only one pattern would be active across the +application. + +### Future Direction + +To better understand what setups customers actually use, we may: + +- Gather architecture information from pre-sales and post-sales teams on how customer solutions are + engineered +- Use an AI agent to simulate common customer architectures and identify the most frequent Next.js + feature usage patterns and personalization setups +- Expand this reference implementation into separate focused examples per customer archetype + ## Architecture Each route is fully isolated with its own SDK setup. The root layout is neutral — no SDK From d05d3f0c5f9f0e8a4b38806b096a089bc3122a49 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Tue, 5 May 2026 14:16:28 +0200 Subject: [PATCH 09/17] feat: add react-web-sdk+node-sdk_nextjs SSR hybrid implementation Standalone Next.js App Router implementation where the Node SDK resolves entries server-side and the React SDK hydrates for event tracking and interactive controls (consent, identify). Runs on port 3001 to avoid conflicts with the CSR implementation. --- .../.env.example | 16 ++++ .../react-web-sdk+node-sdk_nextjs/.gitignore | 42 +++++++++ .../react-web-sdk+node-sdk_nextjs/.npmrc | 1 + .../react-web-sdk+node-sdk_nextjs/AGENTS.md | 45 ++++++++++ .../react-web-sdk+node-sdk_nextjs/README.md | 35 ++++++++ .../app/globals.css | 1 + .../app/layout.tsx | 23 +++++ .../app/page.tsx | 86 ++++++++++++++++++ .../components/ClientProviderWrapper.tsx | 52 +++++++++++ .../components/InteractiveControls.tsx | 90 +++++++++++++++++++ .../config/entries.ts | 17 ++++ .../eslint.config.mjs | 11 +++ .../lib/config.ts | 23 +++++ .../lib/contentful-client.ts | 34 +++++++ .../lib/optimization-server.ts | 15 ++++ .../middleware.ts | 37 ++++++++ .../next.config.ts | 20 +++++ .../package.json | 68 ++++++++++++++ .../pnpm-workspace.yaml | 3 + .../postcss.config.mjs | 7 ++ .../tsconfig.json | 38 ++++++++ .../types/contentful.ts | 11 +++ package.json | 1 + 23 files changed, 676 insertions(+) create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/.env.example create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/.gitignore create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/.npmrc create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/AGENTS.md create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/README.md create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/app/globals.css create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/app/layout.tsx create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/app/page.tsx create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/components/ClientProviderWrapper.tsx create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/components/InteractiveControls.tsx create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/config/entries.ts create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/eslint.config.mjs create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/lib/config.ts create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/lib/contentful-client.ts create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/lib/optimization-server.ts create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/middleware.ts create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/next.config.ts create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/package.json create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/pnpm-workspace.yaml create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/postcss.config.mjs create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/tsconfig.json create mode 100644 implementations/react-web-sdk+node-sdk_nextjs/types/contentful.ts diff --git a/implementations/react-web-sdk+node-sdk_nextjs/.env.example b/implementations/react-web-sdk+node-sdk_nextjs/.env.example new file mode 100644 index 00000000..9273d7d6 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/.env.example @@ -0,0 +1,16 @@ +DOTENV_CONFIG_QUIET=true + +PUBLIC_NINETAILED_CLIENT_ID="mock-client-id" +PUBLIC_NINETAILED_ENVIRONMENT="main" + +PUBLIC_EXPERIENCE_API_BASE_URL="http://localhost:8000/experience/" +PUBLIC_INSIGHTS_API_BASE_URL="http://localhost:8000/insights/" + +PUBLIC_CONTENTFUL_TOKEN="mock-token" +PUBLIC_CONTENTFUL_PREVIEW_TOKEN="mock-preview-token" +PUBLIC_CONTENTFUL_ENVIRONMENT="master" +PUBLIC_CONTENTFUL_SPACE_ID="mock-space-id" + +PUBLIC_CONTENTFUL_CDA_HOST="localhost:8000" +PUBLIC_CONTENTFUL_BASE_PATH="contentful" +PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL="false" diff --git a/implementations/react-web-sdk+node-sdk_nextjs/.gitignore b/implementations/react-web-sdk+node-sdk_nextjs/.gitignore new file mode 100644 index 00000000..818f88a7 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/.gitignore @@ -0,0 +1,42 @@ +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage +/playwright-report +/test-results + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/implementations/react-web-sdk+node-sdk_nextjs/.npmrc b/implementations/react-web-sdk+node-sdk_nextjs/.npmrc new file mode 100644 index 00000000..135f7a0d --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/.npmrc @@ -0,0 +1 @@ +shared-workspace-lockfile=false diff --git a/implementations/react-web-sdk+node-sdk_nextjs/AGENTS.md b/implementations/react-web-sdk+node-sdk_nextjs/AGENTS.md new file mode 100644 index 00000000..f8ebb6cf --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/AGENTS.md @@ -0,0 +1,45 @@ +# AGENTS.md + +Read the repository root `AGENTS.md` first. + +## Scope + +This is the Next.js SSR hybrid reference implementation. The Node SDK resolves entries on the server +(personalization is SSR). The React SDK hydrates on the client for event tracking and interactive +controls (consent, identify, page views, clicks). + +This represents a customer setup where: + +- Personalized content is resolved server-side for fast first paint +- Client-side JS is only used for tracking and interactive features +- The same anonymous profile cookie bridges server and client + +## Key Paths + +- `app/` — Next.js App Router (single page, Server Component) +- `lib/` — SDK config, Contentful client, Node SDK singleton +- `components/` — ClientProviderWrapper (React SDK), InteractiveControls +- `middleware.ts` — cookie lifecycle +- `.env.example` + +## Local Rules + +- Next.js App Router only. No Pages Router. +- Server Components must not import from `@contentful/optimization-react-web`. +- Client components (`"use client"`) must not import from `@contentful/optimization-node`. +- Use the SDK's `OptimizationRoot` directly — no custom provider wrappers around it. +- If you changed a consumed package, run `pnpm build:pkgs` and reinstall before trusting results. + +## Commands + +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs implementation:install` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs typecheck` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs build` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs dev` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs serve` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs serve:stop` + +## Usually Validate + +- Run `typecheck` for local code changes. +- Run `build` when changing production bundling behavior. diff --git a/implementations/react-web-sdk+node-sdk_nextjs/README.md b/implementations/react-web-sdk+node-sdk_nextjs/README.md new file mode 100644 index 00000000..2cfa8445 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/README.md @@ -0,0 +1,35 @@ +# Next.js SSR Hybrid Reference Implementation + +`react-web-sdk+node-sdk_nextjs` — Next.js App Router reference using `@contentful/optimization-node` +for server-side entry resolution and `@contentful/optimization-react-web` for client-side event +tracking and interactive controls. + +## Architecture + +``` +Request + → Middleware: reads ctfl-opt-aid cookie, calls Node SDK sdk.page(), sets cookie + → Server Component (page.tsx): reads cookie, calls Node SDK sdk.page(), + fetches entries from CDA, resolves with sdk.resolveOptimizedEntry(), + renders personalized HTML + +Browser + → HTML arrives with correct personalized content (zero JS for content) + → React SDK hydrates via ClientProviderWrapper (dynamic, ssr:false) + → InteractiveControls hydrate for consent/identify buttons + → Auto page tracking and entry interaction tracking active +``` + +## Setup + +```bash +pnpm build:pkgs +pnpm implementation:run -- react-web-sdk+node-sdk_nextjs implementation:install +cp implementations/react-web-sdk+node-sdk_nextjs/.env.example implementations/react-web-sdk+node-sdk_nextjs/.env +``` + +## Development + +```bash +pnpm implementation:run -- react-web-sdk+node-sdk_nextjs dev +``` diff --git a/implementations/react-web-sdk+node-sdk_nextjs/app/globals.css b/implementations/react-web-sdk+node-sdk_nextjs/app/globals.css new file mode 100644 index 00000000..d4b50785 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/app/globals.css @@ -0,0 +1 @@ +@import 'tailwindcss'; diff --git a/implementations/react-web-sdk+node-sdk_nextjs/app/layout.tsx b/implementations/react-web-sdk+node-sdk_nextjs/app/layout.tsx new file mode 100644 index 00000000..3f6c44b9 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/app/layout.tsx @@ -0,0 +1,23 @@ +import { ClientProviderWrapper } from '@/components/ClientProviderWrapper' +import type { Metadata } from 'next' +import './globals.css' + +export const metadata: Metadata = { + title: 'Optimization Next.js SSR Hybrid', + description: + 'Next.js App Router reference: Node SDK resolves entries server-side, React SDK handles client-side tracking and interactive controls.', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + ) +} diff --git a/implementations/react-web-sdk+node-sdk_nextjs/app/page.tsx b/implementations/react-web-sdk+node-sdk_nextjs/app/page.tsx new file mode 100644 index 00000000..b4adc6af --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/app/page.tsx @@ -0,0 +1,86 @@ +import { InteractiveControls } from '@/components/InteractiveControls' +import { ENTRY_IDS } from '@/config/entries' +import { fetchEntries } from '@/lib/contentful-client' +import { sdk } from '@/lib/optimization-server' +import type { ContentEntry } from '@/types/contentful' +import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants' +import { cookies, headers } from 'next/headers' + +function getEntryText(entry: ContentEntry): string { + return typeof entry.fields.text === 'string' ? entry.fields.text : 'No content' +} + +function ServerRenderedEntry({ + baselineEntry, + resolvedEntry, +}: { + baselineEntry: ContentEntry + resolvedEntry: ContentEntry +}) { + return ( +
+

{getEntryText(resolvedEntry)}

+

{`[Entry: ${baselineEntry.sys.id}]`}

+
+ ) +} + +export default async function Home() { + const cookieStore = await cookies() + const headerStore = await headers() + + const anonymousId = cookieStore.get(ANONYMOUS_ID_COOKIE)?.value + const profile = anonymousId ? { id: anonymousId } : undefined + + const [baselineEntries, optimizationData] = await Promise.all([ + fetchEntries(ENTRY_IDS), + sdk.page({ + locale: headerStore.get('accept-language')?.split(',')[0] ?? 'en-US', + userAgent: headerStore.get('user-agent') ?? 'next-js-server', + profile, + }), + ]) + + const resolvedEntries = baselineEntries.map((entry) => { + const { entry: resolved } = sdk.resolveOptimizedEntry( + entry, + optimizationData.selectedOptimizations, + ) + return resolved + }) + + return ( +
+

Next.js SSR Hybrid

+

+ Entries are resolved on the server via the Node SDK. The HTML contains personalized content + with zero client-side JavaScript for rendering. The React SDK hydrates for event tracking + and interactive controls (consent, identify). +

+ + + +
+

Entries (Server-Resolved)

+ {baselineEntries.length === 0 ? ( +

No entries found.

+ ) : ( +
+ {baselineEntries.map((entry, index) => ( + + ))} +
+ )} +
+
+ ) +} diff --git a/implementations/react-web-sdk+node-sdk_nextjs/components/ClientProviderWrapper.tsx b/implementations/react-web-sdk+node-sdk_nextjs/components/ClientProviderWrapper.tsx new file mode 100644 index 00000000..d5158bce --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/components/ClientProviderWrapper.tsx @@ -0,0 +1,52 @@ +'use client' + +// The Web SDK requires browser APIs (localStorage, document.cookie) and cannot +// be instantiated during SSR. Using next/dynamic with ssr:false ensures +// OptimizationRoot is only loaded and rendered on the client. Without this, +// child components that call useOptimization() would throw when rendered +// outside the provider during server rendering. + +import { optimizationConfig } from '@/lib/config' +import dynamic from 'next/dynamic' +import { Suspense, type ReactNode } from 'react' + +const OptimizationRoot = dynamic( + () => + import('@contentful/optimization-react-web').then((mod) => ({ + default: mod.OptimizationRoot, + })), + { ssr: false }, +) + +const NextAppAutoPageTracker = dynamic( + () => + import('@contentful/optimization-react-web/router/next-app').then((mod) => ({ + default: mod.NextAppAutoPageTracker, + })), + { ssr: false }, +) + +interface ClientProviderWrapperProps { + readonly children: ReactNode +} + +export function ClientProviderWrapper({ children }: ClientProviderWrapperProps) { + return ( + + + + + {children} + + ) +} diff --git a/implementations/react-web-sdk+node-sdk_nextjs/components/InteractiveControls.tsx b/implementations/react-web-sdk+node-sdk_nextjs/components/InteractiveControls.tsx new file mode 100644 index 00000000..d6f1c4f3 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/components/InteractiveControls.tsx @@ -0,0 +1,90 @@ +'use client' + +import { useOptimizationContext } from '@contentful/optimization-react-web' +import type { Profile } from '@contentful/optimization-react-web/api-schemas' +import { type JSX, useEffect, useMemo, useState } from 'react' + +export function InteractiveControls(): JSX.Element { + const { sdk, isReady } = useOptimizationContext() + const [consent, setConsent] = useState(undefined) + const [profile, setProfile] = useState(undefined) + + useEffect(() => { + if (!sdk || !isReady) { + return + } + + const consentSub = sdk.states.consent.subscribe((value: boolean | undefined) => { + setConsent(value) + }) + + const profileSub = sdk.states.profile.subscribe((value: Profile | undefined) => { + setProfile(value) + }) + + return () => { + consentSub.unsubscribe() + profileSub.unsubscribe() + } + }, [isReady, sdk]) + + const isIdentified = useMemo( + () => profile !== undefined && Boolean(profile.traits.identified), + [profile], + ) + + if (!sdk || !isReady) { + return ( +
+

SDK loading...

+
+ ) + } + + return ( +
+

Controls

+
+ + + {!isIdentified ? ( + + ) : ( + + )} +
+ +
+

Consent: {String(consent)}

+

Identified: {isIdentified ? 'Yes' : 'No'}

+
+
+ ) +} diff --git a/implementations/react-web-sdk+node-sdk_nextjs/config/entries.ts b/implementations/react-web-sdk+node-sdk_nextjs/config/entries.ts new file mode 100644 index 00000000..72962fff --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/config/entries.ts @@ -0,0 +1,17 @@ +export const AUTO_OBSERVED_ENTRY_IDS = [ + '1JAU028vQ7v6nB2swl3NBo', + '1MwiFl4z7gkwqGYdvCmr8c', + '4ib0hsHWoSOnCVdDkizE8d', + 'xFwgG3oNaOcjzWiGe4vXo', + '2Z2WLOx07InSewC3LUB3eX', +] as const + +export const MANUALLY_OBSERVED_ENTRY_IDS = [ + '5XHssysWUDECHzKLzoIsg1', + '6zqoWXyiSrf0ja7I2WGtYj', + '7pa5bOx8Z9NmNcr7mISvD', +] as const + +export const ENTRY_IDS = [...AUTO_OBSERVED_ENTRY_IDS, ...MANUALLY_OBSERVED_ENTRY_IDS] as const + +export const LIVE_UPDATES_ENTRY_ID = '2Z2WLOx07InSewC3LUB3eX' as const diff --git a/implementations/react-web-sdk+node-sdk_nextjs/eslint.config.mjs b/implementations/react-web-sdk+node-sdk_nextjs/eslint.config.mjs new file mode 100644 index 00000000..7c81a1b4 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/eslint.config.mjs @@ -0,0 +1,11 @@ +import nextVitals from 'eslint-config-next/core-web-vitals' +import nextTs from 'eslint-config-next/typescript' +import { defineConfig, globalIgnores } from 'eslint/config' + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']), +]) + +export default eslintConfig diff --git a/implementations/react-web-sdk+node-sdk_nextjs/lib/config.ts b/implementations/react-web-sdk+node-sdk_nextjs/lib/config.ts new file mode 100644 index 00000000..5abfe222 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/lib/config.ts @@ -0,0 +1,23 @@ +const CLIENT_ID = process.env.PUBLIC_NINETAILED_CLIENT_ID?.trim() ?? 'mock-client-id' +const ENVIRONMENT = process.env.PUBLIC_NINETAILED_ENVIRONMENT?.trim() ?? 'main' +const INSIGHTS_BASE_URL = + process.env.PUBLIC_INSIGHTS_API_BASE_URL?.trim() ?? 'http://localhost:8000/insights/' +const EXPERIENCE_BASE_URL = + process.env.PUBLIC_EXPERIENCE_API_BASE_URL?.trim() ?? 'http://localhost:8000/experience/' + +export const optimizationConfig = { + clientId: CLIENT_ID, + environment: ENVIRONMENT, + api: { + insightsBaseUrl: INSIGHTS_BASE_URL, + experienceBaseUrl: EXPERIENCE_BASE_URL, + }, +} as const + +export const contentfulConfig = { + accessToken: process.env.PUBLIC_CONTENTFUL_TOKEN?.trim() ?? '', + environment: process.env.PUBLIC_CONTENTFUL_ENVIRONMENT?.trim() ?? '', + host: process.env.PUBLIC_CONTENTFUL_CDA_HOST?.trim() ?? '', + space: process.env.PUBLIC_CONTENTFUL_SPACE_ID?.trim() ?? '', + basePath: process.env.PUBLIC_CONTENTFUL_BASE_PATH?.trim(), +} as const diff --git a/implementations/react-web-sdk+node-sdk_nextjs/lib/contentful-client.ts b/implementations/react-web-sdk+node-sdk_nextjs/lib/contentful-client.ts new file mode 100644 index 00000000..aa0b99c4 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/lib/contentful-client.ts @@ -0,0 +1,34 @@ +import type { ContentEntry, ContentEntrySkeleton } from '@/types/contentful' +import { createClient } from 'contentful' +import { contentfulConfig } from './config' + +const INCLUDE_DEPTH = 10 + +function createContentfulClient(): ReturnType { + return createClient({ + accessToken: contentfulConfig.accessToken, + environment: contentfulConfig.environment, + host: contentfulConfig.host, + insecure: contentfulConfig.host.includes('localhost'), + space: contentfulConfig.space, + ...(contentfulConfig.basePath ? { basePath: contentfulConfig.basePath } : {}), + }) +} + +const contentfulClient = createContentfulClient() + +export async function fetchEntry(entryId: string): Promise { + try { + return await contentfulClient.getEntry(entryId, { + include: INCLUDE_DEPTH, + }) + } catch { + return undefined + } +} + +export async function fetchEntries(entryIds: readonly string[]): Promise { + const fetchedEntries = await Promise.all(entryIds.map(fetchEntry)) + + return fetchedEntries.filter((entry): entry is ContentEntry => entry !== undefined) +} diff --git a/implementations/react-web-sdk+node-sdk_nextjs/lib/optimization-server.ts b/implementations/react-web-sdk+node-sdk_nextjs/lib/optimization-server.ts new file mode 100644 index 00000000..5a48f116 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/lib/optimization-server.ts @@ -0,0 +1,15 @@ +import ContentfulOptimization from '@contentful/optimization-node' +import { optimizationConfig } from './config' + +const sdk = new ContentfulOptimization({ + clientId: optimizationConfig.clientId, + environment: optimizationConfig.environment, + logLevel: 'debug', + api: optimizationConfig.api, + app: { + name: 'ContentfulOptimization SDK - Next.js SSR Hybrid (Server)', + version: '0.1.0', + }, +}) + +export { sdk } diff --git a/implementations/react-web-sdk+node-sdk_nextjs/middleware.ts b/implementations/react-web-sdk+node-sdk_nextjs/middleware.ts new file mode 100644 index 00000000..3e37b251 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/middleware.ts @@ -0,0 +1,37 @@ +import { sdk } from '@/lib/optimization-server' +import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants' +import { type NextRequest, NextResponse } from 'next/server' + +export async function middleware(request: NextRequest): Promise { + const anonymousId = request.cookies.get(ANONYMOUS_ID_COOKIE)?.value + const profile = anonymousId ? { id: anonymousId } : undefined + + const url = new URL(request.url) + const data = await sdk.page({ + locale: request.headers.get('accept-language')?.split(',')[0] ?? 'en-US', + userAgent: request.headers.get('user-agent') ?? 'next-js-server', + page: { + path: url.pathname, + query: Object.fromEntries(url.searchParams), + referrer: request.headers.get('referer') ?? '', + search: url.search, + url: request.url, + }, + profile, + }) + + const response = NextResponse.next() + + if (data.profile.id) { + response.cookies.set(ANONYMOUS_ID_COOKIE, data.profile.id, { + path: '/', + sameSite: 'lax', + }) + } + + return response +} + +export const config = { + matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'], +} diff --git a/implementations/react-web-sdk+node-sdk_nextjs/next.config.ts b/implementations/react-web-sdk+node-sdk_nextjs/next.config.ts new file mode 100644 index 00000000..9333b88a --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/next.config.ts @@ -0,0 +1,20 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + serverExternalPackages: ['@contentful/optimization-node'], + env: { + PUBLIC_NINETAILED_CLIENT_ID: process.env.PUBLIC_NINETAILED_CLIENT_ID, + PUBLIC_NINETAILED_ENVIRONMENT: process.env.PUBLIC_NINETAILED_ENVIRONMENT, + PUBLIC_EXPERIENCE_API_BASE_URL: process.env.PUBLIC_EXPERIENCE_API_BASE_URL, + PUBLIC_INSIGHTS_API_BASE_URL: process.env.PUBLIC_INSIGHTS_API_BASE_URL, + PUBLIC_CONTENTFUL_TOKEN: process.env.PUBLIC_CONTENTFUL_TOKEN, + PUBLIC_CONTENTFUL_PREVIEW_TOKEN: process.env.PUBLIC_CONTENTFUL_PREVIEW_TOKEN, + PUBLIC_CONTENTFUL_ENVIRONMENT: process.env.PUBLIC_CONTENTFUL_ENVIRONMENT, + PUBLIC_CONTENTFUL_SPACE_ID: process.env.PUBLIC_CONTENTFUL_SPACE_ID, + PUBLIC_CONTENTFUL_CDA_HOST: process.env.PUBLIC_CONTENTFUL_CDA_HOST, + PUBLIC_CONTENTFUL_BASE_PATH: process.env.PUBLIC_CONTENTFUL_BASE_PATH, + PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL: process.env.PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL, + }, +} + +export default nextConfig diff --git a/implementations/react-web-sdk+node-sdk_nextjs/package.json b/implementations/react-web-sdk+node-sdk_nextjs/package.json new file mode 100644 index 00000000..bebfc4eb --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/package.json @@ -0,0 +1,68 @@ +{ + "name": "@implementation/react-web-sdk+node-sdk_nextjs", + "private": true, + "version": "0.0.0", + "description": "Reference implementation for Next.js (App Router) using @contentful/optimization-node for SSR entry resolution and @contentful/optimization-react-web for client-side event tracking", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "next dev --port 3001", + "build": "next build", + "start": "next start --port 3001", + "clean": "rimraf .next coverage playwright-report test-results .tsbuildinfo", + "preview": "pnpm serve:mocks && pnpm start", + "serve": "pnpm serve:mocks && pnpm serve:app", + "serve:app": "pnpm build && pm2 start --name nextjs-ssr-hybrid-app \"pnpm start\"", + "serve:app:stop": "pm2 stop nextjs-ssr-hybrid-app && pm2 delete nextjs-ssr-hybrid-app", + "serve:mocks": "pm2 start --name nextjs-ssr-hybrid-mocks \"pnpm --dir ../../lib/mocks serve\"", + "serve:mocks:stop": "pm2 stop nextjs-ssr-hybrid-mocks && pm2 delete nextjs-ssr-hybrid-mocks", + "serve:stop": "pnpm serve:app:stop && pnpm serve:mocks:stop", + "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", + "test:e2e:codegen": "playwright codegen", + "test:e2e:report": "playwright show-report", + "test:e2e:ui": "playwright test --ui", + "implementation:playwright:install": "playwright install", + "implementation:playwright:install-deps": "playwright install-deps", + "implementation:setup:e2e": "pnpm implementation:playwright:install && pnpm implementation:playwright:install-deps", + "test:unit": "echo \"No unit tests necessary\"", + "typecheck": "tsc --noEmit", + "lint": "eslint" + }, + "dependencies": { + "@contentful/optimization-node": "0.0.0", + "@contentful/optimization-react-web": "0.0.0", + "@contentful/optimization-web-preview-panel": "0.0.0", + "@contentful/rich-text-react-renderer": "16.1.6", + "@contentful/rich-text-types": "17.2.5", + "contentful": "11.10.5", + "next": "16.2.4", + "react": "19.2.5", + "react-dom": "19.2.5" + }, + "devDependencies": { + "@playwright/test": "1.58.2", + "@tailwindcss/postcss": "4.1.11", + "@types/node": "24.11.0", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "dotenv": "17.3.1", + "eslint": "9.29.0", + "eslint-config-next": "16.2.4", + "pm2": "6.0.14", + "postcss": "8.5.6", + "rimraf": "6.1.3", + "tailwindcss": "4.1.11", + "typescript": "5.9.3" + }, + "pnpm": { + "overrides": { + "@contentful/optimization-api-client": "file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz", + "@contentful/optimization-api-schemas": "file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz", + "@contentful/optimization-core": "file:../../pkgs/contentful-optimization-core-0.0.0.tgz", + "@contentful/optimization-node": "file:../../pkgs/contentful-optimization-node-0.0.0.tgz", + "@contentful/optimization-web": "file:../../pkgs/contentful-optimization-web-0.0.0.tgz", + "@contentful/optimization-react-web": "file:../../pkgs/contentful-optimization-react-web-0.0.0.tgz", + "@contentful/optimization-web-preview-panel": "file:../../pkgs/contentful-optimization-web-preview-panel-0.0.0.tgz" + } + } +} diff --git a/implementations/react-web-sdk+node-sdk_nextjs/pnpm-workspace.yaml b/implementations/react-web-sdk+node-sdk_nextjs/pnpm-workspace.yaml new file mode 100644 index 00000000..581a9d5b --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +ignoredBuiltDependencies: + - sharp + - unrs-resolver diff --git a/implementations/react-web-sdk+node-sdk_nextjs/postcss.config.mjs b/implementations/react-web-sdk+node-sdk_nextjs/postcss.config.mjs new file mode 100644 index 00000000..ae85b2fe --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +} + +export default config diff --git a/implementations/react-web-sdk+node-sdk_nextjs/tsconfig.json b/implementations/react-web-sdk+node-sdk_nextjs/tsconfig.json new file mode 100644 index 00000000..2cc12770 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/implementations/react-web-sdk+node-sdk_nextjs/types/contentful.ts b/implementations/react-web-sdk+node-sdk_nextjs/types/contentful.ts new file mode 100644 index 00000000..2efb9ad7 --- /dev/null +++ b/implementations/react-web-sdk+node-sdk_nextjs/types/contentful.ts @@ -0,0 +1,11 @@ +import type { Document } from '@contentful/rich-text-types' +import type { Entry, EntryFieldTypes, EntrySkeletonType } from 'contentful' + +export interface ContentEntryFields { + text?: EntryFieldTypes.Text | EntryFieldTypes.RichText + nested?: EntryFieldTypes.Array> +} + +export type ContentEntrySkeleton = EntrySkeletonType +export type ContentEntry = Entry +export type RichTextDocument = Document diff --git a/package.json b/package.json index 8414ab57..c2038f13 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "implementation:react-web-sdk": "pnpm run implementation:run -- react-web-sdk", "implementation:web-sdk_react": "pnpm run implementation:run -- web-sdk_react", "implementation:react-web-sdk_nextjs": "pnpm run implementation:run -- react-web-sdk_nextjs", + "implementation:react-web-sdk+node-sdk_nextjs": "pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs", "implementation:web-sdk": "pnpm run implementation:run -- web-sdk", "implementation:lint": "eslint implementations --cache --cache-location .cache/eslint/implementations", "implementation:lint:fix": "eslint implementations --fix", From 59363a298e3fbd7487dc2c0687e750348761d169 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Tue, 5 May 2026 14:24:07 +0200 Subject: [PATCH 10/17] chore: remove old react-web-sdk_nextjs combined implementation This branch only needs the SSR hybrid implementation (react-web-sdk+node-sdk_nextjs). The CSR-only implementation will live on its own branch. --- .../react-web-sdk_nextjs/.env.example | 16 -- .../react-web-sdk_nextjs/.gitignore | 42 ---- implementations/react-web-sdk_nextjs/.npmrc | 1 - .../react-web-sdk_nextjs/AGENTS.md | 68 ------ .../react-web-sdk_nextjs/README.md | 195 ------------------ .../app/client-resolved/layout.tsx | 9 - .../app/client-resolved/page.tsx | 166 --------------- .../react-web-sdk_nextjs/app/favicon.ico | Bin 25931 -> 0 bytes .../react-web-sdk_nextjs/app/globals.css | 1 - .../react-web-sdk_nextjs/app/layout.tsx | 20 -- .../react-web-sdk_nextjs/app/page.tsx | 27 --- .../server-resolved/InteractiveControls.tsx | 90 -------- .../app/server-resolved/layout.tsx | 9 - .../app/server-resolved/page.tsx | 86 -------- .../components/ClientProviderWrapper.tsx | 52 ----- .../react-web-sdk_nextjs/config/entries.ts | 17 -- .../react-web-sdk_nextjs/eslint.config.mjs | 18 -- .../react-web-sdk_nextjs/lib/config.ts | 23 --- .../lib/contentful-client.ts | 34 --- .../lib/optimization-server.ts | 15 -- .../react-web-sdk_nextjs/middleware.ts | 37 ---- .../react-web-sdk_nextjs/next.config.ts | 20 -- .../react-web-sdk_nextjs/package.json | 68 ------ .../react-web-sdk_nextjs/pnpm-workspace.yaml | 3 - .../react-web-sdk_nextjs/postcss.config.mjs | 7 - .../react-web-sdk_nextjs/tsconfig.json | 38 ---- .../react-web-sdk_nextjs/types/contentful.ts | 11 - package.json | 1 - 28 files changed, 1074 deletions(-) delete mode 100644 implementations/react-web-sdk_nextjs/.env.example delete mode 100644 implementations/react-web-sdk_nextjs/.gitignore delete mode 100644 implementations/react-web-sdk_nextjs/.npmrc delete mode 100644 implementations/react-web-sdk_nextjs/AGENTS.md delete mode 100644 implementations/react-web-sdk_nextjs/README.md delete mode 100644 implementations/react-web-sdk_nextjs/app/client-resolved/layout.tsx delete mode 100644 implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx delete mode 100644 implementations/react-web-sdk_nextjs/app/favicon.ico delete mode 100644 implementations/react-web-sdk_nextjs/app/globals.css delete mode 100644 implementations/react-web-sdk_nextjs/app/layout.tsx delete mode 100644 implementations/react-web-sdk_nextjs/app/page.tsx delete mode 100644 implementations/react-web-sdk_nextjs/app/server-resolved/InteractiveControls.tsx delete mode 100644 implementations/react-web-sdk_nextjs/app/server-resolved/layout.tsx delete mode 100644 implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx delete mode 100644 implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx delete mode 100644 implementations/react-web-sdk_nextjs/config/entries.ts delete mode 100644 implementations/react-web-sdk_nextjs/eslint.config.mjs delete mode 100644 implementations/react-web-sdk_nextjs/lib/config.ts delete mode 100644 implementations/react-web-sdk_nextjs/lib/contentful-client.ts delete mode 100644 implementations/react-web-sdk_nextjs/lib/optimization-server.ts delete mode 100644 implementations/react-web-sdk_nextjs/middleware.ts delete mode 100644 implementations/react-web-sdk_nextjs/next.config.ts delete mode 100644 implementations/react-web-sdk_nextjs/package.json delete mode 100644 implementations/react-web-sdk_nextjs/pnpm-workspace.yaml delete mode 100644 implementations/react-web-sdk_nextjs/postcss.config.mjs delete mode 100644 implementations/react-web-sdk_nextjs/tsconfig.json delete mode 100644 implementations/react-web-sdk_nextjs/types/contentful.ts diff --git a/implementations/react-web-sdk_nextjs/.env.example b/implementations/react-web-sdk_nextjs/.env.example deleted file mode 100644 index 9273d7d6..00000000 --- a/implementations/react-web-sdk_nextjs/.env.example +++ /dev/null @@ -1,16 +0,0 @@ -DOTENV_CONFIG_QUIET=true - -PUBLIC_NINETAILED_CLIENT_ID="mock-client-id" -PUBLIC_NINETAILED_ENVIRONMENT="main" - -PUBLIC_EXPERIENCE_API_BASE_URL="http://localhost:8000/experience/" -PUBLIC_INSIGHTS_API_BASE_URL="http://localhost:8000/insights/" - -PUBLIC_CONTENTFUL_TOKEN="mock-token" -PUBLIC_CONTENTFUL_PREVIEW_TOKEN="mock-preview-token" -PUBLIC_CONTENTFUL_ENVIRONMENT="master" -PUBLIC_CONTENTFUL_SPACE_ID="mock-space-id" - -PUBLIC_CONTENTFUL_CDA_HOST="localhost:8000" -PUBLIC_CONTENTFUL_BASE_PATH="contentful" -PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL="false" diff --git a/implementations/react-web-sdk_nextjs/.gitignore b/implementations/react-web-sdk_nextjs/.gitignore deleted file mode 100644 index 818f88a7..00000000 --- a/implementations/react-web-sdk_nextjs/.gitignore +++ /dev/null @@ -1,42 +0,0 @@ -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage -/playwright-report -/test-results - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files -.env -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/implementations/react-web-sdk_nextjs/.npmrc b/implementations/react-web-sdk_nextjs/.npmrc deleted file mode 100644 index 135f7a0d..00000000 --- a/implementations/react-web-sdk_nextjs/.npmrc +++ /dev/null @@ -1 +0,0 @@ -shared-workspace-lockfile=false diff --git a/implementations/react-web-sdk_nextjs/AGENTS.md b/implementations/react-web-sdk_nextjs/AGENTS.md deleted file mode 100644 index c5120a64..00000000 --- a/implementations/react-web-sdk_nextjs/AGENTS.md +++ /dev/null @@ -1,68 +0,0 @@ -# AGENTS.md - -Read the repository root `AGENTS.md` first. - -## Scope - -This is the Next.js (App Router) reference implementation demonstrating -`@contentful/optimization-react-web` for client-side optimization and -`@contentful/optimization-node` for server-side optimization. - -It shows two independent integration patterns, each with its own route layout and SDK setup: - -1. **Client-resolved** (`/client-resolved`): React SDK does everything in the browser — entry - resolution, event tracking, consent, identify. No Node SDK. -2. **Server-resolved** (`/server-resolved`): Node SDK resolves entries on the server as pure HTML. - React SDK hydrates only for interactive controls (consent, identify) and event tracking. - -## Key Paths - -- `app/` — Next.js App Router pages and route-scoped layouts -- `app/client-resolved/` — client-only pattern (React SDK) -- `app/server-resolved/` — server pattern (Node SDK + React SDK for tracking) -- `lib/` — shared utilities (SDK config, Contentful client, server-side helpers) -- `components/` — shared client wrapper (`ClientProviderWrapper`) -- `middleware.ts` — cookie lifecycle management -- `.env.example` - -## Local Rules - -- This implementation uses the Next.js App Router exclusively. Do not add Pages Router files. -- The root layout is neutral — no SDK provider. Each route owns its own SDK setup via route layouts. -- All SDK usage on the client must live inside `"use client"` components. Server Components must not - import from `@contentful/optimization-react-web` or `@contentful/optimization-web`. -- Server-side SDK usage must import from `@contentful/optimization-node` only. -- Do not create a custom `OptimizationProvider` wrapper — use the SDK's `OptimizationRoot` directly. -- Do not add a `src/optimization/` directory. Follow the same direct-import pattern as - `implementations/react-web-sdk`. -- If you changed a consumed package, run `pnpm build:pkgs` and reinstall this implementation before - trusting local results. -- `serve` uses PM2-managed processes. Use `serve:stop` when done. - -## Common Failure Modes - -- Package changes are not reflected here: rerun `pnpm build:pkgs`, then - `pnpm implementation:run -- react-web-sdk_nextjs implementation:install`. -- The app or mocks fail to bind local ports such as `3000` or `8000`: stop only this - implementation's local processes with - `pnpm implementation:run -- react-web-sdk_nextjs serve:stop`. -- Behavior differs from the documented mock setup: compare `.env` with `.env.example` before - changing code. -- Next.js caching stale data: run `pnpm clean` to clear the `.next` directory. -- `ContentfulOptimization is already initialized`: stale singleton from a previous dev session or - HMR. Run `window.contentfulOptimization?.destroy()` in the browser console and refresh. - -## Commands - -- `pnpm implementation:run -- react-web-sdk_nextjs implementation:install` -- `pnpm implementation:run -- react-web-sdk_nextjs typecheck` -- `pnpm implementation:run -- react-web-sdk_nextjs build` -- `pnpm implementation:run -- react-web-sdk_nextjs dev` -- `pnpm implementation:run -- react-web-sdk_nextjs serve` -- `pnpm implementation:run -- react-web-sdk_nextjs serve:stop` - -## Usually Validate - -- Run `typecheck` for local code changes. -- Run `build` when changing production bundling behavior. -- There are no meaningful unit tests here. diff --git a/implementations/react-web-sdk_nextjs/README.md b/implementations/react-web-sdk_nextjs/README.md deleted file mode 100644 index 0597f9f6..00000000 --- a/implementations/react-web-sdk_nextjs/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Next.js React Web SDK Reference Implementation - -Next.js App Router reference implementation demonstrating `@contentful/optimization-react-web` -(client-side) and `@contentful/optimization-node` (server-side SSR) as independent integration -patterns. - -## Purpose and Context - -This reference implementation exists to explore and document the most common customer setups for -personalization in Next.js. In practice, customer architectures vary significantly: - -- **Pure CSR**: SPA-style, React SDK handles everything in the browser -- **Pure SSR with client-side tracking**: Node SDK resolves personalized content on the server, - client JS is limited to event tracking (page views, clicks, consent) -- **Hybrid**: Server resolves entries for fast first paint, client takes over for live reactivity - -The right pattern depends on how the customer's solution is engineered — information typically -gathered during pre-sales and post-sales. This implementation aims to demonstrate the range of -setups so the team and customers can evaluate which approach fits their architecture. - -### Current Status - -This implementation currently demonstrates two patterns independently. They are **not designed to -coexist in the same running application** — navigating between them will cause a singleton conflict -because the Web SDK only allows one instance per runtime. A customer would choose one pattern for -their application, not switch between them at runtime. - -### Known Limitation: Singleton Conflict Between Routes - -The `ContentfulOptimization` Web SDK is a singleton. Both route layouts (`/client-resolved` and -`/server-resolved`) initialize their own `OptimizationRoot` via `ClientProviderWrapper`. Navigating -between routes without a full page reload causes: - -``` -ContentfulOptimization SDK failed to initialize: ContentfulOptimization is already initialized -``` - -This is expected given the current setup — these patterns are meant to be viewed independently -(e.g., direct navigation or separate browser tabs), not as a multi-page app with client-side routing -between them. In a real customer deployment, only one pattern would be active across the -application. - -### Future Direction - -To better understand what setups customers actually use, we may: - -- Gather architecture information from pre-sales and post-sales teams on how customer solutions are - engineered -- Use an AI agent to simulate common customer architectures and identify the most frequent Next.js - feature usage patterns and personalization setups -- Expand this reference implementation into separate focused examples per customer archetype - -## Architecture - -Each route is fully isolated with its own SDK setup. The root layout is neutral — no SDK -initialization at the app level. - -### Client-Resolved (`/client-resolved`) - -The React SDK does everything in the browser. No Node SDK involvement. - -``` -Browser loads page - → JavaScript downloads and executes - → React SDK initializes (browser-only singleton via dynamic import, ssr:false) - → SDK calls Experience API from the browser - → Component fetches entries from Contentful CDA - → SDK resolves entries client-side - → React renders content -``` - -- **First paint**: empty/loading state until JS loads and SDK resolves -- **Reactivity**: immediate — identify, consent, reset all re-resolve entries instantly -- **Client JS**: required for all content rendering - -### Server-Resolved (`/server-resolved`) - -The Node SDK resolves entries on the server. Client JS is only needed for interactive controls -(consent, identify) and event tracking — not for content rendering. - -``` -Request hits server - → Middleware reads ctfl-opt-aid cookie, calls Node SDK sdk.page(), sets/refreshes cookie - → Server Component reads cookie, calls Node SDK sdk.page() for selectedOptimizations - → Server Component fetches entries from Contentful CDA - → Node SDK resolves entries with sdk.resolveOptimizedEntry() - → Server renders final HTML with correct personalized content - -Browser receives HTML - → Content is visible immediately (static server HTML, zero JS) - → InteractiveControls component hydrates for consent/identify buttons - → Entry content does NOT re-render on hydration -``` - -- **First paint**: correct resolved content immediately, no loading flash -- **Reactivity**: profile changes (identify/reset) require a page refresh to re-resolve entries -- **Client JS**: only for interactive controls, not for content - -### Key Differences - -| | Client-Resolved | Server-Resolved | -| -------------------------- | --------------------- | ----------------------------- | -| Who resolves entries | Browser (React SDK) | Server (Node SDK) | -| First paint | Loading state | Correct content | -| Client JS for content | Required | None | -| Experience API called from | Browser | Server (middleware + page) | -| Profile changes | Instant re-resolution | Requires page refresh | -| `"use client"` boundary | Entire page | Small controls component only | - -### Cookie-Based Profile Consistency - -Both patterns share the `ctfl-opt-aid` cookie managed by Next.js middleware. The middleware runs on -every request and ensures an anonymous profile exists before any page renders. This is the bridge -between server and client — both SDKs use the same profile identity. - -## Setup - -```bash -# From the repository root: -pnpm build:pkgs -pnpm implementation:run -- react-web-sdk_nextjs implementation:install - -# Copy env: -cp implementations/react-web-sdk_nextjs/.env.example implementations/react-web-sdk_nextjs/.env -``` - -## Development - -```bash -# Start mock server + dev server: -pnpm implementation:run -- react-web-sdk_nextjs dev - -# Or with PM2-managed processes: -pnpm implementation:run -- react-web-sdk_nextjs serve -pnpm implementation:run -- react-web-sdk_nextjs serve:stop -``` - -## Key Files - -``` -app/ - layout.tsx # Root layout — neutral, no SDK - page.tsx # Home page with navigation to both patterns - - client-resolved/ - layout.tsx # Route layout — ClientProviderWrapper (React SDK) - page.tsx # "use client" — React SDK resolves entries, full interactivity - - server-resolved/ - layout.tsx # Route layout — ClientProviderWrapper (React SDK for tracking) - page.tsx # Server Component — Node SDK resolves entries, pure HTML - InteractiveControls.tsx # "use client" — consent/identify buttons only - -components/ - ClientProviderWrapper.tsx # "use client" — dynamic import of OptimizationRoot (ssr:false) - -lib/ - config.ts # Shared SDK configuration from env vars - contentful-client.ts # Contentful CDA client - optimization-server.ts # Node SDK singleton for server-side use - -middleware.ts # Cookie lifecycle — ensures ctfl-opt-aid exists on every request -config/entries.ts # Entry IDs shared across both patterns -types/contentful.ts # Contentful entry type definitions -``` - -## Design Decisions - -### Why `next/dynamic` with `ssr: false`? - -The Web SDK requires browser APIs (`localStorage`, `document.cookie`) and cannot be instantiated -during server rendering. Next.js server-renders `"use client"` components too — the directive only -marks the hydration boundary, not the execution boundary. `dynamic({ ssr: false })` is the standard -Next.js pattern for browser-only third-party SDKs. - -### Why isolated route layouts instead of a root provider? - -Each integration pattern is independent. A customer would choose one approach, not mix them. Keeping -them isolated means: - -- No shared singleton conflicts between routes -- Each pattern is self-contained and testable -- Clear demonstration of what each approach requires - -### Why does the middleware also call `sdk.page()`? - -The middleware runs on every route. Its job is cookie lifecycle — ensuring `ctfl-opt-aid` exists for -first-time visitors regardless of which page they land on. The server-resolved page makes its own -`sdk.page()` call for data fetching. They serve different purposes. - -### Why don't server-resolved entries update on identify/reset? - -Server Components render once on the server. They are not reactive. To see updated entries after a -profile change, the page needs a new server render (page refresh or `router.refresh()`). This is the -fundamental SSR tradeoff: fast first paint at the cost of immediate reactivity. diff --git a/implementations/react-web-sdk_nextjs/app/client-resolved/layout.tsx b/implementations/react-web-sdk_nextjs/app/client-resolved/layout.tsx deleted file mode 100644 index 559e42a1..00000000 --- a/implementations/react-web-sdk_nextjs/app/client-resolved/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { ClientProviderWrapper } from '@/components/ClientProviderWrapper' - -export default function ClientResolvedLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return {children} -} diff --git a/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx b/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx deleted file mode 100644 index f93dc500..00000000 --- a/implementations/react-web-sdk_nextjs/app/client-resolved/page.tsx +++ /dev/null @@ -1,166 +0,0 @@ -'use client' - -import { ENTRY_IDS } from '@/config/entries' -import { fetchEntries } from '@/lib/contentful-client' -import type { ContentEntry } from '@/types/contentful' -import { useOptimization, useOptimizationContext } from '@contentful/optimization-react-web' -import type { Profile } from '@contentful/optimization-react-web/api-schemas' -import { type JSX, useEffect, useMemo, useState } from 'react' - -function getEntryText(entry: ContentEntry): string { - return typeof entry.fields.text === 'string' ? entry.fields.text : 'No content' -} - -function ResolvedEntry({ entry }: { entry: ContentEntry }): JSX.Element { - const { resolveEntry, resolveEntryData } = useOptimization() - const resolvedEntry = resolveEntry(entry) - const { selectedOptimization } = resolveEntryData(entry) - const experienceId = - selectedOptimization && - typeof selectedOptimization === 'object' && - 'experienceId' in selectedOptimization - ? (selectedOptimization as { experienceId: string }).experienceId - : undefined - - return ( -
-

{getEntryText(resolvedEntry)}

-

{`[Entry: ${entry.sys.id}]`}

-
- ) -} - -function EntryList(): JSX.Element { - const { sdk, isReady } = useOptimizationContext() - const [entries, setEntries] = useState([]) - const [consent, setConsent] = useState(undefined) - const [profile, setProfile] = useState(undefined) - const [selectedOptimizationCount, setSelectedOptimizationCount] = useState(0) - - useEffect(() => { - if (!sdk || !isReady) { - return - } - - const consentSub = sdk.states.consent.subscribe((value: boolean | undefined) => { - setConsent(value) - }) - - const profileSub = sdk.states.profile.subscribe((value: Profile | undefined) => { - setProfile(value) - }) - - const selectedOptSub = sdk.states.selectedOptimizations.subscribe((value) => { - setSelectedOptimizationCount(Array.isArray(value) ? value.length : 0) - }) - - return () => { - consentSub.unsubscribe() - profileSub.unsubscribe() - selectedOptSub.unsubscribe() - } - }, [isReady, sdk]) - - useEffect(() => { - if (!sdk || !isReady) { - return - } - - void fetchEntries(ENTRY_IDS).then((nextEntries) => { - setEntries(nextEntries) - }) - }, [isReady, sdk]) - - const isIdentified = useMemo( - () => profile !== undefined && Boolean(profile.traits.identified), - [profile], - ) - - if (!sdk || !isReady) { - return

Loading SDK...

- } - - return ( -
-
-

Controls

-
- - - {!isIdentified ? ( - - ) : ( - - )} -
- -
-

Consent: {String(consent)}

-

- Selected Optimizations: {selectedOptimizationCount} -

-

Identified: {isIdentified ? 'Yes' : 'No'}

-
-
- -
-

Entries (Client-Resolved)

- {entries.length === 0 ? ( -

Loading entries...

- ) : ( -
- {entries.map((entry) => ( - - ))} -
- )} -
-
- ) -} - -export default function ClientResolvedPage(): JSX.Element { - return ( -
-

Client-Resolved Pattern

-

- Entries are fetched from Contentful and resolved entirely on the client via the React SDK. - The server renders an HTML shell; the Web SDK resolves optimizations in the browser. -

- -
- ) -} diff --git a/implementations/react-web-sdk_nextjs/app/favicon.ico b/implementations/react-web-sdk_nextjs/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/implementations/react-web-sdk_nextjs/app/globals.css b/implementations/react-web-sdk_nextjs/app/globals.css deleted file mode 100644 index d4b50785..00000000 --- a/implementations/react-web-sdk_nextjs/app/globals.css +++ /dev/null @@ -1 +0,0 @@ -@import 'tailwindcss'; diff --git a/implementations/react-web-sdk_nextjs/app/layout.tsx b/implementations/react-web-sdk_nextjs/app/layout.tsx deleted file mode 100644 index 8835a4e0..00000000 --- a/implementations/react-web-sdk_nextjs/app/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { Metadata } from 'next' -import './globals.css' - -export const metadata: Metadata = { - title: 'Optimization Next.js Reference Implementation', - description: - 'Next.js App Router reference implementation using @contentful/optimization-react-web and @contentful/optimization-node', -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - - {children} - - ) -} diff --git a/implementations/react-web-sdk_nextjs/app/page.tsx b/implementations/react-web-sdk_nextjs/app/page.tsx deleted file mode 100644 index a4ff86e7..00000000 --- a/implementations/react-web-sdk_nextjs/app/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Link from 'next/link' - -export default function Home() { - return ( -
-

Optimization Next.js Reference Implementation

-

- This implementation demonstrates two integration patterns with the Contentful Optimization - SDKs in a Next.js App Router application. -

- -
- ) -} diff --git a/implementations/react-web-sdk_nextjs/app/server-resolved/InteractiveControls.tsx b/implementations/react-web-sdk_nextjs/app/server-resolved/InteractiveControls.tsx deleted file mode 100644 index d6f1c4f3..00000000 --- a/implementations/react-web-sdk_nextjs/app/server-resolved/InteractiveControls.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client' - -import { useOptimizationContext } from '@contentful/optimization-react-web' -import type { Profile } from '@contentful/optimization-react-web/api-schemas' -import { type JSX, useEffect, useMemo, useState } from 'react' - -export function InteractiveControls(): JSX.Element { - const { sdk, isReady } = useOptimizationContext() - const [consent, setConsent] = useState(undefined) - const [profile, setProfile] = useState(undefined) - - useEffect(() => { - if (!sdk || !isReady) { - return - } - - const consentSub = sdk.states.consent.subscribe((value: boolean | undefined) => { - setConsent(value) - }) - - const profileSub = sdk.states.profile.subscribe((value: Profile | undefined) => { - setProfile(value) - }) - - return () => { - consentSub.unsubscribe() - profileSub.unsubscribe() - } - }, [isReady, sdk]) - - const isIdentified = useMemo( - () => profile !== undefined && Boolean(profile.traits.identified), - [profile], - ) - - if (!sdk || !isReady) { - return ( -
-

SDK loading...

-
- ) - } - - return ( -
-

Controls

-
- - - {!isIdentified ? ( - - ) : ( - - )} -
- -
-

Consent: {String(consent)}

-

Identified: {isIdentified ? 'Yes' : 'No'}

-
-
- ) -} diff --git a/implementations/react-web-sdk_nextjs/app/server-resolved/layout.tsx b/implementations/react-web-sdk_nextjs/app/server-resolved/layout.tsx deleted file mode 100644 index af161672..00000000 --- a/implementations/react-web-sdk_nextjs/app/server-resolved/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { ClientProviderWrapper } from '@/components/ClientProviderWrapper' - -export default function ServerResolvedLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return {children} -} diff --git a/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx b/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx deleted file mode 100644 index 4abd3255..00000000 --- a/implementations/react-web-sdk_nextjs/app/server-resolved/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ENTRY_IDS } from '@/config/entries' -import { fetchEntries } from '@/lib/contentful-client' -import { sdk } from '@/lib/optimization-server' -import type { ContentEntry } from '@/types/contentful' -import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants' -import { cookies, headers } from 'next/headers' -import { InteractiveControls } from './InteractiveControls' - -function getEntryText(entry: ContentEntry): string { - return typeof entry.fields.text === 'string' ? entry.fields.text : 'No content' -} - -function ServerRenderedEntry({ - baselineEntry, - resolvedEntry, -}: { - baselineEntry: ContentEntry - resolvedEntry: ContentEntry -}) { - return ( -
-

{getEntryText(resolvedEntry)}

-

{`[Entry: ${baselineEntry.sys.id}]`}

-
- ) -} - -export default async function ServerResolvedPage() { - const cookieStore = await cookies() - const headerStore = await headers() - - const anonymousId = cookieStore.get(ANONYMOUS_ID_COOKIE)?.value - const profile = anonymousId ? { id: anonymousId } : undefined - - const [baselineEntries, optimizationData] = await Promise.all([ - fetchEntries(ENTRY_IDS), - sdk.page({ - locale: headerStore.get('accept-language')?.split(',')[0] ?? 'en-US', - userAgent: headerStore.get('user-agent') ?? 'next-js-server', - profile, - }), - ]) - - const resolvedEntries = baselineEntries.map((entry) => { - const { entry: resolved } = sdk.resolveOptimizedEntry( - entry, - optimizationData.selectedOptimizations, - ) - return resolved - }) - - return ( -
-

Server-Resolved Pattern

-

- Entries are pre-resolved on the server via the Node SDK. The HTML contains resolved content - with zero client-side JavaScript for rendering. Interactive features (consent, identify) - hydrate separately. -

- - - -
-

Entries (Server-Resolved)

- {baselineEntries.length === 0 ? ( -

No entries found.

- ) : ( -
- {baselineEntries.map((entry, index) => ( - - ))} -
- )} -
-
- ) -} diff --git a/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx b/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx deleted file mode 100644 index 83eb04ae..00000000 --- a/implementations/react-web-sdk_nextjs/components/ClientProviderWrapper.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client' - -// The Web SDK requires browser APIs (localStorage, document.cookie) and cannot -// be instantiated during SSR. Using next/dynamic with ssr:false ensures -// OptimizationRoot is only loaded and rendered on the client. Without this, -// child components that call useOptimization() would throw when rendered -// outside the provider during server rendering. - -import { optimizationConfig } from '@/lib/config' -import dynamic from 'next/dynamic' -import { Suspense, type ReactNode } from 'react' - -const OptimizationRoot = dynamic( - () => - import('@contentful/optimization-react-web').then((mod) => ({ - default: mod.OptimizationRoot, - })), - { ssr: false }, -) - -const NextAppAutoPageTracker = dynamic( - () => - import('@contentful/optimization-react-web/router/next-app').then((mod) => ({ - default: mod.NextAppAutoPageTracker, - })), - { ssr: false }, -) - -interface ClientProviderWrapperProps { - readonly children: ReactNode -} - -export function ClientProviderWrapper({ children }: ClientProviderWrapperProps) { - return ( - - - - - {children} - - ) -} diff --git a/implementations/react-web-sdk_nextjs/config/entries.ts b/implementations/react-web-sdk_nextjs/config/entries.ts deleted file mode 100644 index 72962fff..00000000 --- a/implementations/react-web-sdk_nextjs/config/entries.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const AUTO_OBSERVED_ENTRY_IDS = [ - '1JAU028vQ7v6nB2swl3NBo', - '1MwiFl4z7gkwqGYdvCmr8c', - '4ib0hsHWoSOnCVdDkizE8d', - 'xFwgG3oNaOcjzWiGe4vXo', - '2Z2WLOx07InSewC3LUB3eX', -] as const - -export const MANUALLY_OBSERVED_ENTRY_IDS = [ - '5XHssysWUDECHzKLzoIsg1', - '6zqoWXyiSrf0ja7I2WGtYj', - '7pa5bOx8Z9NmNcr7mISvD', -] as const - -export const ENTRY_IDS = [...AUTO_OBSERVED_ENTRY_IDS, ...MANUALLY_OBSERVED_ENTRY_IDS] as const - -export const LIVE_UPDATES_ENTRY_ID = '2Z2WLOx07InSewC3LUB3eX' as const diff --git a/implementations/react-web-sdk_nextjs/eslint.config.mjs b/implementations/react-web-sdk_nextjs/eslint.config.mjs deleted file mode 100644 index 05e726d1..00000000 --- a/implementations/react-web-sdk_nextjs/eslint.config.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; - -const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, - // Override default ignores of eslint-config-next. - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - ]), -]); - -export default eslintConfig; diff --git a/implementations/react-web-sdk_nextjs/lib/config.ts b/implementations/react-web-sdk_nextjs/lib/config.ts deleted file mode 100644 index 5abfe222..00000000 --- a/implementations/react-web-sdk_nextjs/lib/config.ts +++ /dev/null @@ -1,23 +0,0 @@ -const CLIENT_ID = process.env.PUBLIC_NINETAILED_CLIENT_ID?.trim() ?? 'mock-client-id' -const ENVIRONMENT = process.env.PUBLIC_NINETAILED_ENVIRONMENT?.trim() ?? 'main' -const INSIGHTS_BASE_URL = - process.env.PUBLIC_INSIGHTS_API_BASE_URL?.trim() ?? 'http://localhost:8000/insights/' -const EXPERIENCE_BASE_URL = - process.env.PUBLIC_EXPERIENCE_API_BASE_URL?.trim() ?? 'http://localhost:8000/experience/' - -export const optimizationConfig = { - clientId: CLIENT_ID, - environment: ENVIRONMENT, - api: { - insightsBaseUrl: INSIGHTS_BASE_URL, - experienceBaseUrl: EXPERIENCE_BASE_URL, - }, -} as const - -export const contentfulConfig = { - accessToken: process.env.PUBLIC_CONTENTFUL_TOKEN?.trim() ?? '', - environment: process.env.PUBLIC_CONTENTFUL_ENVIRONMENT?.trim() ?? '', - host: process.env.PUBLIC_CONTENTFUL_CDA_HOST?.trim() ?? '', - space: process.env.PUBLIC_CONTENTFUL_SPACE_ID?.trim() ?? '', - basePath: process.env.PUBLIC_CONTENTFUL_BASE_PATH?.trim(), -} as const diff --git a/implementations/react-web-sdk_nextjs/lib/contentful-client.ts b/implementations/react-web-sdk_nextjs/lib/contentful-client.ts deleted file mode 100644 index aa0b99c4..00000000 --- a/implementations/react-web-sdk_nextjs/lib/contentful-client.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { ContentEntry, ContentEntrySkeleton } from '@/types/contentful' -import { createClient } from 'contentful' -import { contentfulConfig } from './config' - -const INCLUDE_DEPTH = 10 - -function createContentfulClient(): ReturnType { - return createClient({ - accessToken: contentfulConfig.accessToken, - environment: contentfulConfig.environment, - host: contentfulConfig.host, - insecure: contentfulConfig.host.includes('localhost'), - space: contentfulConfig.space, - ...(contentfulConfig.basePath ? { basePath: contentfulConfig.basePath } : {}), - }) -} - -const contentfulClient = createContentfulClient() - -export async function fetchEntry(entryId: string): Promise { - try { - return await contentfulClient.getEntry(entryId, { - include: INCLUDE_DEPTH, - }) - } catch { - return undefined - } -} - -export async function fetchEntries(entryIds: readonly string[]): Promise { - const fetchedEntries = await Promise.all(entryIds.map(fetchEntry)) - - return fetchedEntries.filter((entry): entry is ContentEntry => entry !== undefined) -} diff --git a/implementations/react-web-sdk_nextjs/lib/optimization-server.ts b/implementations/react-web-sdk_nextjs/lib/optimization-server.ts deleted file mode 100644 index 377062b6..00000000 --- a/implementations/react-web-sdk_nextjs/lib/optimization-server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import ContentfulOptimization from '@contentful/optimization-node' -import { optimizationConfig } from './config' - -const sdk = new ContentfulOptimization({ - clientId: optimizationConfig.clientId, - environment: optimizationConfig.environment, - logLevel: 'debug', - api: optimizationConfig.api, - app: { - name: 'ContentfulOptimization SDK - Next.js Reference (Server)', - version: '0.1.0', - }, -}) - -export { sdk } diff --git a/implementations/react-web-sdk_nextjs/middleware.ts b/implementations/react-web-sdk_nextjs/middleware.ts deleted file mode 100644 index 3e37b251..00000000 --- a/implementations/react-web-sdk_nextjs/middleware.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { sdk } from '@/lib/optimization-server' -import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants' -import { type NextRequest, NextResponse } from 'next/server' - -export async function middleware(request: NextRequest): Promise { - const anonymousId = request.cookies.get(ANONYMOUS_ID_COOKIE)?.value - const profile = anonymousId ? { id: anonymousId } : undefined - - const url = new URL(request.url) - const data = await sdk.page({ - locale: request.headers.get('accept-language')?.split(',')[0] ?? 'en-US', - userAgent: request.headers.get('user-agent') ?? 'next-js-server', - page: { - path: url.pathname, - query: Object.fromEntries(url.searchParams), - referrer: request.headers.get('referer') ?? '', - search: url.search, - url: request.url, - }, - profile, - }) - - const response = NextResponse.next() - - if (data.profile.id) { - response.cookies.set(ANONYMOUS_ID_COOKIE, data.profile.id, { - path: '/', - sameSite: 'lax', - }) - } - - return response -} - -export const config = { - matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'], -} diff --git a/implementations/react-web-sdk_nextjs/next.config.ts b/implementations/react-web-sdk_nextjs/next.config.ts deleted file mode 100644 index 9333b88a..00000000 --- a/implementations/react-web-sdk_nextjs/next.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextConfig } from 'next' - -const nextConfig: NextConfig = { - serverExternalPackages: ['@contentful/optimization-node'], - env: { - PUBLIC_NINETAILED_CLIENT_ID: process.env.PUBLIC_NINETAILED_CLIENT_ID, - PUBLIC_NINETAILED_ENVIRONMENT: process.env.PUBLIC_NINETAILED_ENVIRONMENT, - PUBLIC_EXPERIENCE_API_BASE_URL: process.env.PUBLIC_EXPERIENCE_API_BASE_URL, - PUBLIC_INSIGHTS_API_BASE_URL: process.env.PUBLIC_INSIGHTS_API_BASE_URL, - PUBLIC_CONTENTFUL_TOKEN: process.env.PUBLIC_CONTENTFUL_TOKEN, - PUBLIC_CONTENTFUL_PREVIEW_TOKEN: process.env.PUBLIC_CONTENTFUL_PREVIEW_TOKEN, - PUBLIC_CONTENTFUL_ENVIRONMENT: process.env.PUBLIC_CONTENTFUL_ENVIRONMENT, - PUBLIC_CONTENTFUL_SPACE_ID: process.env.PUBLIC_CONTENTFUL_SPACE_ID, - PUBLIC_CONTENTFUL_CDA_HOST: process.env.PUBLIC_CONTENTFUL_CDA_HOST, - PUBLIC_CONTENTFUL_BASE_PATH: process.env.PUBLIC_CONTENTFUL_BASE_PATH, - PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL: process.env.PUBLIC_OPTIMIZATION_ENABLE_PREVIEW_PANEL, - }, -} - -export default nextConfig diff --git a/implementations/react-web-sdk_nextjs/package.json b/implementations/react-web-sdk_nextjs/package.json deleted file mode 100644 index cb4e8eb1..00000000 --- a/implementations/react-web-sdk_nextjs/package.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "name": "@implementation/react-web-sdk_nextjs", - "private": true, - "version": "0.0.0", - "description": "Reference implementation for Next.js (App Router) using @contentful/optimization-react-web and @contentful/optimization-node for SSR", - "license": "MIT", - "type": "module", - "scripts": { - "dev": "next dev --port 3000", - "build": "next build", - "start": "next start --port 3000", - "clean": "rimraf .next coverage playwright-report test-results .tsbuildinfo", - "preview": "pnpm serve:mocks && pnpm start", - "serve": "pnpm serve:mocks && pnpm serve:app", - "serve:app": "pnpm build && pm2 start --name nextjs-react-web-sdk-app \"pnpm start\"", - "serve:app:stop": "pm2 stop nextjs-react-web-sdk-app && pm2 delete nextjs-react-web-sdk-app", - "serve:mocks": "pm2 start --name nextjs-react-web-sdk-mocks \"pnpm --dir ../../lib/mocks serve\"", - "serve:mocks:stop": "pm2 stop nextjs-react-web-sdk-mocks && pm2 delete nextjs-react-web-sdk-mocks", - "serve:stop": "pnpm serve:app:stop && pnpm serve:mocks:stop", - "test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT", - "test:e2e:codegen": "playwright codegen", - "test:e2e:report": "playwright show-report", - "test:e2e:ui": "playwright test --ui", - "implementation:playwright:install": "playwright install", - "implementation:playwright:install-deps": "playwright install-deps", - "implementation:setup:e2e": "pnpm implementation:playwright:install && pnpm implementation:playwright:install-deps", - "test:unit": "echo \"No unit tests necessary\"", - "typecheck": "tsc --noEmit", - "lint": "eslint" - }, - "dependencies": { - "@contentful/optimization-node": "0.0.0", - "@contentful/optimization-react-web": "0.0.0", - "@contentful/optimization-web-preview-panel": "0.0.0", - "@contentful/rich-text-react-renderer": "16.1.6", - "@contentful/rich-text-types": "17.2.5", - "contentful": "11.10.5", - "next": "16.2.4", - "react": "19.2.5", - "react-dom": "19.2.5" - }, - "devDependencies": { - "@playwright/test": "1.58.2", - "@tailwindcss/postcss": "4.1.11", - "@types/node": "24.11.0", - "@types/react": "19.2.14", - "@types/react-dom": "19.2.3", - "dotenv": "17.3.1", - "eslint": "9.29.0", - "eslint-config-next": "16.2.4", - "pm2": "6.0.14", - "postcss": "8.5.6", - "rimraf": "6.1.3", - "tailwindcss": "4.1.11", - "typescript": "5.9.3" - }, - "pnpm": { - "overrides": { - "@contentful/optimization-api-client": "file:../../pkgs/contentful-optimization-api-client-0.0.0.tgz", - "@contentful/optimization-api-schemas": "file:../../pkgs/contentful-optimization-api-schemas-0.0.0.tgz", - "@contentful/optimization-core": "file:../../pkgs/contentful-optimization-core-0.0.0.tgz", - "@contentful/optimization-node": "file:../../pkgs/contentful-optimization-node-0.0.0.tgz", - "@contentful/optimization-web": "file:../../pkgs/contentful-optimization-web-0.0.0.tgz", - "@contentful/optimization-react-web": "file:../../pkgs/contentful-optimization-react-web-0.0.0.tgz", - "@contentful/optimization-web-preview-panel": "file:../../pkgs/contentful-optimization-web-preview-panel-0.0.0.tgz" - } - } -} diff --git a/implementations/react-web-sdk_nextjs/pnpm-workspace.yaml b/implementations/react-web-sdk_nextjs/pnpm-workspace.yaml deleted file mode 100644 index 581a9d5b..00000000 --- a/implementations/react-web-sdk_nextjs/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -ignoredBuiltDependencies: - - sharp - - unrs-resolver diff --git a/implementations/react-web-sdk_nextjs/postcss.config.mjs b/implementations/react-web-sdk_nextjs/postcss.config.mjs deleted file mode 100644 index 61e36849..00000000 --- a/implementations/react-web-sdk_nextjs/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/implementations/react-web-sdk_nextjs/tsconfig.json b/implementations/react-web-sdk_nextjs/tsconfig.json deleted file mode 100644 index 2cc12770..00000000 --- a/implementations/react-web-sdk_nextjs/tsconfig.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "ESNext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "react-jsx", - "incremental": true, - "forceConsistentCasingInFileNames": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - } - }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts", - "**/*.mts" - ], - "exclude": ["node_modules"] -} diff --git a/implementations/react-web-sdk_nextjs/types/contentful.ts b/implementations/react-web-sdk_nextjs/types/contentful.ts deleted file mode 100644 index 2efb9ad7..00000000 --- a/implementations/react-web-sdk_nextjs/types/contentful.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Document } from '@contentful/rich-text-types' -import type { Entry, EntryFieldTypes, EntrySkeletonType } from 'contentful' - -export interface ContentEntryFields { - text?: EntryFieldTypes.Text | EntryFieldTypes.RichText - nested?: EntryFieldTypes.Array> -} - -export type ContentEntrySkeleton = EntrySkeletonType -export type ContentEntry = Entry -export type RichTextDocument = Document diff --git a/package.json b/package.json index c2038f13..fa142cd7 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "implementation:run": "tsx ./scripts/run-implementation-script.ts", "implementation:react-web-sdk": "pnpm run implementation:run -- react-web-sdk", "implementation:web-sdk_react": "pnpm run implementation:run -- web-sdk_react", - "implementation:react-web-sdk_nextjs": "pnpm run implementation:run -- react-web-sdk_nextjs", "implementation:react-web-sdk+node-sdk_nextjs": "pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs", "implementation:web-sdk": "pnpm run implementation:run -- web-sdk", "implementation:lint": "eslint implementations --cache --cache-location .cache/eslint/implementations", From 531eada6ecdce8536763794f007a5866c7aacf2a Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Tue, 5 May 2026 14:38:00 +0200 Subject: [PATCH 11/17] chore: remove spike-nextjs-support.md Superseded by the actual reference implementations. --- documentation/spike-nextjs-support.md | 152 -------------------------- 1 file changed, 152 deletions(-) delete mode 100644 documentation/spike-nextjs-support.md diff --git a/documentation/spike-nextjs-support.md b/documentation/spike-nextjs-support.md deleted file mode 100644 index 6eb379ed..00000000 --- a/documentation/spike-nextjs-support.md +++ /dev/null @@ -1,152 +0,0 @@ -# [SPIKE] Next.js Support — React Web SDK / Next.js Implementation - -## Document Status - -Draft - -## Document Author / Owner - -Lotfi Arif - -## Contributor(s) - -(none yet) - -## PRD Links - -- PRD: Optimization React Web SDK - -## Reference Links - -- Confluence Project Page -- GitHub Repo - ---- - -## 1. Purpose - -Determine what it takes to fully support a Next.js application (SSR, SSG, bonus ESR) with the -Optimization SDK. Evaluate whether Node SDK + React Web SDK composition is sufficient or whether a -dedicated Next.js SDK is warranted. Produce a working reference implementation. - -## 2. Approaches - -### Approach A: Node SDK + React Web SDK (Composition) - -Customer manually wires Node SDK on the server and React Web SDK on the client — similar to the -existing `node-sdk+web-sdk` reference implementation but in a Next.js context. - -- Server: Node SDK calls Experience API with `preflight: true`, passes data as props -- Client: React Web SDK hydrates with server-provided data, takes over from there -- Cookie: `ctfl-opt-aid` shared between server and client for profile continuity - -| Dimension | Assessment | -| ----------------------- | ------------------------------------------------------------------------------------------------ | -| Complexity for customer | High — must manage cookies, Node SDK setup, `preflight`, data passing, `'use client'` boundaries | -| Maintenance for us | Low — no new package | -| Consistency | Low — every customer wires it differently | -| Error surface | High — easy to forget `preflight`, double-count events, or leak state | - -### Approach B: Dedicated Next.js SDK (`@contentful/optimization-next`) - -A thin package providing the standard glue code, composed from Node SDK + React Web SDK internals. -Would provide things like: - -- `getOptimizationData()` — async helper for Server Components / `getServerSideProps` -- `` / `` — server/client boundary pair -- `optimizationMiddleware()` — cookie management, optionally edge variant selection -- `withOptimization(gssp)` — Pages Router HOC - -| Dimension | Assessment | -| ----------------------- | ------------------------------------------------------------- | -| Complexity for customer | Low — single provider, familiar Next.js patterns | -| Maintenance for us | Medium — new package, but thin wrapper over existing SDKs | -| Consistency | High — every customer uses the same pattern | -| Error surface | Low — `preflight`, cookies, and boundaries handled internally | - -### Recommendation - -**Start with Approach A as a reference implementation** to prove the architecture works. If that -reveals significant boilerplate (which we expect it will), extract it into a dedicated Next.js SDK -(Approach B). The dedicated SDK is only warranted if there is standard glue code that most/all -integrations would need. - -## 3. What We Need to Know - -### Rendering Mode Feasibility - -**SSR — Feasible.** Node SDK (`CoreStateless`) is fully request-scoped and platform-agnostic. The -pattern is: server fetches optimization data with `preflight: true`, serializes it into page props, -client hydrates with that data. The existing `node-sdk+web-sdk` implementation already proves this -flow with Express; Next.js just changes the transport (props instead of EJS templates). - -**SSG — Partially feasible.** No real user at build time, so only baseline content can be resolved. -Personalization happens client-side after hydration (causes a content shift). ISR has the same -limitation unless the regeneration is request-triggered (effectively SSR). - -**ESR — Research-only.** `CoreStateless` is likely Edge-compatible (no Node-specific APIs, just -global `fetch`), but needs verification. The value would be running variant selection in Next.js -Middleware at the edge. Risk: cold start + API round-trip may negate latency benefits. - -### Key Technical Pieces - -1. **Dehydration/rehydration:** Server fetches `OptimizationData`, passes it to a client provider - that seeds the Web SDK state without making a fresh API call. In App Router this is natural - (Server Component props serialize automatically). In Pages Router, `getServerSideProps` returns - it as page props. - -2. **`'use client'` / `'use server'` boundaries:** The Web SDK and all React hooks must remain in - Client Components. Server-side data fetching (Node SDK) lives in Server Components or - `getServerSideProps`. - -3. **Cookie continuity:** `ctfl-opt-aid` cookie bridges server and client identity. Server reads it - from the request, passes it to Node SDK; Web SDK picks it up on hydration. - -4. **`preflight: true`:** Already exists in the API client. Server calls should use preflight so the - profile state is previewed but not persisted — client persists on hydration. - -### What Already Exists - -- `CoreStateless` / Node SDK — fully server-safe, request-scoped, no browser APIs -- `preflight` flag on Experience API client -- `ctfl-opt-aid` cookie constant exported from both Node and Web SDKs -- Next.js router adapters (`./router/next-app`, `./router/next-pages`) in React Web SDK -- `node-sdk+web-sdk` reference implementation demonstrating the SSR-to-client handoff pattern -- `typeof window === 'undefined'` guards throughout the Web SDK - -### What Is Missing - -- A way for `OptimizationProvider` to accept pre-fetched server data (currently always creates a - fresh Web SDK instance that calls the API) -- Server-side entry resolution without signals (the resolution logic in `CoreBase` is pure, but - `OptimizedEntry` reads from signals) -- Next.js-specific cookie reading (via `next/headers` `cookies()`) -- A reference implementation proving the full Next.js flow end-to-end - -## 4. Open Questions - -1. **App Router, Pages Router, or both?** Architecture differs significantly. Recommend App Router - primary, Pages Router secondary. - -2. **Should `preflight: true` be the default for server-side calls?** Probably yes (server previews, - client persists), but some customers may want server-side persistence for bots/crawlers. - -3. **Is Edge Runtime compatibility a hard requirement or nice-to-have?** Determines whether we need - to verify/fix transitive dependencies. - -4. **Is there appetite for Next.js Middleware?** Enables edge-based variant selection and A/B - routing but adds complexity. - -5. **What is the anonymous ID lifecycle for SSG?** No user at build time — always render baseline - and defer personalization to client? - -6. **How should the reference implementation handle Contentful entries?** Use `contentful.js`, or - mock/fixture approach like existing implementations? - -## 5. Next Steps - -1. Build a Next.js App Router reference implementation (`implementations/nextjs-react-web-sdk/`) - using Approach A (Node SDK + React Web SDK composition) -2. Evaluate how much boilerplate the reference implementation requires -3. Decide whether to extract into a dedicated Next.js SDK (Approach B) -4. Bonus: verify `CoreStateless` Edge Runtime compatibility From 35ae83a8506d7cb2230ba1f9166f4233a16fc6c4 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Tue, 5 May 2026 14:41:24 +0200 Subject: [PATCH 12/17] chore: remove session notes file --- session-ses_22be.md | 447 -------------------------------------------- 1 file changed, 447 deletions(-) delete mode 100644 session-ses_22be.md diff --git a/session-ses_22be.md b/session-ses_22be.md deleted file mode 100644 index 25bf0464..00000000 --- a/session-ses_22be.md +++ /dev/null @@ -1,447 +0,0 @@ -# SSR vs SSG in Next.js with edge personalization - -**Session ID:** ses_22be2983affe0ibj2AEPG9G17s -**Created:** 4/28/2026, 2:43:10 PM -**Updated:** 4/28/2026, 5:03:35 PM - ---- - -## User - -I need to discuss some things and it is best to go one by own here. -the first thing that i need to learn about is SSR in nextjs and what are its use cases and is it possible to always have pure SSR components? I would like to know the same case for SSG in nextJs as well -There was previously a module called Middleware (now proxy.js) that helps execute HTTP requests before they hit Next.js servers. Middleware functions are deployed at the cloud edge (e.g., CDNs or Edge workers). -I recommend including this in the reference implementation to show how the customer can use SSG while also serving personalized content by proxying requests to our Optimization API to fetch variants before NextJS renders the components, which could be useful. -I got the idea to research this from Sebastian, who mentioned that we had a customer (Home Depot) with a slow, static React frontend (cached) that did not work well with our personalization. That caused flicker on first paint, which was not ideal for them. The solution they came up with was to use an edge worker that receives the request and knows what content to serve before first paint. -Looking through the documentation, I can see that NextJS recommends avoiding its use unless there are no other options available. but would serve personalization with SSG and proxy actually work in theory if we are using something like the experience API that we have? - ---- - -## Assistant (Debug · nt-opus · 27.4s) - - - -This is a great architectural discussion. Let me address each point: - -## SSR in Next.js - -**Server-Side Rendering** generates HTML on every request at the server. Use cases: -- Pages needing real-time data (dashboards, user profiles) -- Personalized content per request -- SEO-critical pages with dynamic data - -**Can you have pure SSR components?** Yes, in the App Router (Next.js 13+), all components are Server Components by default. You only need `"use client"` when you need browser APIs, event handlers, or React state/effects. In Pages Router, `getServerSideProps` achieves SSR per-page. - -## SSG in Next.js - -**Static Site Generation** pre-renders pages at build time. Use cases: -- Marketing pages, blogs, docs -- Product listing pages with infrequent changes -- Any content that's the same for all users - -**Can you have pure SSG?** Yes, but the limitation is obvious: **SSG serves the same HTML to everyone**. There's no per-user personalization at render time. You can use ISR (Incremental Static Revalidation) to periodically rebuild, but it's still not per-user. - -## The Edge Middleware / Proxy Approach for SSG + Personalization - -This is the key architectural question, and **yes, it works in theory**. Here's how: - -1. **User request hits the edge** (Cloudflare Worker, Vercel Edge Middleware, etc.) -2. **Edge calls the Experience/Optimization API** with the user's context (cookies, geo, device, etc.) to resolve which variant to serve -3. **Edge rewrites/proxies the request** to the correct pre-built static page variant (e.g., `/homepage?variant=B` -> serves `/homepage-b.html`) -4. **Next.js serves the pre-rendered static page** -- zero flicker, fully cached, fast TTFB - -### Why this solves the Home Depot problem - -The flicker happened because: static HTML renders -> client JS hydrates -> JS calls personalization API -> DOM updates with new content (flash). With edge resolution, the correct content is served **before first paint**. - -### Practical considerations - -- **You need to pre-build all variants as static pages** (SSG with dynamic routes or params), which means a finite, known set of variants -- **The Experience API call adds latency at the edge**, but edge functions are typically 1-50ms from the user, so the round-trip to the Optimization API is the bottleneck. If the API is fast (~50-100ms), total overhead is acceptable -- **Caching at the edge** can help -- you can cache variant resolution by segment/audience key, not just per-user -- **Cookie-based routing** is common: edge sets a cookie with the variant, subsequent requests skip the API call entirely - -### Why Next.js discourages Middleware overuse - -Next.js warns against heavy Middleware because: -- It runs on **every matched request** (including static assets if not filtered) -- It has a limited runtime (Edge Runtime, no Node.js APIs) -- Complex logic there is harder to debug/test -- They'd prefer you use Server Components or Route Handlers - -**But personalization variant resolution is exactly the right use case for it** -- it's a lightweight, stateless decision that must happen before rendering. - -## Recommendation for the Reference Implementation - -Including a `proxy.js` / Middleware example that: - -1. Intercepts the request at the edge -2. Calls the Optimization/Experience API to resolve the variant -3. Rewrites to the correct pre-rendered SSG page - -...would be a strong demonstration. It shows customers they can have **both** the performance of SSG **and** personalized content without flicker. This is particularly compelling for large retail customers like Home Depot where TTFP and CLS are critical metrics. - -The main trade-off is that variant count must be bounded and pages pre-built, but for most A/B testing and personalization scenarios (hero banners, CTAs, layouts), that's entirely feasible. - -Want me to look at the current reference implementation in this repo to see how this could be integrated? - ---- - -## User - -with the practical considerations, we would need to build all the variants for this right? with the way that we do it now i am sure there is a contentful component that we can use to feed in the entry IDs that we get from the experience API which renders those entries to a degree right? since its coming from contentful anyways. - ---- - -## Assistant (Debug · nt-opus · 13.1s) - - - -You're right, and that's an important distinction that simplifies things significantly. - -## You don't need to pre-build every variant - -The flow would be: - -1. **Edge receives request** -> calls Experience API -> gets back the winning variant (which is a Contentful entry ID) -2. **Edge passes that entry ID** to Next.js (via header, cookie, or rewrite param) -3. **Next.js page (SSG with ISR or SSR)** receives the entry ID and fetches that entry from Contentful at render time -4. **The Contentful component renders whatever entry it gets** -- it doesn't care which variant it is - -So the page template is generic -- it's just "render this Contentful entry." The personalization decision (which entry) is made at the edge, but the actual content comes from Contentful's API as it always does. - -### This means: - -- **No need to pre-build N variants per page** -- you have one page/template that accepts an entry ID -- **Content authors can add/remove variants in Contentful** without triggering rebuilds -- **The Contentful SDK components already handle rendering entries** -- you just need to pass the right entry ID to them -- **SSG still works** for the page shell/layout, with the personalized content block being the dynamic part resolved at the edge - -### The realistic flow becomes: - -``` -User -> Edge/Proxy -> Experience API -> "serve entry xyz123" - -> Next.js (cached template) + Contentful fetch(xyz123) -> Full HTML -``` - -The only latency cost is the Experience API call at the edge + the Contentful fetch for that specific entry. And if you're already using Contentful's CDN, that fetch is fast. - -This is much more practical than pre-building all variant combinations. The page is essentially a shell that says "render whatever Contentful entry I'm told to," and the edge decides which entry that is. - ---- - -## User - -lets not start giving recommendations but its more me trying to learn about SSR and SSG and how I can implement it -how the HTML looks like in SSR if i were to implement react web sdk with the node SDK inside of a nextjs app -what are the benefits and some detailed technical components of how it works under the hood and how i can be an expert on it. - ---- - -## Assistant (Debug · nt-opus · 49.5s) - - - -## SSR Under the Hood in Next.js - -### What actually happens on a request - -``` -Browser sends GET /page - ↓ -Next.js server receives request - ↓ -Server executes your React component tree as JavaScript - ↓ -React's renderToString() / renderToReadableStream() converts -the component tree into an HTML string - ↓ -Server sends complete HTML to browser - ↓ -Browser paints immediately (no blank screen) - ↓ -React JS bundle downloads - ↓ -React "hydrates" — attaches event listeners to existing DOM -``` - -### The HTML output - -When the server renders a page using SSR, the browser receives **fully formed HTML**. If you had a component like: - -```jsx -// Server Component in App Router (SSR by default) -export default async function Page() { - // This runs on the server — Node SDK call - const experience = await fetchExperience(experienceId); - - return ( -
-

Welcome back, John

- -

Special offer just for you

-
- ); -} -``` - -The browser receives something like: - -```html - - - - - - - -
-
-

Welcome back, John

- -

Special offer just for you

-
-
- - - -``` - -Key things to notice: -- **The content is in the HTML** — not injected later by JS -- **`__NEXT_DATA__`** is a JSON blob that Next.js injects so React on the client can hydrate without re-fetching the same data -- **Scripts are `defer`** — they load after HTML parsing, so paint happens first - -### Hydration — the critical concept - -Once the HTML is painted, React needs to "take over" the DOM to make it interactive. This is hydration: - -1. React downloads and executes in the browser -2. It rebuilds the virtual DOM tree in memory -3. It walks the existing server-rendered DOM and **attaches event listeners** to matching nodes -4. It does NOT re-render — it assumes the server HTML matches what React would produce client-side - -**If there's a mismatch** (server rendered "Welcome back, John" but client-side state says "Guest"), React throws a hydration error and may re-render, causing flicker. This is exactly what happens with client-side personalization on static pages — the server HTML doesn't match what the client wants to show. - -### Server Components vs Client Components (App Router) - -``` -Server Component (default) -├── Runs ONLY on the server -├── Can use async/await, Node APIs, Node SDK -├── Output is HTML + React Server Component Payload (RSC Payload) -├── NEVER ships JS to the browser -└── Cannot use useState, useEffect, onClick, etc. - -Client Component ("use client") -├── Renders on server first (for initial HTML) AND on client -├── Ships JS bundle to browser for hydration -├── Can use hooks, browser APIs, event handlers -└── React Web SDK components would be this if they need interactivity -``` - -The RSC Payload is a binary format that describes the server component tree. It's streamed alongside HTML and lets React on the client understand the component boundaries without needing the server component code. - -### How Node SDK + React Web SDK would fit - -``` -Server Component (Node SDK) -│ -│ Runs on server: fetches experiences, resolves variants, -│ gets Contentful entries — all in Node.js -│ -├── Passes resolved data as props to: -│ -│ ├── Server Components (no interactivity needed) -│ │ → Pure HTML, zero JS shipped -│ │ → Contentful rich text, images, static content -│ │ -│ └── Client Components ("use client", React Web SDK) -│ → Interactive elements (carousels, forms, analytics) -│ → Hydrates on client with event listeners -│ → The Web SDK JS ships in the bundle -``` - -In practice: - -```jsx -// app/page.tsx — Server Component -import { getExperience } from '@ninetailed/node-sdk'; -import { HeroBanner } from './HeroBanner'; // client component - -export default async function Page() { - const experience = await getExperience({ - experienceId: 'xxx', - profile: getProfileFromCookies(), - }); - - const entry = await fetchContentfulEntry(experience.variantId); - - // Data resolved on server, passed as serialized props - return ; -} -``` - -```jsx -// app/HeroBanner.tsx — Client Component -"use client"; - -export function HeroBanner({ content }) { - // This component hydrates on the client - // content was resolved on the server — no flicker - return ( -
-

{content.fields.headline}

-
- ); -} -``` - -### What the network looks like - -**First request (SSR):** -``` -GET /page → 200 HTML (~50-200ms TTFB) -GET /_next/static/chunks/main.js → JS bundle (cached) -GET /_next/static/chunks/page.js → Page-specific JS (cached) -``` - -**The HTML response is complete.** The user sees content immediately. JS loads in background for interactivity. - -### Benefits — the technical specifics - -**1. TTFB to First Contentful Paint** -- CSR: browser gets empty `
` → downloads JS → executes → fetches data → renders. Could be 2-4s on slow connections. -- SSR: browser gets full HTML → paints. TTFB is slower (server has to render) but FCP is much faster because there's no JS execution in the critical path. - -**2. No layout shift from personalization** -- The server already resolved the variant. The HTML contains the correct content. There's no "swap" after hydration. -- CLS (Cumulative Layout Shift) stays at 0 for personalized content. - -**3. SEO** -- Crawlers get complete HTML. Googlebot can execute JS, but other crawlers can't, and even Google prefers server-rendered content. - -**4. Streaming SSR (React 18+)** -- Next.js App Router uses `renderToReadableStream()` which streams HTML chunks as they resolve -- If one component is slow (e.g., waiting on Experience API), the rest of the page streams first, and a `` fallback shows until it resolves -- The browser starts painting before the full response is complete - -```jsx -export default async function Page() { - return ( -
-
{/* streams immediately */} - }> - {/* streams when Experience API responds */} - -
{/* streams immediately */} -
- ); -} -``` - -**5. The `__NEXT_DATA__` / RSC Payload contract** -- In Pages Router: server serializes props into `__NEXT_DATA__` JSON, client React reads it during hydration so it doesn't re-fetch -- In App Router: the RSC Payload is a streaming binary format that describes the server component output, more efficient than JSON serialization -- Either way, data fetched on the server is passed to the client without a second network request - -### What you need to understand to be expert-level - -1. **The rendering pipeline**: request → server render → HTML stream → client JS download → hydration → interactive -2. **Hydration mismatches**: the #1 source of bugs. Server and client must produce identical DOM for the initial render -3. **Serialization boundary**: data passing from server to client components must be JSON-serializable (no functions, no classes, no circular refs) -4. **The cost of "use client"**: every client component's JS ships to the browser. Pushing the "use client" boundary as deep as possible in the tree keeps bundle size small -5. **Caching layers**: Next.js has multiple caches (Full Route Cache, Data Cache, Router Cache) that interact with SSR. Understanding when a page is dynamically rendered vs statically cached is critical -6. **`dynamic = 'force-dynamic'` vs `revalidate`**: these control whether Next.js treats a route as SSR (every request) or ISR (cached, revalidated periodically) - ---- - -## User - -i have this written about SSG -is ther esomething i can write about SSR that is a similar style? -NextJS Personalization with SSG -There was previously a module called Middleware (now proxy.js) that helps execute HTTP requests before they hit Next.js servers. Middleware functions are deployed at the cloud edge (e.g., CDNs or Edge workers). -I recommend including this in the reference implementation to show how the customer can use SSG while also serving personalized content by proxying requests to our Optimization API to fetch variants before NextJS renders the components, which could be useful. -I got the idea to research this from Sebastian, who mentioned that we had a customer (Home Depot) with a slow, static React frontend (cached) that did not work well with our personalization. That caused a flicker on the first paint, which was not ideal for them. The solution they came up with was to use an edge worker that receives the request and knows what content to serve before first paint. -Looking through the documentation, I can see that NextJS recommends avoiding its use unless there are no other options available. - [Image 1] - ---- - -## Assistant (Debug · nt-opus · 19.5s) - - - -Here's an SSR counterpart in the same style: - ---- - -**NextJS Personalization with SSR** - -With SSR, personalization happens on the server at request time — no edge proxy or middleware needed. When a request comes in, the server executes the Node SDK to call the Experience API, resolves the winning variant, fetches the corresponding Contentful entry, and returns fully rendered HTML with the personalized content already in place. The browser paints the correct variant on first load with zero flicker and zero layout shift. - -This is the most straightforward approach to personalization in Next.js because the rendering pipeline naturally supports it — there's no architectural workaround required. The server already has access to request context (cookies, headers, geo) and can pass that to the Experience API before React ever touches the component tree. The resolved content flows down as props, and the HTML the browser receives is complete and correct. - -The trade-off compared to SSG is latency. Every request hits the server, which must call the Experience API and Contentful before responding. There's no cached static page to fall back on. For high-traffic pages where the variant set is small and doesn't change per-user, SSG with edge resolution may perform better. But for pages where personalization is highly dynamic (per-user profile, real-time segmentation, many variant combinations), SSR is simpler to implement and avoids the complexity of pre-building variants or maintaining an edge proxy layer. - -In the App Router, this is the default behavior — components are Server Components and can call the Node SDK directly with async/await. In the Pages Router, `getServerSideProps` achieves the same thing. Either way, the client never sees a loading state or content swap for the personalized block. - -I'd recommend including this in the reference implementation alongside the SSG + proxy approach so customers can compare both patterns and choose based on their traffic profile and performance requirements. - ---- - -Want me to adjust the tone or add/remove anything? - ---- - -## User - -but with calling the experience API it would not use the SDK which is what we would want to use to have further personalization with useing the SDK methods like consent and everything not just the personalization right? -so what is a better way of doing this in a full way? with supporting SSR on nextJs - ---- - -## Assistant (Debug · nt-opus · 14.5s) - - - -You're right. Calling the Experience API directly only gives you variant resolution — you lose the full SDK surface (consent management, event tracking, profile merging, `identify`, `track`, `page`, etc.). - -## The full approach for SSR with the SDK - -You need **both** SDKs working together: - -**Server side (Node SDK)** handles the initial render: -- Resolves experiences/variants at request time using profile context -- Fetches the correct Contentful entries -- Returns fully rendered HTML — no flicker - -**Client side (React Web SDK)** hydrates and takes over: -- Manages the ongoing user session (profile, consent, traits) -- Tracks events (`track`, `page`) -- Handles `identify` for known users -- Manages consent state -- Listens for profile changes and can re-evaluate experiences client-side if the profile updates mid-session (e.g., user logs in, consent changes, new traits added) - -The flow looks like: - -``` -Request → Server (Node SDK resolves variant, renders HTML) - → Browser paints correct content immediately - → React hydrates - → Web SDK initializes with the same profile state - → Web SDK handles all ongoing interactions: - consent, track, identify, page, etc. - → If profile changes mid-session, Web SDK - re-evaluates and can swap variants client-side -``` - -The key is that the **Node SDK handles the first render** (solving flicker) and the **Web SDK handles everything after** (solving the full personalization lifecycle). The server passes the initial profile/variant state to the client so the Web SDK doesn't start from scratch — it picks up where the Node SDK left off, avoiding a hydration mismatch. - -This is the pattern worth documenting in the reference implementation: not just "how to get the right variant on first paint" but "how to run the full personalization lifecycle with SSR as the foundation." - ---- - From e0a9c31090383fdfca283c9a81739f7622f297c8 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Tue, 5 May 2026 16:34:41 +0200 Subject: [PATCH 13/17] fix: exclude Next.js implementations from root ESLint The root ESLint (v10) is incompatible with eslint-plugin-react pulled by eslint-config-next. The Next.js implementations have their own local eslint.config.mjs and are linted via their own setup. --- eslint.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eslint.config.ts b/eslint.config.ts index 37c07080..99d3b8fe 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -31,6 +31,8 @@ export default defineConfig( 'docs/media/**', '**/ios/**', '**/node_modules', + 'implementations/react-web-sdk+node-sdk_nextjs/**', + 'implementations/react-web-sdk_nextjs/**', ], }, js.configs.recommended, From f1c2d4256487035cb9a6ee3c33f11ea37cdb6683 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Tue, 5 May 2026 16:43:06 +0200 Subject: [PATCH 14/17] chore: rename directory to a clearer name to the reference implementation --- eslint.config.ts | 2 -- .../.env.example | 0 .../.gitignore | 0 .../.npmrc | 0 .../AGENTS.md | 12 ++++++------ .../README.md | 12 ++++++------ .../app/globals.css | 0 .../app/layout.tsx | 0 .../app/page.tsx | 0 .../components/ClientProviderWrapper.tsx | 0 .../components/InteractiveControls.tsx | 0 .../config/entries.ts | 0 .../eslint.config.mjs | 0 .../lib/config.ts | 0 .../lib/contentful-client.ts | 0 .../lib/optimization-server.ts | 0 .../middleware.ts | 0 .../next.config.ts | 0 .../package.json | 2 +- .../pnpm-workspace.yaml | 0 .../postcss.config.mjs | 0 .../tsconfig.json | 0 .../types/contentful.ts | 0 package.json | 2 +- 24 files changed, 14 insertions(+), 16 deletions(-) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/.env.example (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/.gitignore (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/.npmrc (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/AGENTS.md (75%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/README.md (57%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/app/globals.css (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/app/layout.tsx (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/app/page.tsx (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/components/ClientProviderWrapper.tsx (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/components/InteractiveControls.tsx (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/config/entries.ts (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/eslint.config.mjs (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/lib/config.ts (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/lib/contentful-client.ts (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/lib/optimization-server.ts (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/middleware.ts (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/next.config.ts (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/package.json (98%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/pnpm-workspace.yaml (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/postcss.config.mjs (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/tsconfig.json (100%) rename implementations/{react-web-sdk+node-sdk_nextjs => react-web-sdk+node-sdk_nextjs-ssr}/types/contentful.ts (100%) diff --git a/eslint.config.ts b/eslint.config.ts index 99d3b8fe..37c07080 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -31,8 +31,6 @@ export default defineConfig( 'docs/media/**', '**/ios/**', '**/node_modules', - 'implementations/react-web-sdk+node-sdk_nextjs/**', - 'implementations/react-web-sdk_nextjs/**', ], }, js.configs.recommended, diff --git a/implementations/react-web-sdk+node-sdk_nextjs/.env.example b/implementations/react-web-sdk+node-sdk_nextjs-ssr/.env.example similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/.env.example rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/.env.example diff --git a/implementations/react-web-sdk+node-sdk_nextjs/.gitignore b/implementations/react-web-sdk+node-sdk_nextjs-ssr/.gitignore similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/.gitignore rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/.gitignore diff --git a/implementations/react-web-sdk+node-sdk_nextjs/.npmrc b/implementations/react-web-sdk+node-sdk_nextjs-ssr/.npmrc similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/.npmrc rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/.npmrc diff --git a/implementations/react-web-sdk+node-sdk_nextjs/AGENTS.md b/implementations/react-web-sdk+node-sdk_nextjs-ssr/AGENTS.md similarity index 75% rename from implementations/react-web-sdk+node-sdk_nextjs/AGENTS.md rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/AGENTS.md index f8ebb6cf..436a41b1 100644 --- a/implementations/react-web-sdk+node-sdk_nextjs/AGENTS.md +++ b/implementations/react-web-sdk+node-sdk_nextjs-ssr/AGENTS.md @@ -32,12 +32,12 @@ This represents a customer setup where: ## Commands -- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs implementation:install` -- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs typecheck` -- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs build` -- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs dev` -- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs serve` -- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs serve:stop` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr implementation:install` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr typecheck` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr build` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr dev` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr serve` +- `pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr serve:stop` ## Usually Validate diff --git a/implementations/react-web-sdk+node-sdk_nextjs/README.md b/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md similarity index 57% rename from implementations/react-web-sdk+node-sdk_nextjs/README.md rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md index 2cfa8445..1f7c39d1 100644 --- a/implementations/react-web-sdk+node-sdk_nextjs/README.md +++ b/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md @@ -1,8 +1,8 @@ # Next.js SSR Hybrid Reference Implementation -`react-web-sdk+node-sdk_nextjs` — Next.js App Router reference using `@contentful/optimization-node` -for server-side entry resolution and `@contentful/optimization-react-web` for client-side event -tracking and interactive controls. +`react-web-sdk+node-sdk_nextjs-ssr` — Next.js App Router reference using +`@contentful/optimization-node` for server-side entry resolution and +`@contentful/optimization-react-web` for client-side event tracking and interactive controls. ## Architecture @@ -24,12 +24,12 @@ Browser ```bash pnpm build:pkgs -pnpm implementation:run -- react-web-sdk+node-sdk_nextjs implementation:install -cp implementations/react-web-sdk+node-sdk_nextjs/.env.example implementations/react-web-sdk+node-sdk_nextjs/.env +pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr implementation:install +cp implementations/react-web-sdk+node-sdk_nextjs-ssr/.env.example implementations/react-web-sdk+node-sdk_nextjs-ssr/.env ``` ## Development ```bash -pnpm implementation:run -- react-web-sdk+node-sdk_nextjs dev +pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr dev ``` diff --git a/implementations/react-web-sdk+node-sdk_nextjs/app/globals.css b/implementations/react-web-sdk+node-sdk_nextjs-ssr/app/globals.css similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/app/globals.css rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/app/globals.css diff --git a/implementations/react-web-sdk+node-sdk_nextjs/app/layout.tsx b/implementations/react-web-sdk+node-sdk_nextjs-ssr/app/layout.tsx similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/app/layout.tsx rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/app/layout.tsx diff --git a/implementations/react-web-sdk+node-sdk_nextjs/app/page.tsx b/implementations/react-web-sdk+node-sdk_nextjs-ssr/app/page.tsx similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/app/page.tsx rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/app/page.tsx diff --git a/implementations/react-web-sdk+node-sdk_nextjs/components/ClientProviderWrapper.tsx b/implementations/react-web-sdk+node-sdk_nextjs-ssr/components/ClientProviderWrapper.tsx similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/components/ClientProviderWrapper.tsx rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/components/ClientProviderWrapper.tsx diff --git a/implementations/react-web-sdk+node-sdk_nextjs/components/InteractiveControls.tsx b/implementations/react-web-sdk+node-sdk_nextjs-ssr/components/InteractiveControls.tsx similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/components/InteractiveControls.tsx rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/components/InteractiveControls.tsx diff --git a/implementations/react-web-sdk+node-sdk_nextjs/config/entries.ts b/implementations/react-web-sdk+node-sdk_nextjs-ssr/config/entries.ts similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/config/entries.ts rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/config/entries.ts diff --git a/implementations/react-web-sdk+node-sdk_nextjs/eslint.config.mjs b/implementations/react-web-sdk+node-sdk_nextjs-ssr/eslint.config.mjs similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/eslint.config.mjs rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/eslint.config.mjs diff --git a/implementations/react-web-sdk+node-sdk_nextjs/lib/config.ts b/implementations/react-web-sdk+node-sdk_nextjs-ssr/lib/config.ts similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/lib/config.ts rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/lib/config.ts diff --git a/implementations/react-web-sdk+node-sdk_nextjs/lib/contentful-client.ts b/implementations/react-web-sdk+node-sdk_nextjs-ssr/lib/contentful-client.ts similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/lib/contentful-client.ts rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/lib/contentful-client.ts diff --git a/implementations/react-web-sdk+node-sdk_nextjs/lib/optimization-server.ts b/implementations/react-web-sdk+node-sdk_nextjs-ssr/lib/optimization-server.ts similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/lib/optimization-server.ts rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/lib/optimization-server.ts diff --git a/implementations/react-web-sdk+node-sdk_nextjs/middleware.ts b/implementations/react-web-sdk+node-sdk_nextjs-ssr/middleware.ts similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/middleware.ts rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/middleware.ts diff --git a/implementations/react-web-sdk+node-sdk_nextjs/next.config.ts b/implementations/react-web-sdk+node-sdk_nextjs-ssr/next.config.ts similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/next.config.ts rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/next.config.ts diff --git a/implementations/react-web-sdk+node-sdk_nextjs/package.json b/implementations/react-web-sdk+node-sdk_nextjs-ssr/package.json similarity index 98% rename from implementations/react-web-sdk+node-sdk_nextjs/package.json rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/package.json index bebfc4eb..9dbea176 100644 --- a/implementations/react-web-sdk+node-sdk_nextjs/package.json +++ b/implementations/react-web-sdk+node-sdk_nextjs-ssr/package.json @@ -1,5 +1,5 @@ { - "name": "@implementation/react-web-sdk+node-sdk_nextjs", + "name": "@implementation/react-web-sdk+node-sdk_nextjs-ssr", "private": true, "version": "0.0.0", "description": "Reference implementation for Next.js (App Router) using @contentful/optimization-node for SSR entry resolution and @contentful/optimization-react-web for client-side event tracking", diff --git a/implementations/react-web-sdk+node-sdk_nextjs/pnpm-workspace.yaml b/implementations/react-web-sdk+node-sdk_nextjs-ssr/pnpm-workspace.yaml similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/pnpm-workspace.yaml rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/pnpm-workspace.yaml diff --git a/implementations/react-web-sdk+node-sdk_nextjs/postcss.config.mjs b/implementations/react-web-sdk+node-sdk_nextjs-ssr/postcss.config.mjs similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/postcss.config.mjs rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/postcss.config.mjs diff --git a/implementations/react-web-sdk+node-sdk_nextjs/tsconfig.json b/implementations/react-web-sdk+node-sdk_nextjs-ssr/tsconfig.json similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/tsconfig.json rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/tsconfig.json diff --git a/implementations/react-web-sdk+node-sdk_nextjs/types/contentful.ts b/implementations/react-web-sdk+node-sdk_nextjs-ssr/types/contentful.ts similarity index 100% rename from implementations/react-web-sdk+node-sdk_nextjs/types/contentful.ts rename to implementations/react-web-sdk+node-sdk_nextjs-ssr/types/contentful.ts diff --git a/package.json b/package.json index fa142cd7..406fef96 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "implementation:run": "tsx ./scripts/run-implementation-script.ts", "implementation:react-web-sdk": "pnpm run implementation:run -- react-web-sdk", "implementation:web-sdk_react": "pnpm run implementation:run -- web-sdk_react", - "implementation:react-web-sdk+node-sdk_nextjs": "pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs", + "implementation:react-web-sdk+node-sdk_nextjs-ssr": "pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs-ssr", "implementation:web-sdk": "pnpm run implementation:run -- web-sdk", "implementation:lint": "eslint implementations --cache --cache-location .cache/eslint/implementations", "implementation:lint:fix": "eslint implementations --fix", From 951663e0b39dddb3011e7339c1bd2204c2945bef Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Wed, 6 May 2026 14:41:33 +0200 Subject: [PATCH 15/17] fix: ignore nextJs project linting due to eslint-config-next bug --- .../react-web-sdk+node-sdk_nextjs-ssr/eslint.config.mjs | 9 +++++++++ package.json | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/implementations/react-web-sdk+node-sdk_nextjs-ssr/eslint.config.mjs b/implementations/react-web-sdk+node-sdk_nextjs-ssr/eslint.config.mjs index 7c81a1b4..694bcdc6 100644 --- a/implementations/react-web-sdk+node-sdk_nextjs-ssr/eslint.config.mjs +++ b/implementations/react-web-sdk+node-sdk_nextjs-ssr/eslint.config.mjs @@ -6,6 +6,15 @@ const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, globalIgnores(['.next/**', 'out/**', 'build/**', 'next-env.d.ts']), + { + settings: { + // Fix for ESLint 10+: eslint-plugin-react uses context.getFilename() (legacy API) (this package is used by eslint-config-nex) + // which was removed in ESLint 10 flat config. Declaring the version explicitly + // prevents the plugin from trying to auto-detect it and failing. + // but we will still get lint errors so we need the main lint project to ignore this for now + react: { version: '19' }, + }, + }, ]) export default eslintConfig diff --git a/package.json b/package.json index 406fef96..80c50e19 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "implementation:web-sdk_react": "pnpm run implementation:run -- web-sdk_react", "implementation:react-web-sdk+node-sdk_nextjs-ssr": "pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs-ssr", "implementation:web-sdk": "pnpm run implementation:run -- web-sdk", - "implementation:lint": "eslint implementations --cache --cache-location .cache/eslint/implementations", - "implementation:lint:fix": "eslint implementations --fix", + "implementation:lint": "eslint implementations --ignore-pattern implementations/react-web-sdk+node-sdk_nextjs-ssr --cache --cache-location .cache/eslint/implementations && pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs-ssr lint", + "implementation:lint:fix": "eslint implementations --ignore-pattern implementations/react-web-sdk+node-sdk_nextjs-ssr --fix && pnpm run implementation:run -- react-web-sdk+node-sdk_nextjs-ssr lint", "implementation:typecheck": "pnpm run implementation:run -- --all -- typecheck", "lint": "eslint lib packages --cache --cache-location .cache/eslint/workspace", "lint:fix": "eslint lib packages --fix", From 69c4afc7642ba448ce14c4a3a414df6e1ad66706 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Wed, 6 May 2026 15:15:15 +0200 Subject: [PATCH 16/17] docs: update readme with pattern information of this ssr setup --- .../README.md | 179 ++++++++++++++++-- 1 file changed, 168 insertions(+), 11 deletions(-) diff --git a/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md b/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md index 1f7c39d1..96b87a4a 100644 --- a/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md +++ b/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md @@ -1,25 +1,165 @@ -# Next.js SSR Hybrid Reference Implementation +# Next.js SSR + Client Events Reference Implementation `react-web-sdk+node-sdk_nextjs-ssr` — Next.js App Router reference using `@contentful/optimization-node` for server-side entry resolution and `@contentful/optimization-react-web` for client-side event tracking and interactive controls. +## Pattern: SSR-Primary with CSR Analytics + +This setup is the simplest and most robust Next.js personalization pattern. The server owns +all personalization decisions. The client owns all analytics and interactive concerns. + +### Why this pattern? + +- **No flicker.** Personalized content is in the HTML from the server. No loading states, no client-side variant swaps. +- **Full SEO.** Search engines see the resolved personalized content. +- **Minimal client JS.** Content rendering requires zero JavaScript. Only tracking and interactive + controls (consent, identify) need client hydration. +- **No Next.js SDK needed.** The Node SDK (stateless) works in Server Components and Middleware. The + React Web SDK (stateful) works in Client Components. No framework-specific glue package required. + +### Responsibility split + +| Concern | Where it runs | SDK used | +|---------|--------------|----------| +| Anonymous ID cookie lifecycle | Middleware (Edge Runtime) | Node SDK | +| Profile resolution (`sdk.page()`) | Server Component | Node SDK | +| Entry variant resolution | Server Component | Node SDK (`resolveOptimizedEntry`) | +| HTML rendering of personalized content | Server Component | None (plain React) | +| Page view tracking | Client (after hydration) | React Web SDK (`NextAppAutoPageTracker`) | +| Entry interaction tracking (views/clicks/hovers) | Client (after hydration) | React Web SDK (`autoTrackEntryInteraction`) | +| Consent management | Client (after hydration) | React Web SDK (`sdk.consent()`) | +| User identification | Client (after hydration) | React Web SDK (`sdk.identify()`) | + +### Behavioral expectations + +Once the page is served, the personalized content is **static until the next server roundtrip**. +Client-side actions like granting consent or identifying the user update the Web SDK's internal +state and fire analytics events, but they do **not** cause the page content to re-render or swap +variants. The user sees the updated personalization only on the next full navigation (a new server +request where the Node SDK re-resolves entries with the updated profile). + +This is intentional: the server is the single source of truth for what content to show. The client +never contradicts what the server rendered. + ## Architecture ``` -Request - → Middleware: reads ctfl-opt-aid cookie, calls Node SDK sdk.page(), sets cookie - → Server Component (page.tsx): reads cookie, calls Node SDK sdk.page(), - fetches entries from CDA, resolves with sdk.resolveOptimizedEntry(), - renders personalized HTML +┌─────────────────────────────────────────────────────────────────────┐ +│ REQUEST PHASE (Server) │ +│ │ +│ 1. Middleware (Edge Runtime) │ +│ ├─ Read `ctfl-opt-aid` cookie from request │ +│ ├─ Call Node SDK `sdk.page()` with request context + profile │ +│ └─ Set `ctfl-opt-aid` cookie on response with profile.id │ +│ │ +│ 2. Server Component (page.tsx) │ +│ ├─ Read `ctfl-opt-aid` cookie (set by middleware in same cycle) │ +│ ├─ Fetch Contentful entries from CDA (in parallel) │ +│ ├─ Call Node SDK `sdk.page()` → get selectedOptimizations │ +│ ├─ For each entry: `sdk.resolveOptimizedEntry(entry, selected)` │ +│ └─ Render resolved entries as plain HTML │ +│ │ +│ ↓ HTML response with personalized content │ +└─────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ HYDRATION PHASE (Browser) │ +│ │ +│ 3. ClientProviderWrapper (dynamic, ssr: false) │ +│ ├─ OptimizationRoot initializes Web SDK │ +│ ├─ Reads `ctfl-opt-aid` cookie → same identity as server │ +│ ├─ NextAppAutoPageTracker fires page view event │ +│ └─ autoTrackEntryInteraction observes elements with │ +│ data-ctfl-entry-id attributes (views, clicks, hovers) │ +│ │ +│ 4. InteractiveControls (client component) │ +│ ├─ Subscribes to sdk.states.consent / sdk.states.profile │ +│ ├─ Renders consent toggle button │ +│ └─ Renders identify / reset buttons │ +│ │ +│ Note: No content re-rendering happens client-side. │ +│ Content remains as server-rendered until next navigation. │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Key implementation patterns + +### 1. Cookie as the identity bridge + +The `ctfl-opt-aid` cookie is the **only shared state** between server and client. Middleware +creates it, the Server Component reads it, and the Web SDK reads it from `document.cookie` after +hydration. This ensures both sides operate on the same anonymous profile. + +```typescript +// middleware.ts +const anonymousId = request.cookies.get(ANONYMOUS_ID_COOKIE)?.value +const profile = anonymousId ? { id: anonymousId } : undefined +const data = await sdk.page({ ...requestContext, profile }) +response.cookies.set(ANONYMOUS_ID_COOKIE, data.profile.id, { path: '/', sameSite: 'lax' }) +``` + +### 2. Node SDK as a module-level singleton + +The Node SDK is stateless and safe to reuse across requests. A single instance is created at module +load and imported by both middleware and page: -Browser - → HTML arrives with correct personalized content (zero JS for content) - → React SDK hydrates via ClientProviderWrapper (dynamic, ssr:false) - → InteractiveControls hydrate for consent/identify buttons - → Auto page tracking and entry interaction tracking active +```typescript +// lib/optimization-server.ts +import ContentfulOptimization from '@contentful/optimization-node' +const sdk = new ContentfulOptimization({ clientId, environment, api }) +export { sdk } ``` +### 3. React Web SDK loaded only on the client + +The Web SDK depends on browser APIs (`localStorage`, `document.cookie`, `IntersectionObserver`). +Using `next/dynamic` with `ssr: false` prevents any server-side instantiation: + +```typescript +// components/ClientProviderWrapper.tsx +const OptimizationRoot = dynamic( + () => import('@contentful/optimization-react-web').then((mod) => ({ default: mod.OptimizationRoot })), + { ssr: false }, +) +``` + +### 4. Server Components never import from the React Web SDK + +This is a hard rule. Server Components use `@contentful/optimization-node` only. Client Components +(`"use client"`) use `@contentful/optimization-react-web` only. Mixing them causes runtime errors +or bundling issues. + +### 5. Data attributes for automatic interaction tracking + +Server-rendered entries include `data-ctfl-entry-id` and `data-ctfl-baseline-id` attributes. After +hydration, the Web SDK's `autoTrackEntryInteraction` uses a MutationObserver to detect these +elements and registers IntersectionObserver (views), click listeners, and hover listeners +automatically: + +```tsx +
+ {/* content */} +
+``` + +## When does the user see updated personalization? + +| User action | Effect on displayed content | When personalization updates | +|---|---|---| +| First page load (anonymous) | Baseline or variant per profile | Immediate (server-resolved) | +| Grant/reject consent | No change to content | Next server request | +| Identify (`sdk.identify()`) | No change to content | Next server request | +| Navigate to another page (full navigation) | New server-resolved content | Immediate (new SSR) | +| Browser refresh | Server re-resolves with updated profile | Immediate (new SSR) | + +The key insight: **client actions update the profile server-side (via the Experience API)**, but the +rendered content is only a snapshot of the profile state at the time of the server request. The next +request will reflect the updated profile. + ## Setup ```bash @@ -33,3 +173,20 @@ cp implementations/react-web-sdk+node-sdk_nextjs-ssr/.env.example implementation ```bash pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr dev ``` + +## When to use this pattern + +- Content-heavy marketing sites where SEO and first-paint performance matter +- Sites where personalization is based on profile traits, audience segments, or geo — not + real-time interactions within the same page +- Teams that want the simplest mental model: server decides what to show, client tracks what happened +- Sites already using Next.js App Router with Server Components + +## When NOT to use this pattern + +- If you need instant client-side variant swaps after identify (e.g., "Welcome back, Charles!" + appearing without a page refresh) — consider (Hybrid SSR + CSR takeover) +- If your site is a pure SPA with no server rendering — use the React Web SDK directly (see + `react-web-sdk` implementation) +- If you need edge-side personalization for static/cached pages — consider a middleware-based ESR + pattern with `resolveOptimizedEntry` at the edge From 745a411734e99e83171f20b04ee5652ca0dc1490 Mon Sep 17 00:00:00 2001 From: Lotfi Arif <52082662+Lotfi-Arif@users.noreply.github.com> Date: Wed, 6 May 2026 15:54:39 +0200 Subject: [PATCH 17/17] fix: format fix with prettier --- .../README.md | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md b/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md index 96b87a4a..dc59a3ff 100644 --- a/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md +++ b/implementations/react-web-sdk+node-sdk_nextjs-ssr/README.md @@ -6,12 +6,13 @@ ## Pattern: SSR-Primary with CSR Analytics -This setup is the simplest and most robust Next.js personalization pattern. The server owns -all personalization decisions. The client owns all analytics and interactive concerns. +This setup is the simplest and most robust Next.js personalization pattern. The server owns all +personalization decisions. The client owns all analytics and interactive concerns. ### Why this pattern? -- **No flicker.** Personalized content is in the HTML from the server. No loading states, no client-side variant swaps. +- **No flicker.** Personalized content is in the HTML from the server. No loading states, no + client-side variant swaps. - **Full SEO.** Search engines see the resolved personalized content. - **Minimal client JS.** Content rendering requires zero JavaScript. Only tracking and interactive controls (consent, identify) need client hydration. @@ -20,16 +21,16 @@ all personalization decisions. The client owns all analytics and interactive con ### Responsibility split -| Concern | Where it runs | SDK used | -|---------|--------------|----------| -| Anonymous ID cookie lifecycle | Middleware (Edge Runtime) | Node SDK | -| Profile resolution (`sdk.page()`) | Server Component | Node SDK | -| Entry variant resolution | Server Component | Node SDK (`resolveOptimizedEntry`) | -| HTML rendering of personalized content | Server Component | None (plain React) | -| Page view tracking | Client (after hydration) | React Web SDK (`NextAppAutoPageTracker`) | -| Entry interaction tracking (views/clicks/hovers) | Client (after hydration) | React Web SDK (`autoTrackEntryInteraction`) | -| Consent management | Client (after hydration) | React Web SDK (`sdk.consent()`) | -| User identification | Client (after hydration) | React Web SDK (`sdk.identify()`) | +| Concern | Where it runs | SDK used | +| ------------------------------------------------ | ------------------------- | ------------------------------------------- | +| Anonymous ID cookie lifecycle | Middleware (Edge Runtime) | Node SDK | +| Profile resolution (`sdk.page()`) | Server Component | Node SDK | +| Entry variant resolution | Server Component | Node SDK (`resolveOptimizedEntry`) | +| HTML rendering of personalized content | Server Component | None (plain React) | +| Page view tracking | Client (after hydration) | React Web SDK (`NextAppAutoPageTracker`) | +| Entry interaction tracking (views/clicks/hovers) | Client (after hydration) | React Web SDK (`autoTrackEntryInteraction`) | +| Consent management | Client (after hydration) | React Web SDK (`sdk.consent()`) | +| User identification | Client (after hydration) | React Web SDK (`sdk.identify()`) | ### Behavioral expectations @@ -87,9 +88,9 @@ never contradicts what the server rendered. ### 1. Cookie as the identity bridge -The `ctfl-opt-aid` cookie is the **only shared state** between server and client. Middleware -creates it, the Server Component reads it, and the Web SDK reads it from `document.cookie` after -hydration. This ensures both sides operate on the same anonymous profile. +The `ctfl-opt-aid` cookie is the **only shared state** between server and client. Middleware creates +it, the Server Component reads it, and the Web SDK reads it from `document.cookie` after hydration. +This ensures both sides operate on the same anonymous profile. ```typescript // middleware.ts @@ -119,7 +120,8 @@ Using `next/dynamic` with `ssr: false` prevents any server-side instantiation: ```typescript // components/ClientProviderWrapper.tsx const OptimizationRoot = dynamic( - () => import('@contentful/optimization-react-web').then((mod) => ({ default: mod.OptimizationRoot })), + () => + import('@contentful/optimization-react-web').then((mod) => ({ default: mod.OptimizationRoot })), { ssr: false }, ) ``` @@ -127,8 +129,8 @@ const OptimizationRoot = dynamic( ### 4. Server Components never import from the React Web SDK This is a hard rule. Server Components use `@contentful/optimization-node` only. Client Components -(`"use client"`) use `@contentful/optimization-react-web` only. Mixing them causes runtime errors -or bundling issues. +(`"use client"`) use `@contentful/optimization-react-web` only. Mixing them causes runtime errors or +bundling issues. ### 5. Data attributes for automatic interaction tracking @@ -138,23 +140,20 @@ elements and registers IntersectionObserver (views), click listeners, and hover automatically: ```tsx -
+
{/* content */}
``` ## When does the user see updated personalization? -| User action | Effect on displayed content | When personalization updates | -|---|---|---| -| First page load (anonymous) | Baseline or variant per profile | Immediate (server-resolved) | -| Grant/reject consent | No change to content | Next server request | -| Identify (`sdk.identify()`) | No change to content | Next server request | -| Navigate to another page (full navigation) | New server-resolved content | Immediate (new SSR) | -| Browser refresh | Server re-resolves with updated profile | Immediate (new SSR) | +| User action | Effect on displayed content | When personalization updates | +| ------------------------------------------ | --------------------------------------- | ---------------------------- | +| First page load (anonymous) | Baseline or variant per profile | Immediate (server-resolved) | +| Grant/reject consent | No change to content | Next server request | +| Identify (`sdk.identify()`) | No change to content | Next server request | +| Navigate to another page (full navigation) | New server-resolved content | Immediate (new SSR) | +| Browser refresh | Server re-resolves with updated profile | Immediate (new SSR) | The key insight: **client actions update the profile server-side (via the Experience API)**, but the rendered content is only a snapshot of the profile state at the time of the server request. The next @@ -177,9 +176,10 @@ pnpm implementation:run -- react-web-sdk+node-sdk_nextjs-ssr dev ## When to use this pattern - Content-heavy marketing sites where SEO and first-paint performance matter -- Sites where personalization is based on profile traits, audience segments, or geo — not - real-time interactions within the same page -- Teams that want the simplest mental model: server decides what to show, client tracks what happened +- Sites where personalization is based on profile traits, audience segments, or geo — not real-time + interactions within the same page +- Teams that want the simplest mental model: server decides what to show, client tracks what + happened - Sites already using Next.js App Router with Server Components ## When NOT to use this pattern