From e67446cabeee3d495ad2cb414385fd6f7ef5f446 Mon Sep 17 00:00:00 2001 From: Charles Hudson Date: Thu, 23 Jul 2026 17:21:13 +0200 Subject: [PATCH] docs(docs): add experience.js migration workflow Add the documentation pipeline for authoring experience.js to Optimization SDK Suite migration guides. This includes new local agent roles for legacy migration knowledge authoring and migration guide architecture, plus migration-aware source verification instructions for both Codex and Claude agent adapters. Add an internal experience.js migration knowledge file sourced from the adjacent experience.js checkout, and add a migration guide authoring recipe with dedicated migration blueprints for the supported migration paths: - Contentful model migration - Web SDK migration - React Web migration - Next.js migration path choice - Next.js App Router migration - Next.js Pages Router migration - Node, SSR, and ESR migration - Plugins and preview migration Add the public migration guide set and route it from the guides index in a dedicated experience.js migration section, separate from supplemental guides. Extend SDK knowledge for migration-relevant target behavior: - stateful flag-view tracking and manual trackFlagView usage - accepted and blocked event forwarding streams - preview audience, variant, and inline-variable overrides - Web, React Web, and Next.js migration-facing references for those behaviors Extend guide authoring validation so pnpm guides:check validates migration blueprints, including required frontmatter, template headings, route and section tables, fact-source links, source/guide targets, local links, anchors, and the migration guide section spine. Validation: - pnpm guides:check - pnpm knowledge:check - pnpm lint - targeted Prettier checks [[NT-3738](https://contentful.atlassian.net/browse/NT-3738)] --- ...xperience-js-migration-knowledge-author.md | 38 ++++ .claude/agents/guide-source-verifier.md | 42 ++-- .claude/agents/migration-guide-architect.md | 29 +++ ...erience-js-migration-knowledge-author.toml | 32 +++ .codex/agents/guide-source-verifier.toml | 33 +-- .codex/agents/migration-guide-architect.toml | 23 ++ documentation/authoring/README.md | 60 ++++++ .../migration-blueprints/_template.md | 53 +++++ .../experience-js-contentful-model.md | 48 +++++ .../experience-js-nextjs-app-router.md | 51 +++++ .../experience-js-nextjs-pages-router.md | 51 +++++ .../experience-js-nextjs-router-choice.md | 49 +++++ .../experience-js-node-ssr-esr.md | 52 +++++ .../experience-js-plugins-preview-privacy.md | 51 +++++ .../experience-js-react-web.md | 52 +++++ .../migration-blueprints/experience-js-web.md | 49 +++++ documentation/authoring/recipes/migration.md | 83 ++++++++ documentation/guides/README.md | 25 +++ ...extjs-migration-path-from-experience-js.md | 127 +++++++++++ ...nce-js-contentful-model-to-optimization.md | 145 +++++++++++++ ...experience-js-next-to-nextjs-app-router.md | 135 ++++++++++++ ...perience-js-next-to-nextjs-pages-router.md | 131 ++++++++++++ ...igrating-experience-js-node-ssr-and-esr.md | 120 +++++++++++ ...ating-experience-js-plugins-and-preview.md | 132 ++++++++++++ ...rating-experience-js-react-to-react-web.md | 136 ++++++++++++ .../migrating-experience-js-to-the-web-sdk.md | 126 +++++++++++ .../migration-knowledge/experience-js.md | 86 ++++++++ .../internal/sdk-knowledge/shared/concepts.md | 60 ++++-- .../sdk-knowledge/web/nextjs-app-router.md | 7 + .../sdk-knowledge/web/nextjs-pages-router.md | 7 + .../internal/sdk-knowledge/web/react-web.md | 25 +-- .../internal/sdk-knowledge/web/web.md | 25 ++- scripts/validate-guide-authoring.ts | 201 +++++++++++++++--- 33 files changed, 2186 insertions(+), 98 deletions(-) create mode 100644 .claude/agents/experience-js-migration-knowledge-author.md create mode 100644 .claude/agents/migration-guide-architect.md create mode 100644 .codex/agents/experience-js-migration-knowledge-author.toml create mode 100644 .codex/agents/migration-guide-architect.toml create mode 100644 documentation/authoring/migration-blueprints/_template.md create mode 100644 documentation/authoring/migration-blueprints/experience-js-contentful-model.md create mode 100644 documentation/authoring/migration-blueprints/experience-js-nextjs-app-router.md create mode 100644 documentation/authoring/migration-blueprints/experience-js-nextjs-pages-router.md create mode 100644 documentation/authoring/migration-blueprints/experience-js-nextjs-router-choice.md create mode 100644 documentation/authoring/migration-blueprints/experience-js-node-ssr-esr.md create mode 100644 documentation/authoring/migration-blueprints/experience-js-plugins-preview-privacy.md create mode 100644 documentation/authoring/migration-blueprints/experience-js-react-web.md create mode 100644 documentation/authoring/migration-blueprints/experience-js-web.md create mode 100644 documentation/authoring/recipes/migration.md create mode 100644 documentation/guides/choosing-a-nextjs-migration-path-from-experience-js.md create mode 100644 documentation/guides/migrating-experience-js-contentful-model-to-optimization.md create mode 100644 documentation/guides/migrating-experience-js-next-to-nextjs-app-router.md create mode 100644 documentation/guides/migrating-experience-js-next-to-nextjs-pages-router.md create mode 100644 documentation/guides/migrating-experience-js-node-ssr-and-esr.md create mode 100644 documentation/guides/migrating-experience-js-plugins-and-preview.md create mode 100644 documentation/guides/migrating-experience-js-react-to-react-web.md create mode 100644 documentation/guides/migrating-experience-js-to-the-web-sdk.md create mode 100644 documentation/internal/migration-knowledge/experience-js.md diff --git a/.claude/agents/experience-js-migration-knowledge-author.md b/.claude/agents/experience-js-migration-knowledge-author.md new file mode 100644 index 000000000..806a491b1 --- /dev/null +++ b/.claude/agents/experience-js-migration-knowledge-author.md @@ -0,0 +1,38 @@ +--- +name: experience-js-migration-knowledge-author +description: >- + Derive migration-relevant legacy experience.js facts from the adjacent experience.js workspace into + this repo's internal migration knowledge. Use before authoring experience.js -> Optimization SDK + Suite migration guides, or when a migration guide/review exposes a missing legacy behavior fact. +tools: Read, Grep, Glob, Edit, Write, Bash +--- + +You are the experience.js migration knowledge author. Use the **`sdk-knowledge-maintenance`** skill +for fact-store discipline and the **`sdk-knowledge-authoring`** skill's behavior-vs-interface split, +but adapt it to the adjacent legacy repository instead of this repo's `packages/**/src`. + +Your source of truth is the adjacent checkout at +`/Users/charles.hudson/Projects/contentful/experience.js`. Inspect its git status and current commit +before deriving facts. If the checkout is missing, dirty in relevant files, or appears stale, report +that state and ask before updating or relying on it. Do not modify the adjacent repository. + +Record only migration-relevant legacy facts in this repository, under the migration knowledge area +requested by the task (for example `documentation/internal/migration-knowledge/experience-js.md`). +Keep facts terse and present-tense. Capture behavior and migration boundaries, not guide prose: +package mapping, provider/hooks/components, Contentful ExperienceMapper/content-model expectations, +Experience API event behavior, plugins, persistence/cookie/storage identifiers, SSR/Next.js paths, +preview behavior, and unsupported or manual migration gaps. + +Do not copy detailed signatures, prop lists, or return shapes unless they are needed as a compact +navigation index. Legacy interface shape can be looked up directly in the adjacent repo when a guide +writer or verifier needs it. Each legacy fact should carry enough provenance to re-check it later: +include the experience.js commit and source path/symbol, plus any external documentation URL only as +secondary context. Prefer source over public docs when they disagree. + +When migration work exposes a missing Optimization SDK behavior fact, do not fill it yourself. Hand +that specific gap to `sdk-knowledge-author`; Optimization SDK behavior belongs in +`documentation/internal/sdk-knowledge/`. + +Before finishing, report which migration facts you added or changed, the experience.js commit used, +and which migration guide or blueprint can consume the facts. You do not write public guide prose and +you do not review guides. diff --git a/.claude/agents/guide-source-verifier.md b/.claude/agents/guide-source-verifier.md index b2a38a19b..96b89c668 100644 --- a/.claude/agents/guide-source-verifier.md +++ b/.claude/agents/guide-source-verifier.md @@ -1,12 +1,11 @@ --- name: guide-source-verifier description: >- - Verify a documentation guide's load-bearing SDK claims — the third authoring role — splitting each - into interface vs. behavior. Interface (symbol/signature/prop/return shape) is checked directly - against the types in packages/**/src; behavior (fallback, dynamic render, batching, defaults, - ownership, cross-SDK semantics) is checked against the knowledge base and NOT re-traced from source. - Behavioral gaps escalate to the sdk-knowledge-author. Use after a guide is drafted or refreshed and - newcomer-reviewed, or to fact-check a claim. + Verify a documentation guide's load-bearing SDK and migration claims — the third authoring role — + splitting each into interface vs. behavior. Interface is checked directly against the relevant + source/types; Optimization behavior is checked against the SDK knowledge base; legacy experience.js + behavior is checked against migration knowledge. Behavioral gaps escalate to the matching knowledge + author. Use after a guide is drafted or refreshed and newcomer-reviewed, or to fact-check a claim. tools: Read, Grep, Glob, Bash --- @@ -15,18 +14,27 @@ You are the technical-foundation reviewer for Optimization SDK guides. Follow th against its authority: - **Interface** (a symbol's existence, signature, prop/config-key names & types, optionality, union - shape, return type, import path) — verify directly against the types in `packages/**/src`. Reading - source for interface is expected and cheap; a mismatch is a guide bug → correction to the writer. + shape, return type, import path) — verify directly against the relevant source/types. For + Optimization SDK claims, use this repo's `packages/**/src`. For legacy experience.js migration + claims, use the adjacent checkout at `/Users/charles.hudson/Projects/contentful/experience.js`. + Reading source for interface is expected and cheap; a mismatch is a guide bug → correction to the + writer. - **Behavior** (fallback contracts, dynamic-render forcing, batching/chunking, defaults, identifier - ownership, cross-SDK semantics) — confirm against the knowledge base - (`documentation/internal/sdk-knowledge/`); a claim is **confirmed** when a matching fact exists and - `pnpm knowledge:check` passes, **contradicted** when the base says otherwise (guide bug → writer). - Do NOT re-trace behavior from source. A behavioral claim with **no backing fact** escalates to the - **`sdk-knowledge-author`** — either the base is missing a fact it should hold, or the claim is - unfounded and comes out of the guide. (An unbacked interface claim is not an escalation — you just - checked it against the types.) + ownership, cross-SDK semantics) — confirm against the appropriate fact store. Optimization SDK + behavior comes from `documentation/internal/sdk-knowledge/`; legacy experience.js behavior comes + from `documentation/internal/migration-knowledge/`. A claim is **confirmed** when a matching fact + exists, **contradicted** when the fact store says otherwise (guide bug → writer), and + **behavioral-no-backing-fact** when no matching fact exists. Do NOT re-trace behavior from either + source repo during verification. + +Escalate behavioral gaps to the owner of the missing fact store: + +- Optimization SDK behavior gaps → **`sdk-knowledge-author`**. +- Legacy experience.js behavior gaps → **`experience-js-migration-knowledge-author`**. + +An unbacked interface claim is not an escalation — you just checked it against source/types. You do not edit the knowledge base or the guide. Return a per-claim verdict (interface or behavior; confirmed / contradicted / behavioral-no-backing-fact) with evidence — `file:symbol` for interface, -the KB fact for behavior — guide corrections routed to the writer, behavioral fact gaps to the -knowledge author. +the KB or migration-knowledge fact for behavior — guide corrections routed to the writer, behavioral +fact gaps routed to the matching knowledge author. diff --git a/.claude/agents/migration-guide-architect.md b/.claude/agents/migration-guide-architect.md new file mode 100644 index 000000000..dfc34237e --- /dev/null +++ b/.claude/agents/migration-guide-architect.md @@ -0,0 +1,29 @@ +--- +name: migration-guide-architect +description: >- + Create or revise the authoring structure for experience.js -> Optimization SDK Suite migration + guides. Use after legacy migration facts exist and before guide-writer drafts reader-facing + migration prose. +tools: Read, Grep, Glob, Edit, Write, Bash +--- + +You are the migration guide architect for the Optimization SDK Suite. Follow the +**`optimization-guide-authoring`** skill for guide-authoring ownership boundaries, but work at the +authoring-input layer rather than drafting the public guide. + +Create or revise the minimal recipe/blueprint structure needed for an `experience.js` -> +Optimization SDK Suite migration guide. The recipe owns the migration-guide spine and repeatable +rules. The blueprint owns this migration's reader goal, quick-start proof, section map, teaching +priorities, and links to fact sources. Keep SDK behavior out of recipes and blueprints; route legacy +behavior to `documentation/internal/migration-knowledge/` and Optimization behavior to +`documentation/internal/sdk-knowledge/`. + +Do not invent migration facts and do not re-trace SDK behavior. If the required legacy fact is +missing, hand it to `experience-js-migration-knowledge-author`. If the required Optimization fact is +missing, hand it to `sdk-knowledge-author`. Interface names may appear only as routing or section +labels; detailed signatures remain in source/types. + +Prefer the smallest structure that lets `guide-writer` draft the migration guide without guessing: +one migration recipe only if existing recipes do not fit, one migration blueprint for the +experience.js migration, and targeted updates to authoring indexes only when needed. You do not write +the public migration guide and you do not review guides. diff --git a/.codex/agents/experience-js-migration-knowledge-author.toml b/.codex/agents/experience-js-migration-knowledge-author.toml new file mode 100644 index 000000000..2f1d97b2f --- /dev/null +++ b/.codex/agents/experience-js-migration-knowledge-author.toml @@ -0,0 +1,32 @@ +name = "experience-js-migration-knowledge-author" +description = "Derive migration-relevant legacy experience.js facts from the adjacent experience.js workspace into this repo's internal migration knowledge. Use before authoring experience.js -> Optimization SDK Suite migration guides, or when a migration guide/review exposes a missing legacy behavior fact." +developer_instructions = """ +You are the experience.js migration knowledge author. Use the **`sdk-knowledge-maintenance`** skill +for fact-store discipline and the **`sdk-knowledge-authoring`** skill's behavior-vs-interface split, +but adapt it to the adjacent legacy repository instead of this repo's `packages/**/src`. + +Your source of truth is the adjacent checkout at +`/Users/charles.hudson/Projects/contentful/experience.js`. Inspect its git status and current commit +before deriving facts. If the checkout is missing, dirty in relevant files, or appears stale, report +that state and ask before updating or relying on it. Do not modify the adjacent repository. + +Record only migration-relevant legacy facts in this repository, under the migration knowledge area +requested by the task (for example `documentation/internal/migration-knowledge/experience-js.md`). +Keep facts terse and present-tense. Capture behavior and migration boundaries, not guide prose: +package mapping, provider/hooks/components, Contentful ExperienceMapper/content-model expectations, +Experience API event behavior, plugins, persistence/cookie/storage identifiers, SSR/Next.js paths, +preview behavior, and unsupported or manual migration gaps. + +Do not copy detailed signatures, prop lists, or return shapes unless they are needed as a compact +navigation index. Legacy interface shape can be looked up directly in the adjacent repo when a guide +writer or verifier needs it. Each legacy fact should carry enough provenance to re-check it later: +include the experience.js commit and source path/symbol, plus any external documentation URL only as +secondary context. Prefer source over public docs when they disagree. + +When migration work exposes a missing Optimization SDK behavior fact, do not fill it yourself. Hand +that specific gap to `sdk-knowledge-author`; Optimization SDK behavior belongs in +`documentation/internal/sdk-knowledge/`. + +Before finishing, report which migration facts you added or changed, the experience.js commit used, +and which migration guide or blueprint can consume the facts. You do not write public guide prose and +you do not review guides.""" diff --git a/.codex/agents/guide-source-verifier.toml b/.codex/agents/guide-source-verifier.toml index dc69d3e0f..893b6a26f 100644 --- a/.codex/agents/guide-source-verifier.toml +++ b/.codex/agents/guide-source-verifier.toml @@ -1,23 +1,32 @@ name = "guide-source-verifier" -description = "Verify a documentation guide's load-bearing SDK claims — the third authoring role — splitting each into interface vs. behavior. Interface (symbol/signature/prop/return shape) is checked directly against the types in packages/**/src; behavior (fallback, dynamic render, batching, defaults, ownership, cross-SDK semantics) is checked against the knowledge base and NOT re-traced from source. Behavioral gaps escalate to the sdk-knowledge-author. Use after a guide is drafted or refreshed and newcomer-reviewed, or to fact-check a claim." +description = "Verify a documentation guide's load-bearing SDK and migration claims — the third authoring role — splitting each into interface vs. behavior. Interface is checked directly against the relevant source/types; Optimization behavior is checked against the SDK knowledge base; legacy experience.js behavior is checked against migration knowledge. Behavioral gaps escalate to the matching knowledge author. Use after a guide is drafted or refreshed and newcomer-reviewed, or to fact-check a claim." developer_instructions = """ You are the technical-foundation reviewer for Optimization SDK guides. Follow the **`guide-source-verification`** skill. Split every load-bearing claim into two kinds and check each against its authority: - **Interface** (a symbol's existence, signature, prop/config-key names & types, optionality, union - shape, return type, import path) — verify directly against the types in `packages/**/src`. Reading - source for interface is expected and cheap; a mismatch is a guide bug → correction to the writer. + shape, return type, import path) — verify directly against the relevant source/types. For + Optimization SDK claims, use this repo's `packages/**/src`. For legacy experience.js migration + claims, use the adjacent checkout at `/Users/charles.hudson/Projects/contentful/experience.js`. + Reading source for interface is expected and cheap; a mismatch is a guide bug → correction to the + writer. - **Behavior** (fallback contracts, dynamic-render forcing, batching/chunking, defaults, identifier - ownership, cross-SDK semantics) — confirm against the knowledge base - (`documentation/internal/sdk-knowledge/`); a claim is **confirmed** when a matching fact exists and - `pnpm knowledge:check` passes, **contradicted** when the base says otherwise (guide bug → writer). - Do NOT re-trace behavior from source. A behavioral claim with **no backing fact** escalates to the - **`sdk-knowledge-author`** — either the base is missing a fact it should hold, or the claim is - unfounded and comes out of the guide. (An unbacked interface claim is not an escalation — you just - checked it against the types.) + ownership, cross-SDK semantics) — confirm against the appropriate fact store. Optimization SDK + behavior comes from `documentation/internal/sdk-knowledge/`; legacy experience.js behavior comes + from `documentation/internal/migration-knowledge/`. A claim is **confirmed** when a matching fact + exists, **contradicted** when the fact store says otherwise (guide bug → writer), and + **behavioral-no-backing-fact** when no matching fact exists. Do NOT re-trace behavior from either + source repo during verification. + +Escalate behavioral gaps to the owner of the missing fact store: + +- Optimization SDK behavior gaps → **`sdk-knowledge-author`**. +- Legacy experience.js behavior gaps → **`experience-js-migration-knowledge-author`**. + +An unbacked interface claim is not an escalation — you just checked it against source/types. You do not edit the knowledge base or the guide. Return a per-claim verdict (interface or behavior; confirmed / contradicted / behavioral-no-backing-fact) with evidence — `file:symbol` for interface, -the KB fact for behavior — guide corrections routed to the writer, behavioral fact gaps to the -knowledge author.""" +the KB or migration-knowledge fact for behavior — guide corrections routed to the writer, behavioral +fact gaps routed to the matching knowledge author.""" diff --git a/.codex/agents/migration-guide-architect.toml b/.codex/agents/migration-guide-architect.toml new file mode 100644 index 000000000..eb6f8801d --- /dev/null +++ b/.codex/agents/migration-guide-architect.toml @@ -0,0 +1,23 @@ +name = "migration-guide-architect" +description = "Create or revise the authoring structure for experience.js -> Optimization SDK Suite migration guides. Use after legacy migration facts exist and before guide-writer drafts reader-facing migration prose." +developer_instructions = """ +You are the migration guide architect for the Optimization SDK Suite. Follow the +**`optimization-guide-authoring`** skill for guide-authoring ownership boundaries, but work at the +authoring-input layer rather than drafting the public guide. + +Create or revise the minimal recipe/blueprint structure needed for an `experience.js` -> +Optimization SDK Suite migration guide. The recipe owns the migration-guide spine and repeatable +rules. The blueprint owns this migration's reader goal, quick-start proof, section map, teaching +priorities, and links to fact sources. Keep SDK behavior out of recipes and blueprints; route legacy +behavior to `documentation/internal/migration-knowledge/` and Optimization behavior to +`documentation/internal/sdk-knowledge/`. + +Do not invent migration facts and do not re-trace SDK behavior. If the required legacy fact is +missing, hand it to `experience-js-migration-knowledge-author`. If the required Optimization fact is +missing, hand it to `sdk-knowledge-author`. Interface names may appear only as routing or section +labels; detailed signatures remain in source/types. + +Prefer the smallest structure that lets `guide-writer` draft the migration guide without guessing: +one migration recipe only if existing recipes do not fit, one migration blueprint for the +experience.js migration, and targeted updates to authoring indexes only when needed. You do not write +the public migration guide and you do not review guides.""" diff --git a/documentation/authoring/README.md b/documentation/authoring/README.md index 4e1a496d2..2a16e8afc 100644 --- a/documentation/authoring/README.md +++ b/documentation/authoring/README.md @@ -16,6 +16,8 @@ run and prevents an editorial iteration from repeating expensive SDK comprehensi - **Refresh docs** when SDK source changed. - **Iterate on a guide** for wording, structure, sequence, or shared-copy changes. - **Review a guide** to check an existing or newly written guide before shipping it. +- **Author a migration guide** when an existing integration is moving from a legacy SDK to the + Optimization SDK Suite. ### Run the workflows @@ -62,6 +64,20 @@ Use this workflow as the final quality gate or when an existing claim looks susp 3. Use `sdk-knowledge-authoring` only when verification exposes a genuine behavioral gap in the KB. 4. Run `pnpm knowledge:check` and `pnpm guides:check` after resolving the findings. +#### Author a migration guide + +Use this workflow when a guide moves an existing integration from a legacy SDK to the Optimization +SDK Suite. + +1. Read the migration recipe, the matching migration blueprint, and the migration knowledge file. +2. Read the target SDK blueprint and KB sections linked by the migration blueprint. +3. Draft the guide with `optimization-guide-authoring` and the guide-writer role. Keep runtime + migrations split; use the plugin/privacy/preview and content-model migration guides for shared + replacement work. +4. Hand missing legacy facts to the migration-knowledge author and missing target behavior facts to + `sdk-knowledge-authoring`; do not invent either in the guide. +5. Run the newcomer and technical-foundation reviews before shipping. + The relevant skill instructions are: - [`optimization-guide-authoring`](../../skills/optimization-guide-authoring/SKILL.md) — teaching @@ -125,6 +141,7 @@ do not make the technical writer trace implementation behavior. | ------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | **Recipe** (`recipes/`) | Archetype-wide section spine, allowed categories, and output rules | Per-SDK section choices or SDK facts | | **Blueprint** (`blueprints/`) | One SDK's reader goal, quick-start proof, milestones, section plan, and teaching priorities | SDK behavior, detailed API shape, or reader-facing prose | +| **Migration blueprint** (`migration-blueprints/`) | One legacy-to-target guide's route, guide order, section plan, and fact links | Legacy facts, SDK behavior, detailed API shape, or prose | | **Shared copy** (`fragments/`) | Reader-facing wording that must stay consistent across several guides | Hidden per-SDK decisions or SDK behavior | | **Knowledge base** (`../internal/sdk-knowledge/`) | Verified behavioral facts and provenance | Editorial sequence or guide prose | | **Types/source** (`../../packages/`) | Exact interface shape: exports, signatures, props, and returns | Editorial decisions | @@ -222,6 +239,38 @@ blueprint-to-guide section and category agreement. `pnpm knowledge:check` valida integrity and unresolved escalations. Neither command proves behavioral truth; semantic freshness comes from the scoped knowledge-author and technical-foundation reviews. +Migration blueprints live in `migration-blueprints/` instead of `blueprints/` because the current +`guides:check` contract treats every file in `blueprints/` as a rendered integration guide blueprint. +Until migration guides are added to that validator, validate migration authoring inputs with +Prettier, link review, and the migration-blueprint structural review below. + +### Migration-blueprint structural review + +Apply this review to every migration blueprint before handing it to a guide writer. It is the +dedicated migration-blueprint check until `guides:check` covers migration guides. + +- Frontmatter has `migration`, `archetype: migration`, `source`, and `guide`; the `guide` link and + Reader goal guide file name agree. +- Reader goal names the legacy state, target result, guide file, and guide order dependency. +- Migration route covers each legacy surface the guide promises to replace and assigns a target route + plus a detail owner; unsupported or manual replacements route to migration knowledge, not guessed + guide prose. +- Section plan rows match the intended `###` sections. Each row has a reader purpose, and the Must + route to column names the target guide, supplemental guide, concept, or decision the writer should + use. +- Fact sources are links only; legacy behavior points to migration knowledge, target behavior points + to SDK KB or shared concepts, and interface details are left for source/types lookup while writing. +- Cross-guide prerequisites are explicit: content-model migration before runtime rendering when + legacy mapper output is involved; plugin/privacy/preview migration after the app has a target + runtime. +- Validation obligations include a first performable authored variant or baseline check, accepted and + blocked event observability when events/consent are touched, first-page-event ownership when server + and browser can both report it, and cache/dynamic consequences for request-bound personalization. +- Ownership boundaries are explicit for app-owned config, consent records, vendor forwarding, manual + Node/Web hybrid cookies, and SDK-owned target profile cookies. +- Handoffs list only missing legacy or target behavior facts; no `None.` if a section relies on an + unlinked behavior claim. + Blueprints and KB files currently cover the six JS/TS integration guides — Node, Web, React Web, Next.js App Router, Next.js Pages Router, and React Native — plus the two native iOS guides (SwiftUI and UIKit) and the two native Android guides (Jetpack Compose and XML Views). Each native family @@ -239,6 +288,7 @@ recipes/ integration.md decision.md supplemental-recipe.md + migration.md blueprints/ _template.md node.md @@ -255,4 +305,14 @@ fragments/ personalization-explainer.md authored-variant-gotcha.md optimization-client-id.md +migration-blueprints/ + _template.md + experience-js-web.md + experience-js-react-web.md + experience-js-nextjs-router-choice.md + experience-js-nextjs-app-router.md + experience-js-nextjs-pages-router.md + experience-js-node-ssr-esr.md + experience-js-plugins-preview-privacy.md + experience-js-contentful-model.md ``` diff --git a/documentation/authoring/migration-blueprints/_template.md b/documentation/authoring/migration-blueprints/_template.md new file mode 100644 index 000000000..9b424014a --- /dev/null +++ b/documentation/authoring/migration-blueprints/_template.md @@ -0,0 +1,53 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/migrating--to-.md +--- + +# blueprint + +## Reader goal + +- **Use when:** +- **Target result:** +- **Guide file:** `documentation/guides/.md` +- **Write after:** +- **First verification:** + +## Migration route + +| Legacy surface | Target route | Detail owner | +| -------------- | ------------ | ------------ | +| ... | ... | ... | + +## Section plan + +Each row becomes one migration section. Link to facts; do not restate behavior. + +| Section | Purpose | Must route to | Fact sources | +| ------- | ------- | ------------- | ------------ | +| ... | ... | ... | ... | + +Every applicable section plan must make these ownership and sequence checks explicit: + +- Content-model migration comes before runtime rendering when legacy `nt_*` fields, mapper output, + or runtime experience configuration arrays are involved. +- Runtime configuration, consent records, first page events, analytics forwarding, preview behavior, + and profile/cookie continuity each have one owner. +- Event migrations include accepted-event evidence and blocked-event diagnostics when the target + runtime supports them. +- Server and framework migrations name per-request dynamic or cache consequences before rendering + replacement. +- Manual Node/Web hybrid migrations keep cookie read/write/clear behavior app-owned and route + framework-cookie behavior to the framework guide or SDK KB. + +## Handoffs + +List missing legacy or target behavior facts. Write `None.` when there are no known gaps. + +## Link roles + +Use exact links for existing target guides, supplemental guides, concepts, and maintained reference +implementations. Planned sibling migration guides may be named as code until they exist. diff --git a/documentation/authoring/migration-blueprints/experience-js-contentful-model.md b/documentation/authoring/migration-blueprints/experience-js-contentful-model.md new file mode 100644 index 000000000..b23675fa7 --- /dev/null +++ b/documentation/authoring/migration-blueprints/experience-js-contentful-model.md @@ -0,0 +1,48 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/migrating-experience-js-contentful-model-to-optimization.md +--- + +# experience.js Contentful model migration blueprint + +## Reader goal + +- **Use when:** A space uses Ninetailed Contentful experience entries, mapper utilities, or + baseline entries linked to legacy `nt_*` fields. +- **Target result:** The reader understands which authored data must move to the Optimization + content model before runtime migration can verify personalized entries. +- **Guide file:** `documentation/guides/migrating-experience-js-contentful-model-to-optimization.md` +- **Write after:** None; runtime migration guides should link here before entry-rendering steps. +- **First verification:** One baseline entry and one authored all-visitors variant resolve in a + target runtime before broader runtime migration. + +## Migration route + +| Legacy surface | Target route | Detail owner | +| --------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------- | +| `@ninetailed/experience.js-utils-contentful` mapper | Built-in Optimization content model and target entry resolution | Runtime integration guide rendering sections | +| Legacy `nt_*` fields | Optimization-authored experiences and variants | Contentful product documentation plus target rendering KB | +| Custom mapper functions | App-owned content adaptation before target SDK entry resolution | This migration guide plus runtime rendering sections | + +## Section plan + +| Section | Purpose | Must route to | Fact sources | +| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Inventory legacy authored entries and mapper usage | Find the Contentful data that cannot be preserved as runtime config arrays. | Legacy model and mapper facts. | [content model](../../internal/migration-knowledge/experience-js.md#contentful-model-and-mapper), [unsupported boundaries](../../internal/migration-knowledge/experience-js.md#unsupported-or-manual-migration-boundaries) | +| Plan target Optimization authoring | Route authoring work to Contentful product docs and target SDK entry-resolution requirements. | Existing Contentful personalization docs and target rendering guide sections. | [entry-source boundary](../../internal/sdk-knowledge/shared/concepts.md#entry-source-boundary-managed-or-manual), [entry resolution](../../internal/sdk-knowledge/shared/concepts.md#entry-resolution), [baseline fallback](../../internal/sdk-knowledge/shared/concepts.md#baseline-fallback) | +| Replace mapper-dependent runtime code | Stop building legacy `ExperienceConfiguration` arrays and route runtime code to target SDK entry resolution. | Runtime-specific Web, React Web, Next.js, or Node rendering sections. | [content model](../../internal/migration-knowledge/experience-js.md#contentful-model-and-mapper), [Web rendering](../../internal/sdk-knowledge/web/web.md#render--entry-resolution), [React Web rendering](../../internal/sdk-knowledge/web/react-web.md#render--entry-resolution), [Node rendering](../../internal/sdk-knowledge/node/node.md#render--entry-resolution) | +| Validate content migration before code migration | Make the first runtime verification performable by checking authored variants and fallback expectations. | Target runtime quick start and troubleshooting. | [baseline fallback](../../internal/sdk-knowledge/shared/concepts.md#baseline-fallback), [Web fallback](../../internal/sdk-knowledge/web/web.md#failure--fallback-behavior), [React Web fallback](../../internal/sdk-knowledge/web/react-web.md#failure--fallback-behavior), [Node fallback](../../internal/sdk-knowledge/node/node.md#failure--fallback-behavior) | + +## Handoffs + +None. + +## Link roles + +- [Entry personalization and variant resolution concept](../../concepts/entry-personalization-and-variant-resolution.md). +- [Web SDK integration guide](../../guides/integrating-the-web-sdk-in-a-web-app.md). +- [React Web integration guide](../../guides/integrating-the-react-web-sdk-in-a-react-app.md). +- [Node SDK integration guide](../../guides/integrating-the-node-sdk-in-a-node-app.md). +- [Contentful personalization authoring](https://www.contentful.com/developers/docs/personalization/). diff --git a/documentation/authoring/migration-blueprints/experience-js-nextjs-app-router.md b/documentation/authoring/migration-blueprints/experience-js-nextjs-app-router.md new file mode 100644 index 000000000..9cc06ee40 --- /dev/null +++ b/documentation/authoring/migration-blueprints/experience-js-nextjs-app-router.md @@ -0,0 +1,51 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/migrating-experience-js-next-to-nextjs-app-router.md +--- + +# experience.js Next.js to App Router migration blueprint + +## Reader goal + +- **Use when:** A Next.js App Router app carries legacy Next.js, ESR, SSR plugin, or React + experience.js wiring. +- **Target result:** The app uses the App Router SDK for request context, server first paint, + browser takeover, and policy-dependent client features. +- **Guide file:** `documentation/guides/migrating-experience-js-next-to-nextjs-app-router.md` +- **Write after:** `choosing-a-nextjs-migration-path-from-experience-js.md`. +- **First verification:** One dynamic App Router route renders an all-visitors variant and verifies + both accepted and denied-consent event paths. + +## Migration route + +| Legacy surface | Target route | Detail owner | +| -------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| Legacy Next provider and tracker | Bound App Router root, handler, and page tracker | [App Router blueprint](../blueprints/nextjs-app-router.md) | +| SSR/ESR profile helpers | App Router request context and profile cookie | [App Router identifiers](../../internal/sdk-knowledge/web/nextjs-app-router.md#identifier-ownership) | +| React render hooks/components | App Router `OptimizedEntry` and browser takeover | [App Router rendering](../../internal/sdk-knowledge/web/nextjs-app-router.md#render--entry-resolution) | +| Legacy plugins | Supplemental plugin migration | `migrating-experience-js-plugins-and-preview.md` | + +## Section plan + +| Section | Purpose | Must route to | Fact sources | +| ----------------------------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Remove legacy Next.js package assumptions | Prevent writers from treating ESR middleware/selector source as supported imports. | Legacy package mapping and unsupported boundaries. | [package mapping](../../internal/migration-knowledge/experience-js.md#package-mapping), [Next.js SSR ESR](../../internal/migration-knowledge/experience-js.md#nextjs-ssr-and-esr), [unsupported boundaries](../../internal/migration-knowledge/experience-js.md#unsupported-or-manual-migration-boundaries) | +| Install and bind the App Router SDK | Restore the App Router request/root shape before replacing render calls. | App Router setup, package, and runtime sections. | [package](../../internal/sdk-knowledge/web/nextjs-app-router.md#package--entry-points), [setup](../../internal/sdk-knowledge/web/nextjs-app-router.md#setup--initialization-and-binding), [runtime](../../internal/sdk-knowledge/web/nextjs-app-router.md#version--runtime-quirks) | +| Replace SSR/ESR profile continuity | Move profile cookie and request evaluation to target request context with app-owned policy. | Request context, identifiers, consent, and first-event ownership. | [Next.js SSR ESR](../../internal/migration-knowledge/experience-js.md#nextjs-ssr-and-esr), [legacy identifiers](../../internal/migration-knowledge/experience-js.md#identifiers-and-persistence), [target identifiers](../../internal/sdk-knowledge/web/nextjs-app-router.md#identifier-ownership), [events](../../internal/sdk-knowledge/web/nextjs-app-router.md#events--tracking) | +| Replace server-rendered personalization | Route legacy mapped experiences and React wrappers to target entry resolution and server first paint. | App Router rendering and content-model migration. | [React legacy](../../internal/migration-knowledge/experience-js.md#react-render-and-hooks), [content model](../../internal/migration-knowledge/experience-js.md#contentful-model-and-mapper), [rendering](../../internal/sdk-knowledge/web/nextjs-app-router.md#render--entry-resolution), [entry resolution](../../internal/sdk-knowledge/shared/concepts.md#entry-resolution) | +| Replace browser takeover features | Move flags, analytics forwarding, preview, and live updates through the target client runtime. | App Router browser/client sections and supplemental plugin guide. | [plugins](../../internal/migration-knowledge/experience-js.md#plugins-and-preview), [flag views](../../internal/sdk-knowledge/shared/concepts.md#custom-flag-views), [event streams](../../internal/sdk-knowledge/shared/concepts.md#stateful-event-forwarding-streams), [preview](../../internal/sdk-knowledge/shared/concepts.md#preview-overrides), [live updates](../../internal/sdk-knowledge/shared/concepts.md#live-updates) | +| Validate App Router migration | Verify server HTML, hydration stability, profile continuity, event ownership, and cache safety. | App Router production checks and troubleshooting. | [runtime](../../internal/sdk-knowledge/web/nextjs-app-router.md#version--runtime-quirks), [fallback](../../internal/sdk-knowledge/web/nextjs-app-router.md#failure--fallback-behavior), [events](../../internal/sdk-knowledge/web/nextjs-app-router.md#events--tracking) | + +## Handoffs + +None. + +## Link roles + +- [Next.js App Router integration guide](../../guides/integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md). +- [Profile synchronization concept](../../concepts/profile-synchronization-between-client-and-server.md). +- [App Router reference implementation](../../../implementations/nextjs-sdk_app-router/README.md). +- [Contentful model migration guide](../../guides/migrating-experience-js-contentful-model-to-optimization.md). +- [Plugin, privacy, analytics, and preview migration guide](../../guides/migrating-experience-js-plugins-and-preview.md). diff --git a/documentation/authoring/migration-blueprints/experience-js-nextjs-pages-router.md b/documentation/authoring/migration-blueprints/experience-js-nextjs-pages-router.md new file mode 100644 index 000000000..9d54148fe --- /dev/null +++ b/documentation/authoring/migration-blueprints/experience-js-nextjs-pages-router.md @@ -0,0 +1,51 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/migrating-experience-js-next-to-nextjs-pages-router.md +--- + +# experience.js Next.js to Pages Router migration blueprint + +## Reader goal + +- **Use when:** A Pages Router app uses `@ninetailed/experience.js-next`, SSR plugin behavior, or + legacy React surfaces. +- **Target result:** The app uses the Pages Router SDK for server data functions, provider wiring, + page tracking, and target entry resolution. +- **Guide file:** `documentation/guides/migrating-experience-js-next-to-nextjs-pages-router.md` +- **Write after:** `choosing-a-nextjs-migration-path-from-experience-js.md`. +- **First verification:** One Pages Router server data path renders an all-visitors variant and + verifies accepted server evaluation plus denied-consent behavior. + +## Migration route + +| Legacy surface | Target route | Detail owner | +| ------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Next provider/tracker | Pages Router provider and page tracker | [Pages Router blueprint](../blueprints/nextjs-pages-router.md) | +| SSR plugin profile continuity | Pages Router request context and profile cookie | [Pages identifiers](../../internal/sdk-knowledge/web/nextjs-pages-router.md#identifier-ownership) | +| React components and hooks | Pages Router `OptimizedEntry` and React Web hooks | [Pages rendering](../../internal/sdk-knowledge/web/nextjs-pages-router.md#render--entry-resolution) | +| Third-party plugin integrations | Supplemental plugin migration | `migrating-experience-js-plugins-and-preview.md` | + +## Section plan + +| Section | Purpose | Must route to | Fact sources | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Inventory legacy Pages Router wiring | Identify provider placement, tracker override, SSR plugin use, and profile cookie assumptions. | Legacy Next.js and identifiers. | [Next.js SSR ESR](../../internal/migration-knowledge/experience-js.md#nextjs-ssr-and-esr), [identifiers](../../internal/migration-knowledge/experience-js.md#identifiers-and-persistence) | +| Install and bind the Pages Router SDK | Establish target package, factory, server props, and provider before rendering changes. | Pages setup and runtime sections. | [package](../../internal/sdk-knowledge/web/nextjs-pages-router.md#package--entry-points), [setup](../../internal/sdk-knowledge/web/nextjs-pages-router.md#setup--initialization-and-binding), [runtime](../../internal/sdk-knowledge/web/nextjs-pages-router.md#version--runtime-quirks) | +| Replace server profile and page evaluation | Map SSR plugin and tracker behavior to target request context and duplicate-event controls. | Pages events, identifiers, and consent. | [legacy SSR](../../internal/migration-knowledge/experience-js.md#nextjs-ssr-and-esr), [target identifiers](../../internal/sdk-knowledge/web/nextjs-pages-router.md#identifier-ownership), [events](../../internal/sdk-knowledge/web/nextjs-pages-router.md#events--tracking), [page events](../../internal/sdk-knowledge/shared/concepts.md#page-events) | +| Replace personalized rendering | Move legacy React components and Contentful mapper output to Pages Router entry resolution. | Pages rendering and content-model migration. | [React legacy](../../internal/migration-knowledge/experience-js.md#react-render-and-hooks), [content model](../../internal/migration-knowledge/experience-js.md#contentful-model-and-mapper), [rendering](../../internal/sdk-knowledge/web/nextjs-pages-router.md#render--entry-resolution), [entry resolution](../../internal/sdk-knowledge/shared/concepts.md#entry-resolution) | +| Replace client-side extras | Route flags, analytics forwarding, preview, and consent diagnostics through React Web client behavior. | Pages/React Web events plus supplemental plugin guide. | [plugins](../../internal/migration-knowledge/experience-js.md#plugins-and-preview), [flag views](../../internal/sdk-knowledge/shared/concepts.md#custom-flag-views), [event streams](../../internal/sdk-knowledge/shared/concepts.md#stateful-event-forwarding-streams), [preview](../../internal/sdk-knowledge/shared/concepts.md#preview-overrides) | +| Validate Pages Router migration | Verify server props, rendered variant/baseline, tracker ownership, profile continuity, and cache boundaries. | Pages production checks and troubleshooting. | [runtime](../../internal/sdk-knowledge/web/nextjs-pages-router.md#version--runtime-quirks), [fallback](../../internal/sdk-knowledge/web/nextjs-pages-router.md#failure--fallback-behavior), [events](../../internal/sdk-knowledge/web/nextjs-pages-router.md#events--tracking) | + +## Handoffs + +None. + +## Link roles + +- [Next.js Pages Router integration guide](../../guides/integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md). +- [Profile synchronization concept](../../concepts/profile-synchronization-between-client-and-server.md). +- [Pages Router reference implementation](../../../implementations/nextjs-sdk_pages-router/README.md). +- [Contentful model migration guide](../../guides/migrating-experience-js-contentful-model-to-optimization.md). +- [Plugin, privacy, analytics, and preview migration guide](../../guides/migrating-experience-js-plugins-and-preview.md). diff --git a/documentation/authoring/migration-blueprints/experience-js-nextjs-router-choice.md b/documentation/authoring/migration-blueprints/experience-js-nextjs-router-choice.md new file mode 100644 index 000000000..13c60badf --- /dev/null +++ b/documentation/authoring/migration-blueprints/experience-js-nextjs-router-choice.md @@ -0,0 +1,49 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/choosing-a-nextjs-migration-path-from-experience-js.md +--- + +# experience.js Next.js router-choice migration blueprint + +## Reader goal + +- **Use when:** A Next.js app uses `@ninetailed/experience.js-next`, + `@ninetailed/experience.js-next-esr`, or the SSR plugin and must choose App Router, Pages Router, + or a manual Node/Web hybrid target. +- **Target result:** The reader picks the right target migration guide before changing code. +- **Guide file:** `documentation/guides/choosing-a-nextjs-migration-path-from-experience-js.md` +- **Write after:** None. +- **First verification:** The reader can name the target router path, first-page-event owner, + cookie/consent owner, and whether request-bound personalization can be dynamic. + +## Migration route + +| Legacy surface | Target route | Detail owner | +| -------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------- | +| Pages-Router-oriented Next package | Pages Router SDK migration | `migrating-experience-js-next-to-nextjs-pages-router.md` | +| App Router application using legacy wrappers | App Router SDK migration | `migrating-experience-js-next-to-nextjs-app-router.md` | +| ESR helpers or SSR plugin | App Router SDK where possible; otherwise manual Node/Web hybrid | This decision guide plus Node/Web migration guides | + +## Section plan + +| Section | Purpose | Must route to | Fact sources | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Identify the current Next.js integration | Classify router, SSR plugin, ESR helpers, and profile cookie usage before target selection. | Legacy Next.js, SSR, ESR, and identifier facts. | [package mapping](../../internal/migration-knowledge/experience-js.md#package-mapping), [Next.js SSR ESR](../../internal/migration-knowledge/experience-js.md#nextjs-ssr-and-esr), [identifiers](../../internal/migration-knowledge/experience-js.md#identifiers-and-persistence) | +| Choose App Router, Pages Router, or manual hybrid | Give a default target by actual app router and supported target SDK package. | Target Next.js package guides and Node/Web fallback. | [App Router package](../../internal/sdk-knowledge/web/nextjs-app-router.md#package--entry-points), [Pages Router package](../../internal/sdk-knowledge/web/nextjs-pages-router.md#package--entry-points), [Node package](../../internal/sdk-knowledge/node/node.md#package--entry-points), [Web package](../../internal/sdk-knowledge/web/web.md#package--entry-points) | +| Route SSR and first page event ownership | Prevent duplicate or missing page evaluation when moving from legacy SSR plugin/tracker behavior. | Target guide sections for request context and first-event ownership. | [Next.js SSR ESR](../../internal/migration-knowledge/experience-js.md#nextjs-ssr-and-esr), [App events](../../internal/sdk-knowledge/web/nextjs-app-router.md#events--tracking), [Pages events](../../internal/sdk-knowledge/web/nextjs-pages-router.md#events--tracking), [page events](../../internal/sdk-knowledge/shared/concepts.md#page-events) | +| Route cookie and profile continuity | Keep continuity decisions explicit across legacy `ntaid` and target app-owned persistence. | Target identifier sections. | [identifiers](../../internal/migration-knowledge/experience-js.md#identifiers-and-persistence), [App identifiers](../../internal/sdk-knowledge/web/nextjs-app-router.md#identifier-ownership), [Pages identifiers](../../internal/sdk-knowledge/web/nextjs-pages-router.md#identifier-ownership) | + +## Handoffs + +None. + +## Link roles + +- [Next.js App Router integration guide](../../guides/integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md). +- [Next.js Pages Router integration guide](../../guides/integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md). +- [Node SDK integration guide](../../guides/integrating-the-node-sdk-in-a-node-app.md). +- [Web SDK integration guide](../../guides/integrating-the-web-sdk-in-a-web-app.md). +- [App Router migration guide](../../guides/migrating-experience-js-next-to-nextjs-app-router.md). +- [Pages Router migration guide](../../guides/migrating-experience-js-next-to-nextjs-pages-router.md). diff --git a/documentation/authoring/migration-blueprints/experience-js-node-ssr-esr.md b/documentation/authoring/migration-blueprints/experience-js-node-ssr-esr.md new file mode 100644 index 000000000..14d56f02d --- /dev/null +++ b/documentation/authoring/migration-blueprints/experience-js-node-ssr-esr.md @@ -0,0 +1,52 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/migrating-experience-js-node-ssr-and-esr.md +--- + +# experience.js Node, SSR, and ESR migration blueprint + +## Reader goal + +- **Use when:** A server integration uses `@ninetailed/experience.js-node`, SSR plugin helpers, ESR + helpers, or a manual server-to-browser handoff. +- **Target result:** The server uses the Node SDK for request-scoped evaluation and hands browser + continuity to the appropriate Web or framework SDK. +- **Guide file:** `documentation/guides/migrating-experience-js-node-ssr-and-esr.md` +- **Write after:** `choosing-a-nextjs-migration-path-from-experience-js.md` for Next.js apps; + otherwise None. +- **First verification:** One accepted request and one denied-consent request prove event results, + rendered content, cache boundaries, and any manual Node/Web cookie handoff. + +## Migration route + +| Legacy surface | Target route | Detail owner | +| ------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `NinetailedAPIClient` | Node SDK request client and event calls | [Node blueprint](../blueprints/node.md) | +| SSR plugin profile handoff | App-owned cookie/profile continuity | [Node identifiers](../../internal/sdk-knowledge/node/node.md#identifier-ownership) | +| ESR helper preflight behavior | App Router SDK where possible; Node/Web hybrid otherwise | Next.js router-choice migration | +| Server-rendered content mapping | Node entry resolution or framework SDK rendering | [Node rendering](../../internal/sdk-knowledge/node/node.md#render--entry-resolution) | + +## Section plan + +| Section | Purpose | Must route to | Fact sources | +| -------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Inventory server profile and event ownership | Identify which server path creates profiles, emits page events, and persists visitor identity. | Legacy Node, SSR, ESR, and identifiers. | [package mapping](../../internal/migration-knowledge/experience-js.md#package-mapping), [Next.js SSR ESR](../../internal/migration-knowledge/experience-js.md#nextjs-ssr-and-esr), [identifiers](../../internal/migration-knowledge/experience-js.md#identifiers-and-persistence) | +| Replace Node API client calls | Move server mutations and event handling to request-scoped Node SDK calls. | Node setup, context, events, and consent sections. | [legacy runtime](../../internal/migration-knowledge/experience-js.md#legacy-runtime-surfaces), [Node setup](../../internal/sdk-knowledge/node/node.md#setup--initialization-and-binding), [Node events](../../internal/sdk-knowledge/node/node.md#events--tracking), [Node consent](../../internal/sdk-knowledge/node/node.md#consent--persistence) | +| Replace SSR and ESR handoff | Pick framework SDK when available and keep manual hybrid as an escape hatch, not the default. | Next.js router-choice guide; Node/Web continuity sections. | [Next.js SSR ESR](../../internal/migration-knowledge/experience-js.md#nextjs-ssr-and-esr), [unsupported boundaries](../../internal/migration-knowledge/experience-js.md#unsupported-or-manual-migration-boundaries), [Node identifiers](../../internal/sdk-knowledge/node/node.md#identifier-ownership), [Web identifiers](../../internal/sdk-knowledge/web/web.md#identifier-ownership) | +| Replace server content resolution | Move from legacy experience-config mapping to target entry resolution and fallback behavior. | Node rendering and content-model migration guide. | [content model](../../internal/migration-knowledge/experience-js.md#contentful-model-and-mapper), [unsupported boundaries](../../internal/migration-knowledge/experience-js.md#unsupported-or-manual-migration-boundaries), [Node rendering](../../internal/sdk-knowledge/node/node.md#render--entry-resolution), [entry resolution](../../internal/sdk-knowledge/shared/concepts.md#entry-resolution) | +| Validate server migration | Verify accepted or blocked events, profile continuity, rendered content, and cache safety. | Node production checks and troubleshooting. | [Node events](../../internal/sdk-knowledge/node/node.md#events--tracking), [Node fallback](../../internal/sdk-knowledge/node/node.md#failure--fallback-behavior), [Node runtime](../../internal/sdk-knowledge/node/node.md#version--runtime-quirks) | + +## Handoffs + +None. + +## Link roles + +- [Node SDK integration guide](../../guides/integrating-the-node-sdk-in-a-node-app.md). +- [Web SDK integration guide](../../guides/integrating-the-web-sdk-in-a-web-app.md). +- [Profile synchronization concept](../../concepts/profile-synchronization-between-client-and-server.md). +- [Node reference implementation](../../../implementations/node-sdk/README.md). +- [Node plus Web reference implementation](../../../implementations/node-sdk+web-sdk/README.md). +- [Next.js migration path decision guide](../../guides/choosing-a-nextjs-migration-path-from-experience-js.md). diff --git a/documentation/authoring/migration-blueprints/experience-js-plugins-preview-privacy.md b/documentation/authoring/migration-blueprints/experience-js-plugins-preview-privacy.md new file mode 100644 index 000000000..05f2f7af9 --- /dev/null +++ b/documentation/authoring/migration-blueprints/experience-js-plugins-preview-privacy.md @@ -0,0 +1,51 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/migrating-experience-js-plugins-and-preview.md +--- + +# experience.js plugins, privacy, analytics, and preview migration blueprint + +## Reader goal + +- **Use when:** An app uses experience.js analytics, privacy, preview, insights, or third-party + plugin packages. +- **Target result:** The app replaces plugin packages with target SDK consent policy, event streams, + preview panel, insights behavior, and vendor-specific forwarding code where needed. +- **Guide file:** `documentation/guides/migrating-experience-js-plugins-and-preview.md` +- **Write after:** At least one runtime migration guide for the app's target runtime. +- **First verification:** The target runtime shows one accepted event, one blocked diagnostic, and + preview behavior without legacy plugin globals. + +## Migration route + +| Legacy surface | Target route | Detail owner | +| --------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| Privacy plugin | App-owned consent plus target SDK consent inputs | Runtime integration guide consent sections | +| Analytics and vendor plugins | Event-stream forwarding or target insights | [Analytics forwarding guide](../../guides/forwarding-optimization-sdk-context-to-analytics-and-tag-management-tools.md) | +| Preview plugin and preview bridge | Optimization preview panel and override behavior | Target runtime preview sections | +| Insights plugin batching | Target event delivery and forwarding seams | Runtime integration guide events sections | + +## Section plan + +| Section | Purpose | Must route to | Fact sources | +| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Inventory plugin packages and globals | Make plugin replacement explicit and prevent one-for-one package assumptions. | Legacy plugin and unsupported-boundary facts. | [plugins](../../internal/migration-knowledge/experience-js.md#plugins-and-preview), [unsupported boundaries](../../internal/migration-knowledge/experience-js.md#unsupported-or-manual-migration-boundaries) | +| Replace privacy plugin behavior | Route consent storage, accepted/blocked behavior, and queue expectations to target app-owned policy. | Runtime consent sections and blocked-event diagnostics. | [legacy identifiers](../../internal/migration-knowledge/experience-js.md#identifiers-and-persistence), [plugins](../../internal/migration-knowledge/experience-js.md#plugins-and-preview), [shared consent](../../internal/sdk-knowledge/shared/concepts.md#consent--persistence), [event streams](../../internal/sdk-knowledge/shared/concepts.md#stateful-event-forwarding-streams) | +| Replace analytics and tag-manager plugins | Route vendor forwarding to the supplemental analytics guide instead of recreating plugin packages. | Event stream forwarding, dedupe, blocked diagnostics, and vendor examples. | [plugins](../../internal/migration-knowledge/experience-js.md#plugins-and-preview), [event streams](../../internal/sdk-knowledge/shared/concepts.md#stateful-event-forwarding-streams), [Web events](../../internal/sdk-knowledge/web/web.md#events--tracking), [React Web events](../../internal/sdk-knowledge/web/react-web.md#events--tracking) | +| Replace preview plugin behavior | Route authoring/debugging behavior to the target preview panel and clearly identify parity boundaries. | Runtime preview sections and shared preview override facts. | [plugins](../../internal/migration-knowledge/experience-js.md#plugins-and-preview), [preview overrides](../../internal/sdk-knowledge/shared/concepts.md#preview-overrides), [Web runtime](../../internal/sdk-knowledge/web/web.md#version--runtime-quirks), [React Web runtime](../../internal/sdk-knowledge/web/react-web.md#version--runtime-quirks) | +| Validate plugin migration | Verify consent-blocked diagnostics, accepted forwarding, preview attachment, and removed legacy globals/packages. | Runtime guide production checks and analytics supplemental validation. | [event streams](../../internal/sdk-knowledge/shared/concepts.md#stateful-event-forwarding-streams), [preview overrides](../../internal/sdk-knowledge/shared/concepts.md#preview-overrides), [Web consent](../../internal/sdk-knowledge/web/web.md#consent--persistence), [React Web consent](../../internal/sdk-knowledge/web/react-web.md#consent--persistence) | + +## Handoffs + +None. + +## Link roles + +- [Analytics forwarding supplemental guide](../../guides/forwarding-optimization-sdk-context-to-analytics-and-tag-management-tools.md). +- [Consent management concept](../../concepts/consent-management-in-the-optimization-sdk-suite.md). +- [Web SDK integration guide](../../guides/integrating-the-web-sdk-in-a-web-app.md). +- [React Web integration guide](../../guides/integrating-the-react-web-sdk-in-a-react-app.md). +- [Next.js App Router integration guide](../../guides/integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md). +- [Next.js Pages Router integration guide](../../guides/integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md). diff --git a/documentation/authoring/migration-blueprints/experience-js-react-web.md b/documentation/authoring/migration-blueprints/experience-js-react-web.md new file mode 100644 index 000000000..185a21938 --- /dev/null +++ b/documentation/authoring/migration-blueprints/experience-js-react-web.md @@ -0,0 +1,52 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/migrating-experience-js-react-to-react-web.md +--- + +# experience.js React to React Web migration blueprint + +## Reader goal + +- **Use when:** A React app uses `@ninetailed/experience.js-react` provider, hooks, components, or + flags. +- **Target result:** The app uses the React Web provider, `OptimizedEntry`, target hooks, and + app-owned consent/identity policy. +- **Guide file:** `documentation/guides/migrating-experience-js-react-to-react-web.md` +- **Write after:** `migrating-experience-js-to-the-web-sdk.md` only if the app also owns a custom + lower-level Web adapter. +- **First verification:** One page event is accepted or intentionally blocked and one all-visitors + `OptimizedEntry` renders a variant or baseline fallback. + +## Migration route + +| Legacy surface | Target route | Detail owner | +| -------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `NinetailedProvider` | React Web `OptimizationProvider` composition | [React Web blueprint](../blueprints/react-web.md) | +| `Personalize`, `Experience`, and hooks | `OptimizedEntry` and target state hooks | [React Web rendering](../../internal/sdk-knowledge/web/react-web.md#render--entry-resolution) | +| Legacy flag hooks | Target flag reads and manual flag-view route | [Shared flag views](../../internal/sdk-knowledge/shared/concepts.md#custom-flag-views) | +| Legacy plugin behavior | Supplemental plugin/privacy/preview migration | `migrating-experience-js-plugins-and-preview.md` | + +## Section plan + +| Section | Purpose | Must route to | Fact sources | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Inventory provider, hooks, and render components | Separate root wiring from component-level replacement work. | Legacy React facts and target provider setup. | [React legacy](../../internal/migration-knowledge/experience-js.md#react-render-and-hooks), [setup](../../internal/sdk-knowledge/web/react-web.md#setup--initialization-and-binding), [components](../../internal/sdk-knowledge/web/react-web.md#components--hooks) | +| Replace the provider and readiness model | Move root setup and async readiness without keeping legacy global assumptions. | React Web provider and readiness sections. | [legacy runtime](../../internal/migration-knowledge/experience-js.md#legacy-runtime-surfaces), [runtime](../../internal/sdk-knowledge/web/react-web.md#version--runtime-quirks), [fallback](../../internal/sdk-knowledge/web/react-web.md#failure--fallback-behavior) | +| Replace personalized entry rendering | Route legacy component/hook rendering to entry resolution and target render props. | React Web entry rendering guide section plus content-model migration guide. | [React legacy](../../internal/migration-knowledge/experience-js.md#react-render-and-hooks), [content model](../../internal/migration-knowledge/experience-js.md#contentful-model-and-mapper), [rendering](../../internal/sdk-knowledge/web/react-web.md#render--entry-resolution) | +| Replace flags and tracking side effects | Make flag-view behavior and interaction tracking explicit instead of preserving hook names. | Flag-view facts, React Web events, and target hooks. | [React legacy](../../internal/migration-knowledge/experience-js.md#react-render-and-hooks), [flag views](../../internal/sdk-knowledge/shared/concepts.md#custom-flag-views), [events](../../internal/sdk-knowledge/web/react-web.md#events--tracking) | +| Replace consent, identity, analytics, and preview | Keep policy/vendor-specific work out of the core render migration. | React Web policy sections and supplemental plugin guide. | [identifiers](../../internal/migration-knowledge/experience-js.md#identifiers-and-persistence), [plugins](../../internal/migration-knowledge/experience-js.md#plugins-and-preview), [consent](../../internal/sdk-knowledge/web/react-web.md#consent--persistence), [preview](../../internal/sdk-knowledge/shared/concepts.md#preview-overrides) | +| Validate React migration | Verify provider placement, rendered variant/baseline, event stream, and absence of legacy wrappers. | React Web production checks and troubleshooting. | [events](../../internal/sdk-knowledge/web/react-web.md#events--tracking), [fallback](../../internal/sdk-knowledge/web/react-web.md#failure--fallback-behavior), [event streams](../../internal/sdk-knowledge/shared/concepts.md#stateful-event-forwarding-streams) | + +## Handoffs + +None. + +## Link roles + +- [React Web integration guide](../../guides/integrating-the-react-web-sdk-in-a-react-app.md). +- [Web SDK integration guide](../../guides/integrating-the-web-sdk-in-a-web-app.md). +- [React Web reference implementation](../../../implementations/react-web-sdk/README.md). +- [Contentful model migration guide](../../guides/migrating-experience-js-contentful-model-to-optimization.md). +- [Plugin, privacy, analytics, and preview migration guide](../../guides/migrating-experience-js-plugins-and-preview.md). diff --git a/documentation/authoring/migration-blueprints/experience-js-web.md b/documentation/authoring/migration-blueprints/experience-js-web.md new file mode 100644 index 000000000..20aa1c4d5 --- /dev/null +++ b/documentation/authoring/migration-blueprints/experience-js-web.md @@ -0,0 +1,49 @@ +--- +migration: experience-js +archetype: migration +source: ../../internal/migration-knowledge/experience-js.md +guide: ../../guides/migrating-experience-js-to-the-web-sdk.md +--- + +# experience.js to Web SDK migration blueprint + +## Reader goal + +- **Use when:** A plain browser or custom JavaScript adapter uses `@ninetailed/experience.js`. +- **Target result:** The app owns one Optimization Web SDK instance, emits page events, resolves + Contentful entries, and replaces legacy plugin behavior through current SDK seams. +- **Guide file:** `documentation/guides/migrating-experience-js-to-the-web-sdk.md` +- **Write after:** None. +- **First verification:** One accepted page event populates selections and one all-visitors + Optimization-authored entry renders before policy-dependent replacements. + +## Migration route + +| Legacy surface | Target route | Detail owner | +| --------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------ | +| Browser `Ninetailed` client | Web SDK setup, lifecycle, events, and state subscriptions | [Web integration blueprint](../blueprints/web.md) | +| Legacy profile globals and storage keys | Target identifier and persistence policy | [Web SDK KB](../../internal/sdk-knowledge/web/web.md#identifier-ownership) | +| `Personalize`-like manual rendering | Entry resolution and Web Components where useful | [Web SDK KB](../../internal/sdk-knowledge/web/web.md#render--entry-resolution) | +| Plugin analytics, privacy, and preview | Supplemental migration guides | `migrating-experience-js-plugins-and-preview.md` | + +## Section plan + +| Section | Purpose | Must route to | Fact sources | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Inventory the legacy browser integration | Help the reader find the single client, event calls, plugins, and Contentful resolution boundary before editing. | Legacy client, storage, and package mapping; target Web setup. | [package mapping](../../internal/migration-knowledge/experience-js.md#package-mapping), [legacy runtime](../../internal/migration-knowledge/experience-js.md#legacy-runtime-surfaces), [setup](../../internal/sdk-knowledge/web/web.md#setup--initialization-and-binding) | +| Replace client construction and lifecycle | Move from legacy client/plugin construction to one Web SDK instance without preserving legacy plugin arrays. | Web SDK integration guide lifecycle sections. | [legacy runtime](../../internal/migration-knowledge/experience-js.md#legacy-runtime-surfaces), [Web runtime](../../internal/sdk-knowledge/web/web.md#version--runtime-quirks) | +| Replace page, track, identify, and reset calls | Map imperative event calls and identity lifecycle to target event and consent ownership. | Web events, consent, identity, and reset sections. | [legacy runtime](../../internal/migration-knowledge/experience-js.md#legacy-runtime-surfaces), [identifiers](../../internal/migration-knowledge/experience-js.md#identifiers-and-persistence), [events](../../internal/sdk-knowledge/web/web.md#events--tracking), [consent](../../internal/sdk-knowledge/web/web.md#consent--persistence) | +| Replace entry and flag rendering | Move legacy mapped experience configs and flag reads to Optimization content-model entry resolution and flag APIs. | Web rendering plus content-model migration guide for authored data. | [content model](../../internal/migration-knowledge/experience-js.md#contentful-model-and-mapper), [unsupported boundaries](../../internal/migration-knowledge/experience-js.md#unsupported-or-manual-migration-boundaries), [rendering](../../internal/sdk-knowledge/web/web.md#render--entry-resolution), [flag views](../../internal/sdk-knowledge/shared/concepts.md#custom-flag-views) | +| Validate browser migration | Verify rendered variant or baseline, accepted events, blocked diagnostics, and removal of legacy packages. | Web production checks and troubleshooting. | [events](../../internal/sdk-knowledge/web/web.md#events--tracking), [fallback](../../internal/sdk-knowledge/web/web.md#failure--fallback-behavior), [event streams](../../internal/sdk-knowledge/shared/concepts.md#stateful-event-forwarding-streams) | + +## Handoffs + +None. + +## Link roles + +- [Web SDK integration guide](../../guides/integrating-the-web-sdk-in-a-web-app.md). +- [Analytics forwarding supplemental guide](../../guides/forwarding-optimization-sdk-context-to-analytics-and-tag-management-tools.md). +- [Vanilla Web reference implementation](../../../implementations/web-sdk/README.md). +- [Plugin, privacy, analytics, and preview migration guide](../../guides/migrating-experience-js-plugins-and-preview.md). +- [Contentful model migration guide](../../guides/migrating-experience-js-contentful-model-to-optimization.md). diff --git a/documentation/authoring/recipes/migration.md b/documentation/authoring/recipes/migration.md new file mode 100644 index 000000000..872f5c1ac --- /dev/null +++ b/documentation/authoring/recipes/migration.md @@ -0,0 +1,83 @@ +--- +archetype: migration +filename: migrating-*.md +--- + +## Context + +For guides that move an existing integration from a legacy SDK to the Optimization SDK Suite. The +reader goal is not a clean install; it is replacing a working surface without preserving obsolete +architecture. The `guide-writer` renders the **Template** below; it never emits this Context. + +Use this recipe when an integration guide is too start-from-zero and a supplemental recipe is too +narrow. Keep the guide scoped to one runtime or one cross-runtime migration concern. Do not collapse +browser, React, Next.js, server, plugins, and content-model migration into one guide. + +The migration blueprint owns the guide file, source package route, target guide route, section plan, +fact routing, and unresolved handoffs. Legacy facts come from the migration knowledge base. Target +SDK facts come from the SDK knowledge base. Interface shapes still come from package source/types +while writing. + +Migration guides must make ownership changes visible before code changes. The reader should know +which layer owns Contentful authoring, runtime configuration, consent records, cookies, first page +events, analytics forwarding, preview behavior, and cache boundaries before deleting legacy packages. +Route those decisions to target guides, concepts, or supplemental migration guides instead of +recreating SDK facts here. + +For runtime migrations, put content-model prerequisites before rendering work when the app depends +on legacy `nt_*` fields, mapper output, or runtime experience configuration arrays. The first +performable content check should use one authored all-visitors or otherwise always-matching variant +so the reader can separate authoring/setup failures from audience-rule failures. + +For event, consent, and analytics migrations, require both accepted-event and blocked-event +observability when the target runtime supports it. Link runtime-specific consent sections rather +than generalizing consent mechanics in the migration guide. Blocked diagnostics are evidence, not a +replay queue. + +For server, framework, and hybrid migrations, require the guide to name the owner of the first page +event, the target profile-cookie boundary, and any per-request dynamic or cache consequence before +the rendering replacement. Manual Node/Web hybrids are app-owned at the cookie boundary; target +framework adapters may own more of that handoff. + +## Template + +# ⟨Migration guide title⟩ + +Use this guide when ⟨the legacy integration being replaced and the target result⟩. + +## What changes + +⟨A short routing summary: source package(s), target package(s), and the main ownership change. Link +to the target integration guide before detail.⟩ + +## Before you migrate + +⟨Inputs the reader gathers from the existing app and Contentful space: package usage, rendered +surfaces, consent/analytics policy, profile continuity, authored content, target SDK values, +configuration ownership, first-event ownership, and request/cache constraints.⟩ + +## Migration path + +⟨A numbered path that removes legacy wiring, installs target packages, restores the first working +result, then adds policy-dependent replacements. If legacy Contentful mapper output is involved, +route to content-model migration before runtime rendering. Each step links to the target integration +or supplemental guide when that guide owns the detailed procedure.⟩ + +## Replace legacy surfaces + +⟨Sections from the blueprint. Each section names the legacy surface, the target surface or manual +replacement, and the verification the reader should perform.⟩ + +## Validate the migration + +⟨Checks for target rendering, an authored all-visitors or always-matching variant, fallback, accepted +and blocked event evidence, consent, profile continuity, first-event ownership, cache/dynamic +boundaries, analytics forwarding, preview behavior, and dead legacy package removal as applicable.⟩ + +## Troubleshooting + +⟨Only reader-observable migration failures from the blueprint.⟩ + +## Related guides + +⟨Target integration guides, supplemental guides, and concepts.⟩ diff --git a/documentation/guides/README.md b/documentation/guides/README.md index ba768bcab..8c82c9500 100644 --- a/documentation/guides/README.md +++ b/documentation/guides/README.md @@ -7,6 +7,14 @@ children: - ./integrating-the-react-web-sdk-in-a-react-app.md - ./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md - ./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md + - ./migrating-experience-js-contentful-model-to-optimization.md + - ./migrating-experience-js-to-the-web-sdk.md + - ./migrating-experience-js-react-to-react-web.md + - ./choosing-a-nextjs-migration-path-from-experience-js.md + - ./migrating-experience-js-next-to-nextjs-app-router.md + - ./migrating-experience-js-next-to-nextjs-pages-router.md + - ./migrating-experience-js-node-ssr-and-esr.md + - ./migrating-experience-js-plugins-and-preview.md - ./integrating-the-react-native-sdk-in-a-react-native-app.md - ./integrating-the-optimization-ios-sdk-in-a-swiftui-app.md - ./integrating-the-optimization-ios-sdk-in-a-uikit-app.md @@ -53,6 +61,23 @@ Native and mobile SDK guides route to beta SDK surfaces. | [Android SDK in Jetpack Compose](./integrating-the-optimization-android-sdk-in-a-compose-app.md) | Native Android app built with Jetpack Compose | `com.contentful.java:optimization-android` | | [Android SDK in Android Views](./integrating-the-optimization-android-sdk-in-a-views-app.md) | Native Android app built with Android Views or XML layouts | `com.contentful.java:optimization-android` | +## experience.js migration guides + +Use these guides when an app already uses `@ninetailed/experience.js` packages and you are replacing +that integration with the Optimization SDK Suite. Start with the Contentful model guide when legacy +`nt_*` fields or mapper utilities feed personalized rendering. + +| Guide | Legacy integration type | Target path | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------- | +| [Contentful model migration](./migrating-experience-js-contentful-model-to-optimization.md) | Legacy Ninetailed-authored entries, `nt_*` fields, mapper utilities | Optimization-authored experiences and target entry resolution | +| [Web SDK migration](./migrating-experience-js-to-the-web-sdk.md) | Plain browser, static site, SPA, Angular, or custom JavaScript runtime | `@contentful/optimization-web` | +| [React Web migration](./migrating-experience-js-react-to-react-web.md) | React provider, hook, component, or flag surfaces | `@contentful/optimization-react-web` | +| [Next.js migration path choice](./choosing-a-nextjs-migration-path-from-experience-js.md) | Legacy Next.js, SSR plugin, ESR, or manual hybrid wiring | App Router, Pages Router, or manual Node/Web path | +| [Next.js App Router migration](./migrating-experience-js-next-to-nextjs-app-router.md) | Next.js app moving to App Router | `@contentful/optimization-nextjs/app-router` | +| [Next.js Pages Router migration](./migrating-experience-js-next-to-nextjs-pages-router.md) | Pages Router app with legacy SSR/plugin wiring | `@contentful/optimization-nextjs/pages-router` | +| [Node, SSR, and ESR migration](./migrating-experience-js-node-ssr-and-esr.md) | Server API-client, SSR, ESR, or manual handoff code | Node SDK, framework SDK, or manual Node/Web hybrid | +| [Plugins and preview migration](./migrating-experience-js-plugins-and-preview.md) | Legacy privacy, analytics, insights, or preview plugins | Target consent, event streams, forwarding, and preview panel | + ## Supplemental guides - [Building a custom JavaScript Optimization adapter](./building-a-custom-javascript-optimization-adapter.md) - diff --git a/documentation/guides/choosing-a-nextjs-migration-path-from-experience-js.md b/documentation/guides/choosing-a-nextjs-migration-path-from-experience-js.md new file mode 100644 index 000000000..aedb811db --- /dev/null +++ b/documentation/guides/choosing-a-nextjs-migration-path-from-experience-js.md @@ -0,0 +1,127 @@ +# Choosing a Next.js migration path from experience.js + +Use this guide when a Next.js app uses `@ninetailed/experience.js-next`, +`@ninetailed/experience.js-next-esr`, or SSR plugin behavior and you need to choose App Router, +Pages Router, or a manual Node/Web hybrid target before changing code. + +## What changes + +Legacy Next.js surfaces mix React provider behavior, route tracking, SSR profile continuity, and ESR +helpers. The Optimization SDK Suite splits the target by actual runtime: + +- App Router apps use `@contentful/optimization-nextjs/app-router`. +- Pages Router apps use `@contentful/optimization-nextjs/pages-router` and + `@contentful/optimization-nextjs/pages-router/server`. +- Non-Next or unsupported server-rendering shapes use `@contentful/optimization-node` on the server + plus Web or React Web in the browser. + +After choosing, follow the target migration guide instead of mixing router patterns. + +## Before you migrate + +Gather these inputs: + +- Whether the app renders through `app/`, `pages/`, or both. +- Use of `@ninetailed/experience.js-next`, `@ninetailed/experience.js-next-esr`, SSR plugin helpers, + route trackers, or `ntaid`. +- Where the first page event is emitted today: server, browser tracker, or both. +- Where visitor identity is persisted and whether the browser must continue the same profile. +- Whether the target route can be per-request dynamic. + +Use these terms consistently: + +- App Router means routes under `app/`, including Server Components and route handlers. +- Pages Router means routes under `pages/`, especially pages personalized in `getServerSideProps`. +- SSR plugin means legacy experience.js server-side profile and cookie behavior. +- ESR means legacy edge-side rendering helpers from `@ninetailed/experience.js-next-esr`. +- Manual Node/Web hybrid means the app uses the Node SDK on a custom server boundary and the Web or + React Web SDK in the browser. + +## Migration path + +1. Classify the current router and legacy SSR/ESR surfaces. +2. Choose the target package by the route that owns personalization. +3. Decide which layer owns the first page event. +4. Decide how profile continuity moves from `ntaid` to the target `ctfl-opt-aid` policy. +5. Follow the selected runtime migration guide. + +## Replace legacy surfaces + +### Identify the current Next.js integration + +Classify the app by the route that renders personalized content: + +- Use the App Router path when personalized content lives in `app/` routes or Server Components. +- Use the Pages Router path when personalized pages use `pages/` and `getServerSideProps`. +- Use a manual Node/Web hybrid only when the app has a custom server-rendering boundary that the + Next.js adapters do not cover. + +Do not treat unexported ESR middleware or selector source as supported import surfaces. Some ESR +helper files exist in the legacy package source but are not exported from the package entry, so they +were not stable import contracts. If the legacy integration depended on ESR helpers, prefer the App +Router SDK when the route can move there; otherwise treat the replacement as a manual Node/Web +handoff. + +### Choose App Router, Pages Router, or manual hybrid + +Decide whether personalized first paint may be per-request dynamic before choosing the adapter. App +Router server personalization reads request data and makes the affected route dynamic, so it is not +compatible with routes that must stay SSG or ISR. Pages Router `getServerSideProps` is already +per-request. A manual Node/Web hybrid has the same cache responsibility as any custom SSR path: +never share personalized output across visitors. + +| Current app shape | Target | +| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| App Router owns the personalized route | [App Router migration](./migrating-experience-js-next-to-nextjs-app-router.md) | +| Pages Router and `getServerSideProps` own the personalized route | [Pages Router migration](./migrating-experience-js-next-to-nextjs-pages-router.md) | +| Custom SSR outside the Next.js adapters | [Node, SSR, and ESR migration](./migrating-experience-js-node-ssr-and-esr.md), then Web or React Web browser migration | + +Use the highest-level adapter that matches the app. The adapter owns the framework request, +provider, tracker, state handoff, and cookie mechanics that a manual hybrid would otherwise need to +rebuild. + +### Route SSR and first page event ownership + +Avoid duplicate page evaluation. Legacy Next tracking emitted page events on the first route and on +route changes, while SSR helpers could also evaluate the first request. + +In the target App Router path, the configured request handler can own the first page event for +matching requests. In the target Pages Router path, `getServerSideOptimizationProps()` can own the +first page event and pass an `initialPageEvent` value to the browser tracker. If the server did not +report a consented view, the browser tracker can emit the initial page event. + +### Route cookie and profile continuity + +Legacy continuity commonly used `ntaid`. Target Web, React Web, and Next.js browser/framework SDKs +use `ctfl-opt-aid` for the SDK-owned anonymous profile cookie. In a manual Node/Web hybrid, the Node +SDK only exports the `ANONYMOUS_ID_COOKIE` constant; app code must read, write, and clear that +cookie and pass the profile ID through `forRequest({ profile })`. Decide whether migration resets +visitor identity or whether the app reads the legacy cookie and writes the target continuity value +as a one-time operational handoff. + +The target consent record remains app-owned. Do not reuse `__nt-consent__` as if it were an SDK +contract. + +## Validate the migration + +- The selected guide matches the route that renders personalized content. +- Exactly one layer owns the first page event for the first route. +- The browser tracker skips or emits the first route intentionally. +- Cookie and consent ownership are documented in app code before deleting legacy packages. + +## Troubleshooting + +| Symptom | Check | +| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Both server and browser emit the first page event | Use the target guide's initial-page-event skip path when server evaluation succeeded. | +| App Router route no longer behaves statically | Server personalization reads request data and makes the route dynamic; use Pages Router or a browser-only path if static output is required. | +| ESR migration has no matching import | The legacy ESR package did not export every helper present in source; use App Router or a manual Node/Web hybrid. | + +## Related guides + +- [Migrating experience.js Next.js to App Router](./migrating-experience-js-next-to-nextjs-app-router.md) +- [Migrating experience.js Next.js to Pages Router](./migrating-experience-js-next-to-nextjs-pages-router.md) +- [Migrating experience.js Node, SSR, and ESR](./migrating-experience-js-node-ssr-and-esr.md) +- [Next.js App Router integration guide](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md) +- [Next.js Pages Router integration guide](./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md) +- [Profile synchronization between client and server](../concepts/profile-synchronization-between-client-and-server.md) diff --git a/documentation/guides/migrating-experience-js-contentful-model-to-optimization.md b/documentation/guides/migrating-experience-js-contentful-model-to-optimization.md new file mode 100644 index 000000000..38b469ae9 --- /dev/null +++ b/documentation/guides/migrating-experience-js-contentful-model-to-optimization.md @@ -0,0 +1,145 @@ +# Migrating an experience.js Contentful model to Optimization + +Use this guide when a Contentful space uses Ninetailed-authored experience entries, legacy `nt_*` +fields, or `@ninetailed/experience.js-utils-contentful` mapper code and you need the authored +content ready for Optimization SDK rendering. + +## What changes + +The legacy mapper turns Ninetailed Contentful entries into runtime experience configuration arrays. +The Optimization SDK Suite reads Optimization-authored links from the entries you fetch and resolves +the entry at render time. Runtime guides own rendering; this guide owns the authoring inventory and +handoff. + +Before changing runtime code, read the target guide for your app: + +- [Web SDK](./integrating-the-web-sdk-in-a-web-app.md) +- [React Web SDK](./integrating-the-react-web-sdk-in-a-react-app.md) +- [Next.js App Router SDK](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md) +- [Next.js Pages Router SDK](./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md) +- [Node SDK](./integrating-the-node-sdk-in-a-node-app.md) + +## Before you migrate + +Gather these inputs from your existing app and Contentful space: + +- The content types and entries that contain legacy `nt_name`, `nt_description`, `nt_type`, + `nt_config`, `nt_audience`, `nt_variants`, `nt_experience_id`, or `nt_experiences` fields. +- Every call to `ExperienceMapper`, `mapCustomExperience`, async custom mappers, or code that builds + `ExperienceConfiguration` arrays. +- The baseline entries your app renders, the variants attached to them, and the audience rules that + must still apply after migration. +- The target runtime and guide that will render those entries. +- A first verification target: one baseline entry, one Optimization experience linked from that + baseline, and one variant entry. Configure that test experience so every visitor is eligible, so + baseline fallback and a missing authored variant are easy to tell apart. + +There is no SDK-provided content migration codemod or official field mapping for converting +Ninetailed-authored entries to the Optimization content model. Treat the content migration as an +operational authoring task in Contentful, then verify it through the target SDK. + +## Migration path + +1. Inventory the legacy authored entries and mapper usage. +2. Author the target Optimization experiences and variants in Contentful. +3. Stop producing legacy runtime configuration arrays from `nt_config`. +4. Update runtime code to pass fetched Contentful entries to the target SDK's entry-resolution path. +5. Verify one authored all-visitors variant in the target runtime. +6. Remove `@ninetailed/experience.js-utils-contentful` after no runtime path imports it. + +## Replace legacy surfaces + +### Inventory legacy authored entries and mapper usage + +Find the entries and code paths that make personalization depend on the legacy model: + +- Baseline entries with `fields.nt_experiences`. +- Experience entries with `nt_config`, `nt_audience`, `nt_variants`, and `nt_experience_id`. +- Mapper code that converts traffic, distribution, audiences, or replacement components into + runtime objects. +- Custom variant mapping that adds application fields before rendering. + +Do not preserve `ExperienceConfiguration` arrays as the target data model. In Optimization, a +baseline entry links to Optimization experience entries through SDK-owned fields, and matching +experiences link to replacement entries. If the links are absent or unresolved in the fetched +payload, the SDK returns the baseline entry. + +### Plan target Optimization authoring + +Use Contentful personalization authoring to create the target experiences and variants. For the +minimum first proof, identify one legacy baseline entry, create or reuse one target baseline entry, +link one Optimization experience from the baseline entry's `nt_experiences` field, and link one +replacement entry from that experience's `nt_variants` field. Configure that experience with no +restrictive audience, or with an audience rule that matches every visitor in your test route. + +Keep the runtime expectation simple: + +- Fetch one concrete locale, such as `en-US`; do not use all-locale payloads. +- Include linked entries deeply enough for the page or section you render. The default managed + entry-fetching path uses `include: 10`; for manual fetching, set an equivalent include depth in + your app's Contentful query. +- Keep the Contentful client in the app. With manual fetching, your app fetches the baseline entry + and passes it as `baselineEntry` or to `resolveOptimizedEntry()`. With managed fetching, your app + gives the SDK its `contentful.js` client and the SDK fetches explicit entry IDs for the supported + target runtime. + +For the first verification, choose one baseline entry and one variant that targets all visitors. +That proves the authoring links and runtime resolution before you migrate more rules. + +### Replace mapper-dependent runtime code + +Remove mapper output from the rendering path. Replace code that passes mapped experience arrays to +experience.js components with the target runtime's entry-resolution surface: + +- Web: [Resolving entries and rendering the result](./integrating-the-web-sdk-in-a-web-app.md#resolving-entries-and-rendering-the-result) + or [Web Components entry rendering](./integrating-the-web-sdk-in-a-web-app.md#web-components-entry-rendering). +- React Web: + [Resolving entries and rendering the result](./integrating-the-react-web-sdk-in-a-react-app.md#resolving-entries-and-rendering-the-result) + with `OptimizedEntry`, `useOptimizedEntry`, or `resolveOptimizedEntry()`. +- Next.js App Router: + [Personalizing first paint on the server](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md#personalizing-first-paint-on-the-server). +- Next.js Pages Router: + [Personalizing entries](./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md#personalizing-entries) + and + [managed entry rendering](./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md#personalizing-entries). +- Node: + [Fetch and resolve Contentful entries](./integrating-the-node-sdk-in-a-node-app.md#fetch-and-resolve-contentful-entries). + +If custom mapper code added application-specific fields, keep that adaptation outside the SDK +handoff. Fetch or transform the app's own content first, then pass the baseline Contentful entry to +the target resolver. + +### Validate content migration before code migration + +Before migrating the full runtime, verify the first authored entry can resolve in the target guide's +quick path: + +- The baseline entry renders when no visitor matches or links are missing. +- The all-visitors variant renders when the target guide emits the required page or request event. +- The Contentful fetch uses one locale and includes linked Optimization entries. +- No runtime code still imports the legacy Contentful mapper. + +## Validate the migration + +- Search the app for `@ninetailed/experience.js-utils-contentful`, `ExperienceMapper`, `nt_config`, + and runtime `ExperienceConfiguration` construction. +- Verify one target runtime renders both baseline fallback and an authored variant. +- Confirm the same entry is fetched with a concrete locale and linked entries included. +- Confirm authors know which entries remain to be migrated in Contentful before the next runtime + surface moves. + +## Troubleshooting + +| Symptom | Check | +| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| The target runtime always renders baseline | Confirm the fetched baseline entry contains linked Optimization experiences and variant entries, and that the test experience targets the visitor. | +| The runtime crashes or returns empty fields | Confirm the app did not fetch all locales and that linked entries are resolved in the payload. | +| Mapper removal breaks custom fields | Move custom application adaptation before or after SDK entry resolution; do not rebuild legacy experience arrays. | + +## Related guides + +- [Entry personalization and variant resolution](../concepts/entry-personalization-and-variant-resolution.md) +- [Web SDK integration guide](./integrating-the-web-sdk-in-a-web-app.md) +- [React Web integration guide](./integrating-the-react-web-sdk-in-a-react-app.md) +- [Node SDK integration guide](./integrating-the-node-sdk-in-a-node-app.md) +- [Contentful personalization authoring](https://www.contentful.com/developers/docs/personalization/) diff --git a/documentation/guides/migrating-experience-js-next-to-nextjs-app-router.md b/documentation/guides/migrating-experience-js-next-to-nextjs-app-router.md new file mode 100644 index 000000000..e412e4531 --- /dev/null +++ b/documentation/guides/migrating-experience-js-next-to-nextjs-app-router.md @@ -0,0 +1,135 @@ +# Migrating experience.js Next.js to the App Router SDK + +Use this guide when a Next.js App Router app carries legacy Next.js, ESR, SSR plugin, or React +experience.js wiring and you want to move to `@contentful/optimization-nextjs/app-router`. + +## What changes + +The App Router SDK provides bound server and client components for request context, server first +paint, browser takeover, route tracking, and entry resolution. Legacy Next provider, tracker, SSR +plugin, ESR helper, React component, and plugin behavior should be replaced by the App Router SDK +plus the shared migration guides. + +Start with the +[Next.js App Router integration guide](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md). + +## Before you migrate + +Gather these inputs: + +- Current `@ninetailed/experience.js-next`, `@ninetailed/experience.js-next-esr`, SSR plugin, and + React imports. +- Existing middleware, proxy, route tracker, and page-event ownership. +- Use of `ntaid` or browser legacy storage for profile continuity. +- Personalized Server Components, Client Components, and Contentful fetches. +- Consent policy and whether server personalization may make affected routes dynamic. + +## Migration path + +1. Confirm this app should use App Router through + [Choosing a Next.js migration path from experience.js](./choosing-a-nextjs-migration-path-from-experience-js.md). + Then open the + [App Router integration quick start](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md#quick-start) + and make the request handler run before replacing render components. +2. Migrate authored Contentful entries when legacy mapper output is still required. See + [Migrating an experience.js Contentful model to Optimization](./migrating-experience-js-contentful-model-to-optimization.md). +3. Install and bind the App Router SDK. +4. Replace SSR/ESR profile continuity with target request context and cookie behavior. +5. Replace server-rendered personalization with bound `OptimizedEntry`. +6. Replace browser takeover features and plugins through target client surfaces. +7. Remove legacy Next, ESR, React, and plugin packages after imports are gone. + +## Replace legacy surfaces + +### Remove legacy Next.js package assumptions + +Do not carry forward package-root or ESR helper assumptions. The target App Router import path is +`@contentful/optimization-nextjs/app-router`, with browser hooks from +`@contentful/optimization-nextjs/client`. Legacy ESR middleware and selector files are not supported +public import surfaces. + +Remove legacy tracker and provider wiring before adding the bound target root, so one route owner is +responsible for server data and browser tracking. + +### Install and bind the App Router SDK + +Create bound App Router components once using the target integration guide. Configure the request +handler file and export in +[Request context and the profile cookie](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md#request-context-and-the-profile-cookie): +Next.js 16 uses `proxy.ts` with a `proxy` export, while Next.js 15 uses `middleware.ts` with a +`middleware` export. + +With `server.enabled: true`, a missing or misnamed handler is not a baseline fallback path; the +bound root expects server data and throws when the handler did not run. Verify the handler runs by +loading a matched route and confirming the root no longer reports missing server optimization data +before replacing many render surfaces. + +### Replace SSR/ESR profile continuity + +Move profile continuity to the App Router request context and target SDK cookie behavior. The target +profile cookie is `ctfl-opt-aid`; it must be browser-readable so browser takeover can continue the +same visitor. The app still owns the consent record and the server consent resolver. + +When the configured request handler reports the initial page view, pass +`initialPageEvent="skip"` to `NextAppAutoPageTracker` so the browser tracker skips the duplicate. +When server consent denies the event or a route is browser-owned, use `initialPageEvent="emit"` and +let the browser tracker emit according to policy. + +### Replace server-rendered personalization + +Replace legacy mapped experiences and React wrappers with the bound App Router `OptimizedEntry`. +Use `baselineEntry` when your app fetched the entry manually. Use `entryId` only when the factory +has `contentful` config for managed server fetching; if the browser must continue that managed +entry, also provide matching `prefetchManagedEntries` or `prefetchedManagedEntries` on the bound +root. The server component resolves against the current request's selected optimizations and returns +the variant or baseline. + +Server personalization reads request data and makes the route dynamic. Do not keep ISR or static +assumptions on routes that render request-specific personalized output. + +### Replace browser takeover features + +Client-side flags, analytics forwarding, preview, and live updates use the React Web runtime behind +the App Router SDK: + +- Flag reads auto-attempt flag-view tracking when consent and profile state allow it. +- Accepted and blocked event streams are available on the live client SDK. +- Preview panel attachment is a browser concern and forces live updates while open. +- Legacy plugin behavior moves to + [Migrating experience.js plugins and preview](./migrating-experience-js-plugins-and-preview.md). + +### Validate App Router migration + +Verify server HTML, hydration, and browser takeover together: + +- The request handler runs for the personalized route. +- Server-rendered content uses the expected variant or baseline. +- Browser hydration does not briefly revert to empty optimization state. +- The browser tracker skips the first page event when the server already reported it. +- Personalized HTML and resolved outputs are not shared across visitors through caching. + +## Validate the migration + +- Search for remaining `@ninetailed/experience.js-next`, `@ninetailed/experience.js-next-esr`, and + legacy React imports. +- Verify one all-visitors variant on a dynamic App Router route. +- Verify denied consent and accepted consent event paths. +- Verify preview and analytics forwarding only after the core route works. + +## Troubleshooting + +| Symptom | Check | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| The bound root throws instead of showing baseline | Confirm the App Router request handler file and export match your Next.js version and route matcher. | +| The route conflicts with static generation | Server personalization is request-specific; use a dynamic route or move personalization to a browser-only path. | +| Hydration changes the entry | Pass the same baseline entry path or matching managed-entry handoff to the browser. | +| Duplicate page events appear | Skip the browser initial page event when the server already reported the first view. | + +## Related guides + +- [Next.js App Router integration guide](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md) +- [Choosing a Next.js migration path from experience.js](./choosing-a-nextjs-migration-path-from-experience-js.md) +- [Migrating an experience.js Contentful model to Optimization](./migrating-experience-js-contentful-model-to-optimization.md) +- [Migrating experience.js plugins and preview](./migrating-experience-js-plugins-and-preview.md) +- [Profile synchronization between client and server](../concepts/profile-synchronization-between-client-and-server.md) +- [App Router reference implementation](../../implementations/nextjs-sdk_app-router/README.md) diff --git a/documentation/guides/migrating-experience-js-next-to-nextjs-pages-router.md b/documentation/guides/migrating-experience-js-next-to-nextjs-pages-router.md new file mode 100644 index 000000000..b925d931f --- /dev/null +++ b/documentation/guides/migrating-experience-js-next-to-nextjs-pages-router.md @@ -0,0 +1,131 @@ +# Migrating experience.js Next.js to the Pages Router SDK + +Use this guide when a Pages Router app uses `@ninetailed/experience.js-next`, SSR plugin behavior, +or legacy React surfaces and you want to move to the Optimization Pages Router SDK. + +## What changes + +The Pages Router target uses `@contentful/optimization-nextjs/pages-router` for browser components +and `@contentful/optimization-nextjs/pages-router/server` for `getServerSideProps`. Server props +own request evaluation and profile continuity; the browser root receives server state and continues +with React Web behavior. + +Start with the +[Next.js Pages Router integration guide](./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md). + +## Before you migrate + +Gather these inputs: + +- Provider and tracker placement in `_app.tsx`. +- Every `getServerSideProps` path that uses SSR plugin behavior or `ntaid`. +- Legacy React components, hooks, flags, and mapper-dependent entries. +- Consent cookie, profile cookie, and initial page-event behavior. +- Any analytics, privacy, preview, or insights plugins. + +## Migration path + +1. Confirm this app should use Pages Router through + [Choosing a Next.js migration path from experience.js](./choosing-a-nextjs-migration-path-from-experience-js.md). +2. Migrate authored Contentful entries when legacy mapper output is still required. See + [Migrating an experience.js Contentful model to Optimization](./migrating-experience-js-contentful-model-to-optimization.md). +3. Install and bind the Pages Router client and server factories. +4. Replace SSR plugin profile and page evaluation in `getServerSideProps`. +5. Replace personalized rendering with the bound Pages Router `OptimizedEntry`. +6. Replace client-side extras through React Web behavior and the plugin migration guide. +7. Remove legacy Next, React, and plugin packages after imports are gone. + +## Replace legacy surfaces + +### Inventory legacy Pages Router wiring + +Record where the legacy provider and tracker mount, whether `onRouteChange` replaces default page +calls, and which pages use SSR plugin helpers. Also record any code that reads or writes `ntaid`, +because target profile continuity uses the SDK-owned `ctfl-opt-aid` cookie. + +### Install and bind the Pages Router SDK + +Use the target guide to create both factories: + +- Client factory from `@contentful/optimization-nextjs/pages-router`. +- Server factory from `@contentful/optimization-nextjs/pages-router/server`. + +Mount the target `OptimizationRoot` and `NextPagesAutoPageTracker` in `_app.tsx`, passing +`pageProps.contentfulOptimization`. That object is the SDK handoff returned from +`getServerSideOptimizationProps()`; it can contain browser consent defaults, server optimization +state, managed-entry prefetch handoff, and the required `initialPageEvent` value. + +If migrated components will use ``, configure the server factory with the +app's `contentful` client and pass `prefetchManagedEntries` in the server helper options before +switching those components. The Pages Router client factory does not fetch managed entries by +itself. + +### Replace server profile and page evaluation + +Move SSR plugin behavior into `getServerSideOptimizationProps(context)`. The server helper binds +request consent, emits the first page event when allowed, writes the anonymous-id cookie, and passes +server optimization state through page props. Observe accepted server evaluation by checking that +`contentfulOptimization.serverOptimizationState` is present and `initialPageEvent` is `skip`; +observe denied consent by checking that no Experience API call is made and `initialPageEvent` is +`emit`. + +The target server helper resolves `initialPageEvent` so the browser tracker skips the first route +when the server reported it and emits when the server did not. Keep that value wired to the tracker +instead of duplicating legacy route-change code. + +### Replace personalized rendering + +Replace legacy React wrappers and mapper output with the Pages Router `OptimizedEntry`. It accepts a +manual `baselineEntry` or an `entryId` path backed by server-managed prefetch. It can use per-entry +loading, error, and live-update props because it is the React Web component bound for Pages Router. + +If the first render depends on legacy `nt_*` fields, migrate the Contentful model before replacing +the component. + +### Replace client-side extras + +Client features use the React Web runtime: + +- Flags use target flag reads and optional `trackFlagView()`. +- Analytics vendors use accepted and blocked event streams. +- Consent uses app-owned policy passed to server and browser SDK surfaces. +- Preview attaches to the live browser SDK through the preview panel package. + +Use [Migrating experience.js plugins and preview](./migrating-experience-js-plugins-and-preview.md) +for plugin-specific replacement. + +### Validate Pages Router migration + +Verify the server and browser handoff: + +- `getServerSideOptimizationProps()` runs on the personalized page. +- `pageProps.contentfulOptimization` reaches `_app.tsx`. +- The browser tracker uses the server-provided `initialPageEvent`. +- A target `OptimizedEntry` renders a variant or baseline. +- Personalized results are not cached outside the request boundary. + +## Validate the migration + +- Search for `@ninetailed/experience.js-next`, SSR plugin imports, `ntaid`, and legacy React + surfaces. +- Verify accepted server evaluation and denied-consent behavior. +- Verify all-locale Contentful payloads are not used for optimized entries. +- Verify client-side plugin replacements only after the route and rendering work. + +## Troubleshooting + +| Symptom | Check | +| ------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| First page events duplicate | Pass the server `initialPageEvent` value from page props into `NextPagesAutoPageTracker`. | +| `getServerSideProps` returns a 500 on API failure | Wrap the server helper and render baseline on failure when your app needs graceful fallback. | +| Browser render cannot find managed entries | Use server-side managed prefetch and pass `prefetchedManagedEntries` through page props. | +| Hooks import fails | Import React Web hooks from `@contentful/optimization-nextjs/client`, not `/pages-router`. | + +## Related guides + +- [Next.js Pages Router integration guide](./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md) +- [Choosing a Next.js migration path from experience.js](./choosing-a-nextjs-migration-path-from-experience-js.md) +- [Migrating an experience.js Contentful model to Optimization](./migrating-experience-js-contentful-model-to-optimization.md) +- [Migrating experience.js plugins and preview](./migrating-experience-js-plugins-and-preview.md) +- [Profile synchronization between client and server](../concepts/profile-synchronization-between-client-and-server.md) +- [Pages Router reference implementation](../../implementations/nextjs-sdk_pages-router/README.md) diff --git a/documentation/guides/migrating-experience-js-node-ssr-and-esr.md b/documentation/guides/migrating-experience-js-node-ssr-and-esr.md new file mode 100644 index 000000000..f0868e0a5 --- /dev/null +++ b/documentation/guides/migrating-experience-js-node-ssr-and-esr.md @@ -0,0 +1,120 @@ +# Migrating experience.js Node, SSR, and ESR + +Use this guide when server code uses `@ninetailed/experience.js-node`, SSR plugin helpers, ESR +helpers, or a manual server-to-browser handoff. + +## What changes + +Legacy server code commonly uses `NinetailedAPIClient`, SSR plugin continuity, `ntaid`, or ESR +preflight helpers. The Optimization Node SDK is stateless: create one process-level SDK, bind each +incoming request with `forRequest()`, and let the app own cookies, consent, profile persistence, +request context, and caching. + +Follow the [Node SDK integration guide](./integrating-the-node-sdk-in-a-node-app.md) unless a +Next.js adapter owns the route. + +## Before you migrate + +Gather these inputs: + +- Server paths that create profiles, emit page events, or call `NinetailedAPIClient`. +- SSR or ESR helpers and any manual server-to-browser profile handoff. +- Cookie reads/writes for `ntaid` and any browser continuation logic. +- Server-rendered Contentful entries and mapper-dependent experience configuration. +- Consent policy, request locale, user agent, URL, IP/location forwarding, and cache boundaries. + +## Migration path + +1. For Next.js apps, choose the App Router or Pages Router path before building a manual hybrid. +2. Install `@contentful/optimization-node` for non-Next server paths. +3. Replace server API-client calls with request-bound Node SDK calls. +4. Replace SSR/ESR profile handoff with app-owned persistence and target browser continuity. +5. Replace server content resolution with Node or framework entry resolution. +6. Remove legacy Node, SSR, ESR, and browser packages after imports are gone. + +## Replace legacy surfaces + +### Inventory server profile and event ownership + +Identify which server code owns each responsibility: + +- Creates or reads an anonymous profile ID. +- Emits the first page event. +- Persists profile continuity. +- Resolves Contentful entries before rendering. +- Hands state to the browser. + +This inventory prevents one request from being evaluated twice or from losing the profile before +browser takeover. + +### Replace Node API client calls + +Create one `ContentfulOptimization` instance per process and call `forRequest()` for each incoming +request. Event methods live on the request-bound client, not on the singleton. The minimum legal +shape is `forRequest({ consent })`; a migration request usually adds `locale`, `profile`, and +`eventContext`. `locale` selects the request locale, `profile` carries the app-owned anonymous +profile ID, and `eventContext` carries URL, user-agent, referrer, query, and other page data the SDK +cannot infer from your server framework. + +Accepted request-bound `page()` or `identify()` results carry the profile, selected optimizations, +and flag changes for that request. Consent-blocked events return blocked results or diagnostics +without throwing. + +### Replace SSR and ESR handoff + +Use a framework SDK when available. For Next.js, prefer the App Router or Pages Router migration +guide so the adapter owns request state, provider handoff, page-event dedupe, and cookie behavior. + +For a manual Node/Web hybrid, the app owns the profile cookie. The Node SDK exports +`ANONYMOUS_ID_COOKIE` from `@contentful/optimization-node/constants`, and its value is +`ctfl-opt-aid`, but Node does not read, write, or clear cookies for you. Read the cookie from the +incoming request, pass it as `forRequest({ profile: { id } })`, write the returned profile ID only +when persistence consent allows it, and keep the cookie browser-readable if the Web SDK must +continue the same visitor. + +### Replace server content resolution + +Stop building legacy experience configuration arrays. Fetch a baseline Contentful entry with one +concrete locale and enough linked entries, then resolve it with the request's selected +optimizations. The singleton resolver has no ambient visitor state, so pass selections explicitly +or use the request-bound `fetchOptimizedEntry()` path after an accepted request-bound `page()` or +`identify()` call. + +Do not cache personalized outputs across visitors. Raw Contentful baseline entries can follow your +normal content-cache policy, but rendered personalized HTML, merge-tag values, and Experience +responses are request-specific. + +### Validate server migration + +Verify the request boundary: + +- Accepted events return profile data when consent allows them. +- Blocked events do not throw and surface diagnostics. +- The app persists the profile only when persistence consent allows it. +- Entry resolution uses request selections. +- Browser takeover uses a compatible target SDK path. + +## Validate the migration + +- Search for `@ninetailed/experience.js-node`, SSR plugin imports, ESR helper imports, and `ntaid`. +- Verify one accepted request and one denied-consent request. +- Verify a resolved server entry falls back to baseline with no selection. +- Verify cache keys do not share personalized output across visitors. + +## Troubleshooting + +| Symptom | Check | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| Server event methods are missing | Call `forRequest()` first; event methods live on the request-bound client. | +| Non-sticky interaction tracking throws | Bind a request profile ID or use the event flow that derives one before sending Insights interactions. | +| Browser takeover starts a different visitor | Persist and pass the target anonymous ID according to the Web or framework SDK guide. | +| Personalized HTML leaks between visitors | Remove shared caching around request-specific responses and rendered output. | + +## Related guides + +- [Node SDK integration guide](./integrating-the-node-sdk-in-a-node-app.md) +- [Web SDK integration guide](./integrating-the-web-sdk-in-a-web-app.md) +- [Choosing a Next.js migration path from experience.js](./choosing-a-nextjs-migration-path-from-experience-js.md) +- [Profile synchronization between client and server](../concepts/profile-synchronization-between-client-and-server.md) +- [Node reference implementation](../../implementations/node-sdk/README.md) +- [Node plus Web reference implementation](../../implementations/node-sdk+web-sdk/README.md) diff --git a/documentation/guides/migrating-experience-js-plugins-and-preview.md b/documentation/guides/migrating-experience-js-plugins-and-preview.md new file mode 100644 index 000000000..8ff65d3a3 --- /dev/null +++ b/documentation/guides/migrating-experience-js-plugins-and-preview.md @@ -0,0 +1,132 @@ +# Migrating experience.js plugins and preview + +Use this guide when an app uses experience.js analytics, privacy, preview, insights, or third-party +plugin packages and you need to replace them with Optimization SDK Suite surfaces. + +## What changes + +Legacy plugin packages are not one-for-one target packages. Privacy moves to app-owned consent plus +SDK consent inputs. Vendor analytics moves to event-stream forwarding or app-owned business events. +Preview moves to the Optimization preview panel attached to the live Web SDK. Insights delivery is +part of the target SDK event and interaction model. + +Use this after at least one target runtime guide is in place. + +## Before you migrate + +Gather these inputs: + +- Every `@ninetailed/experience.js-plugin-*` package and plugin constructor. +- Code that reads `window.ninetailed.plugins`, `window.ninetailed.consent`, or preview globals. +- Current accepted and blocked consent behavior. +- Vendor destinations such as Google Tag Manager, Segment, Contentsquare, or product analytics. +- Preview workflows authors depend on. +- The target runtime guide for Web, React Web, App Router, or Pages Router. + +## Migration path + +1. Inventory plugin packages and globals. +2. Replace privacy plugin behavior with app-owned consent policy and target SDK consent inputs. +3. Replace analytics and tag-manager plugins with event-stream forwarding. +4. Replace preview plugin behavior with the Optimization preview panel. +5. Validate accepted events, blocked diagnostics, preview overrides, and package removal. + +## Replace legacy surfaces + +### Inventory plugin packages and globals + +List each plugin by purpose, not by package name: + +- Privacy and consent filtering. +- Third-party analytics or tag-manager forwarding. +- Preview widget and bridge behavior. +- Insights batching or custom event delivery. + +Then search for globals. Target SDK code should not depend on `window.ninetailed`, plugin globals, +or `__nt-consent__`. + +### Replace privacy plugin behavior + +The legacy privacy plugin stored consent and filtered events, traits, properties, profile merging, +and features. In the target SDK, the app owns the consent record and passes the current decision to +the SDK. Event consent and persistence consent are separate axes. + +Use the runtime guide's consent section to decide: + +- Which events may be admitted before explicit consent. +- Whether profile persistence is allowed. +- Where the app stores the visitor's consent decision. +- How blocked events are observed through `onEventBlocked` or `states.blockedEventStream`. + +Use the exact runtime section for the app: +[Web consent](./integrating-the-web-sdk-in-a-web-app.md#consent-and-privacy-handoff), +[React Web consent](./integrating-the-react-web-sdk-in-a-react-app.md#consent-and-privacy-handoff), +[App Router consent](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md#consent-identity-profile-and-reset), +or +[Pages Router consent](./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md#consent-identity-profile-and-reset). + +Do not expect blocked legacy payloads to replay after consent changes. Re-emit application events +that still need to happen under the accepted policy. + +### Replace analytics and tag-manager plugins + +Replace vendor plugins with the +[analytics forwarding guide](./forwarding-optimization-sdk-context-to-analytics-and-tag-management-tools.md). +Subscribe to accepted event streams in stateful browser runtimes and use request-local event results +on server runtimes. Dedupe accepted events by `messageId`, and forward only approved primitive +fields. `states.eventStream` carries accepted events; `states.blockedEventStream` carries only +consent-blocked diagnostics. `onEventBlocked` receives the same blocked diagnostic as a callback, +which is useful when you want logging without a browser-state subscription. + +Keep vendor consent separate from SDK consent. SDK consent controls SDK event emission; your app's +analytics policy controls whether data leaves your application boundary for a vendor. + +### Replace preview plugin behavior + +Use the Optimization preview panel for Web and React Web based runtimes. The panel attaches to the +live Web SDK, reads audience and experience definitions from a Contentful client or pre-fetched +entries, and applies preview overrides to selected optimizations and flag changes. Provide either a +`contentful.js` client through `contentful` so the panel can fetch preview entries, or pass +pre-fetched `entries: { audiences, experiences }`. + +Preview overrides can force audiences, variants, and inline-variable flag values. Validate a forced +variant by opening the panel, selecting a non-baseline variant, and watching the optimized entry +change without a page reload. Validate an inline flag by forcing a flag value in the panel and +checking that `getFlag()` or `states.flag(name)` returns that value while the panel is open. While +the panel is open, optimized entries live-update so authors can inspect changes. Legacy preview +widget globals are not target extension points. + +### Validate plugin migration + +Run these checks after the core runtime migration works: + +- Denied consent produces blocked diagnostics for events your policy blocks. +- Accepted SDK events appear once in the forwarding destination. +- Preview panel attaches to the live SDK and can force a variant or inline flag value. +- No legacy plugin package or `window.ninetailed` plugin global remains. + +## Validate the migration + +- Search for `@ninetailed/experience.js-plugin`, `window.ninetailed.plugins`, + `window.ninetailed.consent`, and `__nt-consent__`. +- Verify accepted and blocked event streams in the target runtime. +- Verify the analytics forwarding guide's dedupe behavior with one SDK activity. +- Verify the preview panel only loads in the environments where your app enables it. + +## Troubleshooting + +| Symptom | Check | +| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| Blocked events disappear | Observe `states.blockedEventStream` or `onEventBlocked`; blocked events are diagnostics, not a replay queue. | +| Vendor events duplicate | Dedupe forwarded accepted events by `messageId` outside component or subscriber lifecycles. | +| Preview attaches but does not update entries | Attach to the live Web SDK and confirm the panel has audience and experience entries. | +| Consent appears accepted in the app but SDK blocks events | Confirm the app passes the current consent decision into the target SDK runtime and that `allowedEventTypes` matches policy. | + +## Related guides + +- [Forwarding Optimization SDK context to analytics and tag-management tools](./forwarding-optimization-sdk-context-to-analytics-and-tag-management-tools.md) +- [Consent management in the Optimization SDK Suite](../concepts/consent-management-in-the-optimization-sdk-suite.md) +- [Web SDK integration guide](./integrating-the-web-sdk-in-a-web-app.md) +- [React Web integration guide](./integrating-the-react-web-sdk-in-a-react-app.md) +- [Next.js App Router integration guide](./integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md) +- [Next.js Pages Router integration guide](./integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md) diff --git a/documentation/guides/migrating-experience-js-react-to-react-web.md b/documentation/guides/migrating-experience-js-react-to-react-web.md new file mode 100644 index 000000000..1f86b887c --- /dev/null +++ b/documentation/guides/migrating-experience-js-react-to-react-web.md @@ -0,0 +1,136 @@ +# Migrating experience.js React to the React Web SDK + +Use this guide when a React app uses `@ninetailed/experience.js-react` providers, hooks, +components, or flags and you want to move to `@contentful/optimization-react-web`. + +## What changes + +Legacy React surfaces wrap the browser `Ninetailed` client and render through `Personalize`, +`Experience`, `usePersonalize`, `useExperience`, and flag hooks. React Web uses +`OptimizationRoot`, target hooks, and `OptimizedEntry` over the Optimization content model. The app +keeps Contentful fetching, consent policy, identity policy, routing, analytics, and rendering +components. + +Follow the [React Web integration guide](./integrating-the-react-web-sdk-in-a-react-app.md) for the +target provider and rendering setup. + +## Before you migrate + +Gather these inputs: + +- The root where `NinetailedProvider` is mounted and any existing injected client. +- Every `Personalize`, `Experience`, `usePersonalize`, `useExperience`, `useProfile`, `useFlag`, + `useFlagWithManualTracking`, and `TrackHasSeenComponent` call. +- Router page-tracking code, including whether the app uses React Router, TanStack Router, a custom + router, or no router abstraction. +- Consent, identity, and analytics policy. +- Contentful entries that still depend on legacy mapper output or `nt_*` fields. + +## Migration path + +1. Migrate Contentful authoring when legacy `nt_*` fields feed React rendering. See + [Migrating an experience.js Contentful model to Optimization](./migrating-experience-js-contentful-model-to-optimization.md). +2. Install `@contentful/optimization-react-web`. +3. Replace root wiring with `OptimizationRoot` and the appropriate router page tracker: React + Router, TanStack Router, or an app-owned `trackPageView` call for a custom router. +4. Replace personalized rendering with `OptimizedEntry` or target entry hooks. +5. Replace flags, interactions, consent, identity, analytics, and preview through target surfaces. +6. Remove `@ninetailed/experience.js-react` and legacy browser packages after imports are gone. + +## Replace legacy surfaces + +### Inventory provider, hooks, and render components + +Separate root setup from component-level rendering. Root setup includes `NinetailedProvider`, +plugins, page tracking, consent defaults, and any injected client. Component-level rendering +includes every `Personalize`, `Experience`, hook, flag read, and tracking wrapper. + +This split keeps the first migration step small: get the target root and page event working before +rewriting every personalized component. + +### Replace the provider and readiness model + +Mount one `OptimizationRoot` around the tree that uses personalization. React Web creates the +underlying Web SDK after React commits, so readiness and loading state matter. Provider children +still render; optimized entries handle baseline and loading behavior at the entry boundary. + +Do not rely on `window.ninetailed` or legacy provider prop changes. The target root owns the Web SDK +instance for the browser runtime, and app policy owns consent defaults. + +### Replace personalized entry rendering + +Replace legacy component and hook rendering with target entry resolution: + +- Use `OptimizedEntry` when a component renders a Contentful entry. +- Use `useOptimizedEntry` or `resolveOptimizedEntry()` when you need custom control. +- Keep your existing presentational components; render the resolved entry through them. + +Use one entry source per component: + +- Use `baselineEntry` when your app already fetched the Contentful entry and owns the query, + caching, and include depth. +- Use `entryId` only when `OptimizationRoot` has a `contentful` client for managed fetching. + +Both paths return the baseline when no selection matches or linked entries are missing. If legacy +mapper code still builds experience arrays, migrate the Contentful model first. + +### Replace flags and tracking side effects + +Do not preserve legacy flag hook names as wrappers unless your app needs a temporary compatibility +layer. Use the target SDK flag APIs: + +- `sdk.getFlag()` for a one-off read. +- `sdk.states.flag(name)` for a reactive read. +- `sdk.trackFlagView()` when you need explicit manual flag-view tracking. + +React Web optimized entries track views, clicks, and hovers by default when consent and profile +state allow them. Accepted interaction events appear on the live SDK's `states.eventStream`; consent +blocks appear on `states.blockedEventStream`. If a legacy wrapper existed only to track component +views, replace it with `OptimizedEntry` tracking rather than a custom component. + +### Replace consent, identity, analytics, and preview + +Move policy and vendor work out of the core render migration: + +- Consent uses app-owned state passed to the SDK through target consent actions or defaults. +- Identity uses target `identifyUser` and `resetUser` actions. +- Analytics vendors subscribe to accepted and blocked event streams instead of legacy plugins. +- Preview uses the Optimization preview panel package attached to the live Web SDK. + +Use [Migrating experience.js plugins and preview](./migrating-experience-js-plugins-and-preview.md) +for those replacements. + +### Validate React migration + +Verify these outcomes before deleting the legacy packages: + +- The React root mounts one Optimization SDK runtime. +- One page event is accepted or intentionally blocked by policy. +- A target `OptimizedEntry` renders the authored variant or baseline fallback. +- Interaction and flag-view events follow the target consent policy. +- No legacy React component or hook imports remain. + +## Validate the migration + +- Search for `@ninetailed/experience.js-react`, `Personalize`, `Experience`, `usePersonalize`, + `useExperience`, `useFlag`, and `TrackHasSeenComponent`. +- Verify the app renders the same baseline entry with no matching variant. +- Verify an all-visitors variant renders after the page event. +- Verify accepted events and blocked diagnostics through the target SDK streams. + +## Troubleshooting + +| Symptom | Check | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| The first render has no SDK state | React Web initializes after commit; use entry loading and baseline behavior instead of reading live state during root setup. | +| `OptimizedEntry` renders baseline | Confirm the Contentful payload includes linked Optimization entries and that a page or identify event populated selections. | +| Interaction events are missing | Check consent, profile availability, root/per-entry tracking opt-outs, and whether the rendered node carries target tracking attributes. | +| Preview does not attach | Attach the preview panel to the live Web SDK, not the initial snapshot runtime. | + +## Related guides + +- [React Web integration guide](./integrating-the-react-web-sdk-in-a-react-app.md) +- [Web SDK integration guide](./integrating-the-web-sdk-in-a-web-app.md) +- [Migrating an experience.js Contentful model to Optimization](./migrating-experience-js-contentful-model-to-optimization.md) +- [Migrating experience.js plugins and preview](./migrating-experience-js-plugins-and-preview.md) +- [React Web reference implementation](../../implementations/react-web-sdk/README.md) diff --git a/documentation/guides/migrating-experience-js-to-the-web-sdk.md b/documentation/guides/migrating-experience-js-to-the-web-sdk.md new file mode 100644 index 000000000..50cb75b89 --- /dev/null +++ b/documentation/guides/migrating-experience-js-to-the-web-sdk.md @@ -0,0 +1,126 @@ +# Migrating experience.js to the Optimization Web SDK + +Use this guide when a plain browser app or custom JavaScript adapter uses +`@ninetailed/experience.js` and you want to move to `@contentful/optimization-web`. + +## What changes + +The legacy browser client owns plugin arrays, legacy storage keys, browser globals, and imperative +event methods. The Web SDK uses one `ContentfulOptimization` instance for the browser runtime. Your +app owns Contentful fetching, routing, rendering, consent records, identity policy, and any +third-party forwarding. + +Follow the [Web SDK integration guide](./integrating-the-web-sdk-in-a-web-app.md) for the target +setup details. + +## Before you migrate + +Gather these inputs: + +- The module that constructs `Ninetailed`, any injected instance, and every plugin passed to it. +- Calls to `page`, `track`, `identify`, `batch`, `reset`, `debug`, flag reads, and manual rendering + helpers. +- Use of `window.ninetailed`, `__nt_anonymous_id__`, `__anon_id`, `__nt_profile__`, + `__nt_experiences__`, `__nt_changes__`, `__nt_debug__`, `__nt-consent__`, or `ntaid`. +- The app's consent and analytics policy. +- The Contentful entries that were resolved through legacy mapped experiences. +- The target Optimization `clientId`, environment, locale, and browser-visible config convention. + These values identify the Contentful project and locale the browser runtime may call; keep secrets + out of them and follow your bundler's public-variable convention. + +## Migration path + +1. Migrate authored Contentful entries first when the app depends on legacy `nt_*` fields. Start + with one authored all-visitors variant. See + [Migrating an experience.js Contentful model to Optimization](./migrating-experience-js-contentful-model-to-optimization.md). +2. Install `@contentful/optimization-web` and create one Web SDK instance for the browser runtime. +3. Replace the first `page()` call and entry resolution using the Web SDK guide. +4. Replace `identify`, `track`, flags, reset, and consent with target SDK calls and app-owned policy. +5. Move analytics, privacy, and preview plugins through + [Migrating experience.js plugins and preview](./migrating-experience-js-plugins-and-preview.md). +6. Remove `@ninetailed/experience.js` and legacy plugin packages after imports are gone. + +## Replace legacy surfaces + +### Inventory the legacy browser integration + +Start with the file that creates `new Ninetailed(...)` or injects an existing instance into a +provider. List every plugin and every module that reads `window.ninetailed`. Legacy provider props +did not re-create the client after construction, so treat client setup as a single runtime boundary +rather than a reactive config surface. + +Also inventory persistence. Legacy browser storage and the `ntaid` cookie do not map directly to +the target cookie name or consent model. Decide whether to reset visitor identity during migration +or perform an app-owned continuity handoff. + +### Replace client construction and lifecycle + +Create one `ContentfulOptimization` instance and reuse it. The Web SDK is ready immediately after +construction, but optimization state is empty until an accepted event such as `page()` or +`identify()` returns selections. An accepted event returns `{ accepted: true, data? }`; stateful +browser runtimes also expose accepted events on `states.eventStream`. Keep the order: + +1. Construct the instance. +2. Apply the visitor's current consent defaults. +3. Emit the page or identity event your policy allows. +4. Resolve entries or read flags after state is available. + +Do not preserve legacy plugin arrays. Replace each plugin concern with the target SDK surface or an +app-owned integration. + +### Replace page, track, identify, and reset calls + +Map imperative calls by ownership: + +- `page()` and route tracking move to Web SDK event methods or `trackCurrentPage()`. +- `identify({ userId, traits })` requires a user ID and returns an accepted or blocked result. +- `track({ event, properties })` owns application events. +- `reset()` clears SDK profile state and the SDK-owned `ctfl-opt-aid` cookie, but it does not clear + your consent record. + +The target consent model has two axes: event consent controls whether SDK events may be sent, and +persistence consent controls whether the SDK may store the profile-id cookie. With the default +pre-consent allow-list, `page` and `identify` can be admitted before explicit consent; set +`allowedEventTypes: []` if your policy requires every event to wait. + +### Replace entry and flag rendering + +Stop feeding mapped experience configuration into browser rendering. Pass a fetched baseline entry +to `resolveOptimizedEntry()` or use the Web Components path from the Web guide. When no selection +exists, links are unresolved, or the payload uses all locales, the resolver returns the baseline. + +Replace legacy flag hooks with target flag reads. Stateful flag reads auto-attempt flag-view +tracking when consent and profile state allow it; `trackFlagView()` is the explicit manual path. + +### Validate browser migration + +Use the Web guide's production checks, then add migration-specific checks: + +- One accepted page event populates selections before entry resolution. +- Denied or undecided consent behaves according to your app policy. +- Accepted events appear on `states.eventStream`; consent-blocked diagnostics appear on + `states.blockedEventStream`. +- No code reads `window.ninetailed` or legacy storage keys. + +## Validate the migration + +- Run a browser route with a test all-visitors variant and verify variant rendering. +- Repeat with consent denied and verify blocked diagnostics or baseline behavior. +- Verify analytics forwarding, if any, dedupes by `messageId`. +- Search for remaining `@ninetailed/experience.js`, `window.ninetailed`, and legacy storage keys. + +## Troubleshooting + +| Symptom | Check | +| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Entry resolution always returns baseline | Confirm an accepted page or identify event returned selections before resolution, and confirm the Contentful payload contains linked Optimization entries. | +| A second SDK instance throws | Keep one Web SDK instance per browser runtime; destroy only during teardown. | +| Vendor analytics stopped receiving events | Replace legacy plugins with event-stream forwarding from the target SDK. | +| Flags read but views are missing | Confirm consent, profile availability, and whether you need explicit `trackFlagView()`. | + +## Related guides + +- [Web SDK integration guide](./integrating-the-web-sdk-in-a-web-app.md) +- [Migrating an experience.js Contentful model to Optimization](./migrating-experience-js-contentful-model-to-optimization.md) +- [Migrating experience.js plugins and preview](./migrating-experience-js-plugins-and-preview.md) +- [Forwarding Optimization SDK context to analytics and tag-management tools](./forwarding-optimization-sdk-context-to-analytics-and-tag-management-tools.md) diff --git a/documentation/internal/migration-knowledge/experience-js.md b/documentation/internal/migration-knowledge/experience-js.md new file mode 100644 index 000000000..f4d615eb3 --- /dev/null +++ b/documentation/internal/migration-knowledge/experience-js.md @@ -0,0 +1,86 @@ +# experience.js migration knowledge + +Source checkout: `/Users/charles.hudson/Projects/contentful/experience.js` at `824370e`. + +Internal migration reference. Facts only; not guide prose. Source pointers use +`experience.js@824370e:#`. + +## Package mapping + +| experience.js package | Migration target | Boundary | +| -------------------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@ninetailed/experience.js` | `@contentful/optimization-web` or `@contentful/optimization-node` | Browser stateful client maps to Web; server event construction maps to Node request clients. source: experience.js@824370e:packages/sdks/javascript/package.json#name; experience.js@824370e:packages/sdks/javascript/src/index.ts | +| `@ninetailed/experience.js-react` | `@contentful/optimization-react-web` | Provider/hooks/components migrate to React Web root/actions/state hooks and `OptimizedEntry`. source: experience.js@824370e:packages/sdks/react/package.json#name; experience.js@824370e:packages/sdks/react/src/lib/index.ts | +| `@ninetailed/experience.js-next` | `@contentful/optimization-nextjs/pages-router` or `/app-router` | Legacy package is Pages-Router-oriented and re-exports React + SSR plugin; target choice depends on the app router. source: experience.js@824370e:packages/sdks/nextjs/package.json#name; experience.js@824370e:packages/sdks/nextjs/src/index.ts | +| `@ninetailed/experience.js-next-esr` | `@contentful/optimization-nextjs/app-router` or manual Node/Web hybrid | Legacy ESR exports only edge context/profile helpers; middleware/selector are not exported. source: experience.js@824370e:packages/sdks/nextjs-esr/src/index.ts; experience.js@824370e:packages/sdks/nextjs-esr/src/lib/middleware.ts | +| `@ninetailed/experience.js-node` | `@contentful/optimization-node` | Legacy `NinetailedAPIClient` sends server-side identify/track through `upsertManyProfiles`; target Node SDK is request-scoped and app-owned persistence. source: experience.js@824370e:packages/sdks/nodejs/src/lib/Client/Client.ts#NinetailedAPIClient | +| `@ninetailed/experience.js-utils-contentful` | Built-in Contentful entry model + managed/manual entry source | Legacy mapper converts Ninetailed Contentful entries into Experience.js configs; target SDK reads Optimization content-model fields directly during entry resolution. source: experience.js@824370e:packages/utils/contentful/src/lib/ExperienceMapper.ts#ExperienceMapper | +| `@ninetailed/experience.js-plugin-*` | Mostly manual replacements; preview maps to preview panel; insights maps to built-in insights | Third-party analytics/privacy plugin semantics are not one-for-one SDK packages. source: experience.js@824370e:packages/plugins/analytics/src/index.ts; experience.js@824370e:packages/plugins/preview/src/index.ts | + +## Legacy runtime surfaces + +- `NinetailedProvider` constructs a `Ninetailed` instance once with `useMemo([])` unless an existing instance is injected; prop changes do not re-create the instance. source: experience.js@824370e:packages/sdks/react/src/lib/NinetailedProvider.tsx#NinetailedProvider. +- The browser `Ninetailed` client wraps the `analytics` package, flattens plugin arrays, injects credentials into plugins that accept them, injects an `EventBuilder` into plugins that require it, and exposes profile/experiences on `window.ninetailed` after profile changes. source: experience.js@824370e:packages/sdks/javascript/src/lib/Ninetailed.ts#Ninetailed. +- Core browser profile state starts as loading and falls back to cached profile/experiences/changes on API failure; with no fallback profile, profile becomes `null` and the profile-change payload carries the error. source: experience.js@824370e:packages/sdks/javascript/src/lib/NinetailedCorePlugin/NinetailedCorePlugin.ts#NinetailedCorePlugin. +- Browser event methods `page`, `track`, `identify`, `batch`, sticky component view, variable view, and component view enqueue analytics events and then flush the core queue. source: experience.js@824370e:packages/sdks/javascript/src/lib/Ninetailed.ts#Ninetailed. +- `identify` with an empty user id uses `nt:empty-merge-id`; the core plugin skips identify when that empty merge id has no traits. source: experience.js@824370e:packages/sdks/javascript/src/lib/Ninetailed.ts#Ninetailed; experience.js@824370e:packages/sdks/javascript/src/lib/NinetailedCorePlugin/NinetailedCorePlugin.ts#NinetailedCorePlugin. +- API base URL defaults to `https://experience.ninetailed.co`; profile mutations post to `/v2/organizations/{clientId}/environments/{environment}/profiles...`; 503 responses retry, other non-OK responses bail. source: experience.js@824370e:packages/sdks/shared/src/lib/api/NinetailedApiClient.ts#NinetailedApiClient. +- API mutation requests default to `Content-Type: text/plain`, set `type=preflight` for preflight requests, add `locale` query param, and add `X-Force-IP` when `ip` is supplied. source: experience.js@824370e:packages/sdks/shared/src/lib/api/NinetailedApiClient.ts#NinetailedApiClient. + +## React render and hooks + +- `useProfile` subscribes to profile changes, suppresses deep-equal updates, and omits `experiences` from the hook result. source: experience.js@824370e:packages/sdks/react/src/lib/useProfile.ts#useProfile. +- `usePersonalize` selects among a baseline and variants from the current profile; `Personalize` hides the baseline wrapper during loading unless a custom loading component is supplied, then wraps rendered content in `TrackHasSeenComponent`. source: experience.js@824370e:packages/sdks/react/src/lib/usePersonalize.ts#usePersonalize; experience.js@824370e:packages/sdks/react/src/lib/Personalize.tsx#Personalize. +- `useExperience` subscribes to `onSelectVariant` and starts from the baseline while loading; `Experience` observes the rendered element for views and optional clicks/hovers, renders a marker for non-forwardRef components, and renders nothing but a marker for hidden variants. source: experience.js@824370e:packages/sdks/react/src/lib/Experience/useExperience.ts#useExperience; experience.js@824370e:packages/sdks/react/src/lib/Experience/Experience.tsx#Experience. +- `useFlag` auto-tracks a successful flag by default; `useFlagWithManualTracking` unwraps `{ value }` change payloads, falls back to the default value, and tracks flags as variable component views with id `Variable-{flagKey}`. source: experience.js@824370e:packages/sdks/react/src/lib/useFlag.ts#useFlag; experience.js@824370e:packages/sdks/react/src/lib/useFlagWithManualTracking.ts#useFlagWithManualTracking. + +## Contentful model and mapper + +- Contentful experience entries are expected to expose `nt_name`, `nt_description`, `nt_type`, `nt_config`, `nt_audience`, `nt_variants`, and `nt_experience_id`; missing `nt_config` defaults to zero traffic, two-way `0.5/0.5` distribution, no components, and non-sticky. source: experience.js@824370e:packages/utils/contentful/src/types/ExperienceEntry.ts#ExperienceEntryFields. +- `ExperienceMapper.mapExperience` validates the entry, maps each variant entry to include `id: variant.sys.id`, then delegates to the generic ExperienceMapper. source: experience.js@824370e:packages/utils/contentful/src/lib/ExperienceMapper.ts#ExperienceMapper. +- `ExperienceMapper.mapCustomExperience` and async variant let apps supply their own variant mapping; `mapBaselineWithExperiences` reads `entry.fields.nt_experiences`, filters valid experience entries, and maps them. source: experience.js@824370e:packages/utils/contentful/src/lib/ExperienceMapper.ts#ExperienceMapper. +- Generic mapper converts config `traffic` to `trafficAllocation`, cumulative `distribution` ranges, and entry-replacement components by matching variant references against mapped variants; unsupported component types throw. source: experience.js@824370e:packages/utils/javascript/src/lib/ExperienceMapper.ts#ExperienceMapper. +- Experience matching requires traffic allocation to beat a stable profile-derived random value and requires the profile to contain the audience id when the experience has an audience. source: experience.js@824370e:packages/sdks/shared/src/lib/utils/experiences/isExperienceMatch.ts#isExperienceMatch. +- Variant selection returns baseline when no variants exist, no distribution matches, distribution index is control, or the selected variant metadata is missing. source: experience.js@824370e:packages/sdks/shared/src/lib/utils/experiences/selectVariant.ts#selectVariant. + +## Identifiers and persistence + +| Identifier | Legacy owner | Migration note | source | +| -------------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `__nt_anonymous_id__` | Browser analytics storage | Core browser profile id storage key; SSR plugin syncs it from `ntaid`. | experience.js@824370e:packages/sdks/javascript/src/lib/NinetailedCorePlugin/constants.ts#ANONYMOUS_ID; experience.js@824370e:packages/plugins/ssr/src/lib/analytics/plugin.ts#NinetailedSsrPlugin | +| `__anon_id` | Browser analytics storage | Legacy key migrated into `__nt_anonymous_id__` on core plugin initialize. | experience.js@824370e:packages/sdks/javascript/src/lib/NinetailedCorePlugin/NinetailedCorePlugin.ts#NinetailedCorePlugin | +| `__nt_profile__`, `__nt_experiences__`, `__nt_changes__` | Browser analytics storage | Fallback caches used when profile flush fails. | experience.js@824370e:packages/sdks/javascript/src/lib/NinetailedCorePlugin/constants.ts#PROFILE_FALLBACK_CACHE | +| `__nt_debug__` | Browser analytics storage | Enables console logging when present; `debug(true)` writes it. | experience.js@824370e:packages/sdks/javascript/src/lib/NinetailedCorePlugin/constants.ts#DEBUG_FLAG | +| `__nt-consent__` | Browser analytics storage | Privacy plugin stores accepted consent here. | experience.js@824370e:packages/plugins/privacy/src/NinetailedPrivacyPlugin.ts#NinetailedPrivacyPlugin | +| `ntaid` | Cookie | SSR/ESR profile continuity cookie; SSR plugin writes/removes it and defaults expiry to 365 days. | experience.js@824370e:packages/sdks/shared/src/lib/constants.ts#NINETAILED_ANONYMOUS_ID_COOKIE; experience.js@824370e:packages/plugins/ssr/src/lib/analytics/plugin.ts#NinetailedSsrPlugin | +| `window.ninetailed` | Browser global | Exposes reset, profile/experiences, privacy consent, and preview/plugin APIs. | experience.js@824370e:packages/sdks/javascript/src/lib/Ninetailed.ts#Ninetailed; experience.js@824370e:packages/plugins/preview/src/lib/plugin/NinetailedPreviewPlugin.ts#NinetailedPreviewPlugin | + +## Next.js, SSR, and ESR + +- Next.js package provider wraps React provider and mounts a tracker that calls `ninetailed.page()` on initial route and `routeChangeComplete`, deduping by URL and a `tracked` sentinel. source: experience.js@824370e:packages/sdks/nextjs/src/lib/NinetailedProvider.tsx#NinetailedProvider; experience.js@824370e:packages/sdks/nextjs/src/lib/Tracker.tsx#Tracker. +- `onRouteChange` replaces the default page call and receives `{ isInitialRoute }` plus the `Ninetailed` instance. source: experience.js@824370e:packages/sdks/nextjs/src/lib/Tracker.tsx#Tracker. +- `decodeExperienceVariantsMap` parses `experienceId=variantIndex` pairs separated by commas and skips invalid or zero-like variant indexes. source: experience.js@824370e:packages/sdks/nextjs/src/lib/decodeExperienceVariantsMap.ts#decodeExperienceVariantsMap. +- SSR plugin initializes browser storage from the `ntaid` cookie, writes `ntaid` on profile change, and removes it on profile reset. source: experience.js@824370e:packages/plugins/ssr/src/lib/analytics/plugin.ts#NinetailedSsrPlugin. +- `getServerSideProfile` builds a page event, uses cookie `ntaid` or a new UUID as profile id, upserts through a supplied `NinetailedApiClient`, and returns the profile. source: experience.js@824370e:packages/plugins/ssr/src/lib/analytics/get-server-side-profile.ts#getServerSideProfile. +- ESR helper `buildNinetailedEdgeRequestContext` derives URL, locale, referrer, and user agent from `NextRequest`; `fetchEdgeProfile` sends a preflight page upsert using `ntaid`, optional IP, and optional location. source: experience.js@824370e:packages/sdks/nextjs-esr/src/lib/build-esr-context.ts#buildNinetailedEdgeRequestContext; experience.js@824370e:packages/sdks/nextjs-esr/src/lib/fetchEdgeProfile.ts#fetchEdgeProfile. +- ESR middleware and profile selector are present in source but not exported from the package entry. source: experience.js@824370e:packages/sdks/nextjs-esr/src/index.ts; experience.js@824370e:packages/sdks/nextjs-esr/src/lib/middleware.ts; experience.js@824370e:packages/sdks/nextjs-esr/src/lib/selector.ts#selectNinetailedProfile. + +## Plugins and preview + +- Analytics plugin base dedupes seen elements by element + sanitized payload, dedupes variables by variable id + sanitized payload, ignores seen elements without experience or audience, and maps index `0` to `control`. source: experience.js@824370e:packages/plugins/analytics/src/lib/NinetailedAnalyticsPlugin.ts#NinetailedAnalyticsPlugin. +- Google Analytics plugin only sends deprecated component events via `window.gtag`; it does nothing for experience view events. source: experience.js@824370e:packages/plugins/google-analytics/src/NinetailedGoogleAnalyticsPlugin.ts#NinetailedGoogleAnalyticsPlugin. +- Google Tag Manager, Segment, and Contentsquare plugins forward experience view payloads to `window.dataLayer`, Segment `analytics.track`, or `window.ninetailed.plugins.contentsquare.dataLayer`. source: experience.js@824370e:packages/plugins/google-tagmanager/src/NinetailedGoogleTagmanagerPlugin.ts#NinetailedGoogleTagmanagerPlugin; experience.js@824370e:packages/plugins/segment/src/NinetailedSegmentPlugin.ts#NinetailedSegmentPlugin; experience.js@824370e:packages/plugins/contentsquare/src/NinetailedContentsquarePlugin.ts#NinetailedContentsquarePlugin. +- Privacy plugin defaults to allowing only page events and no traits/track properties/profile merging/features; accepted consent allows page/track/identify/component/click/hover, all traits/properties/events, profile merging, and all features. source: experience.js@824370e:packages/plugins/privacy/src/NinetailedPrivacyPlugin.ts#DEFAULT_PRIVACY_CONFIG; experience.js@824370e:packages/plugins/privacy/src/NinetailedPrivacyPlugin.ts#DEFAULT_ACCEPTED_CONSENT_CONFIG. +- Privacy plugin exposes `window.ninetailed.consent`, writes `__nt-consent__`, updates enabled features when consent storage changes, filters properties/traits by wildcard allow-lists, blocks disallowed events by aborting analytics start events, and queues blocked payloads internally without replay. source: experience.js@824370e:packages/plugins/privacy/src/NinetailedPrivacyPlugin.ts#NinetailedPrivacyPlugin. +- Preview plugin attaches a Zoid widget at `https://preview.widgets.ninetailed.io/v2` by default, exposes `window.ninetailed.plugins.preview`, and only one active plugin instance attaches. source: experience.js@824370e:packages/plugins/preview/src/lib/plugin/NinetailedPreviewPlugin.ts#NinetailedPreviewPlugin; experience.js@824370e:packages/plugins/preview-bridge/src/PreviewBridge.ts#PreviewBridge. +- Preview plugin implements experience-selection middleware for entry replacements and changes-modification middleware for inline variable overrides; audience activation/deactivation and forced variant indexes rewrite active audiences, selected variants, and variable changes in memory. source: experience.js@824370e:packages/plugins/preview/src/lib/plugin/NinetailedPreviewPlugin.ts#NinetailedPreviewPlugin. +- Insights plugin builds component view/click/hover/variable events through injected `EventBuilder`, consent-gates them by dispatching `*_START` actions, batches by previous profile, flushes when queued events reach 25, and uses `navigator.sendBeacon` on page hidden. source: experience.js@824370e:packages/plugins/insights/src/lib/NinetailedInsightsPlugin.ts#NinetailedInsightsPlugin; experience.js@824370e:packages/plugins/insights/src/lib/api/NinetailedInsightsApiClient.ts#NinetailedInsightsApiClient. + +## Unsupported or manual migration boundaries + +- Legacy `Personalize`, `usePersonalize`, `useFlag`, `useFlagWithManualTracking`, and `TrackHasSeenComponent` are legacy data-model/flag surfaces; target guides should route entry replacement through `OptimizedEntry` / `resolveOptimizedEntry` and flags through target SDK flag APIs rather than preserving legacy component shapes. source: experience.js@824370e:packages/sdks/react/src/lib/Personalize.tsx#Personalize; experience.js@824370e:packages/sdks/react/src/lib/useFlag.ts#useFlag. +- Legacy Contentful `ExperienceMapper` is a migration aid, not a target pattern; target SDK guides should not ask readers to build `ExperienceConfiguration` arrays from `nt_config`. source: experience.js@824370e:packages/utils/contentful/src/lib/ExperienceMapper.ts#ExperienceMapper. +- Checked sources do not expose an official Contentful-side content migration/export tool or field mapping for moving legacy Ninetailed-authored entries to the target Optimization content model; the guide should treat content migration as an operational authoring step, not an SDK-provided codemod. source: experience.js@824370e:package.json; experience.js@824370e:packages/playgrounds/easy-hr/contentful/setup.js; experience.js@824370e:packages/playgrounds/easy-hr/contentful/export.js; extern:https://www.contentful.com/developers/docs/personalization/quick-start-guide/; extern:https://www.contentful.com/developers/docs/references/experience-api/. +- Legacy third-party analytics plugins have no direct package mapping in the Optimization SDK Suite; migration is manual event-stream forwarding or target insights behavior. source: experience.js@824370e:packages/plugins/google-tagmanager/src/NinetailedGoogleTagmanagerPlugin.ts#NinetailedGoogleTagmanagerPlugin; experience.js@824370e:packages/plugins/segment/src/NinetailedSegmentPlugin.ts#NinetailedSegmentPlugin. +- Legacy privacy plugin consent rules are not the target consent model; target SDK uses app-owned consent plus SDK `consent` / `defaults` / `allowedEventTypes`. source: experience.js@824370e:packages/plugins/privacy/src/NinetailedPrivacyPlugin.ts#NinetailedPrivacyPlugin. +- Legacy Next ESR package has unexported middleware and selector files; migration guides should not treat them as supported import surfaces. source: experience.js@824370e:packages/sdks/nextjs-esr/src/index.ts. diff --git a/documentation/internal/sdk-knowledge/shared/concepts.md b/documentation/internal/sdk-knowledge/shared/concepts.md index a21fe2878..a8221037f 100644 --- a/documentation/internal/sdk-knowledge/shared/concepts.md +++ b/documentation/internal/sdk-knowledge/shared/concepts.md @@ -120,14 +120,37 @@ skip the duplicate (per-SDK `initialPageEvent` / tracker prop). Interaction even (view/click/hover) are consent-gated browser activity and use the resolved entry id. source: react-web-sdk#auto-page/useAutoPageEmitter.ts; react-web-sdk#router/next-app.tsx -## Event streams and blocked events +## Custom flag views + +Stateful flag reads auto-attempt flag-view tracking: `getFlag(name)` tracks the read immediately, +and `states.flag(name)` tracks `.current`, `subscribe()`, and `subscribeOnce()` reads. The explicit +manual equivalent is `trackFlagView()`, which builds a `component` view event with +`componentType: 'Variable'`. +source: core-sdk#CoreStatefulEventEmitter.ts#getFlag; core-sdk#CoreStatefulEventEmitter.ts#getFlagObservable; core-sdk#CoreStatefulEventEmitter.ts#trackFlagView; core-sdk#events/EventBuilder.ts#buildFlagView + +Auto flag-view attempts require consent for `trackFlagView` and a current profile id. A pre-consent +or pre-profile flag read does not suppress a later accepted same-value track, active +`states.flag(name)` subscriptions re-attempt when tracking becomes allowed, and accepted attempts are +deduped by flag value, component id, experience id, variant index, and profile id. +source: core-sdk#CoreStatefulEventEmitter.ts#attemptFlagViewTracking; core-sdk#CoreStatefulEventEmitter.ts#initializeFlagViewConsentEffect; core-sdk#CoreStatefulEventEmitter.ts#buildFlagViewTrackingSignature + +## Stateful event forwarding streams Stateful JS SDK event forwarding is a current-value signal surface, not a durable event queue. -`states.eventStream` and `states.blockedEventStream` emit the current value immediately on subscribe -and then later signal updates; the exposed streams keep only the latest accepted or blocked event -value while Experience/Insights delivery queues remain internal. A late subscriber must dedupe from -the events it observes; the SDK does not replay a full event history through these observables. -source: core-sdk#CoreStateful.ts#CoreStates; core-sdk#CoreStateful.ts#CoreStateful; core-sdk#signals/Observable.ts#toObservable; core-sdk#signals/signals.ts#event; core-sdk#signals/signals.ts#blockedEvent; core-sdk#queues/ExperienceQueue.ts#ExperienceQueue; core-sdk#queues/InsightsQueue.ts#InsightsQueue +`states.eventStream` emits the most recent accepted Experience or Insights event after event +interceptors and schema validation; `states.blockedEventStream` emits only consent-blocked calls as +`{ reason: 'consent', method, args }`. Both observables emit the current value immediately on +subscribe and then later signal updates; the exposed streams keep only the latest accepted or +blocked event value while Experience/Insights delivery queues remain internal. A late subscriber +must dedupe from the events it observes; the SDK does not replay a full event history through these +observables. Blocked callback failures are logged rather than thrown. +source: core-sdk#CoreStateful.ts#CoreStates; core-sdk#CoreStateful.ts#CoreStateful; core-sdk#signals/Observable.ts#toObservable; core-sdk#signals/signals.ts#event; core-sdk#signals/signals.ts#blockedEvent; core-sdk#queues/ExperienceQueue.ts#ExperienceQueue; core-sdk#queues/InsightsQueue.ts#InsightsQueue; core-sdk#events/BlockedEvent.ts#BlockedEvent; core-sdk#CoreStatefulEventEmitter.ts#reportBlockedEvent + +Event-stream payloads carry each event's normal schema plus universal event fields such as +`messageId`, `channel`, `context`, and timestamps. Optimized-entry interactions add `optimization` +context only to the stream payload, not to the strict API payload. Flag-view stream events are not +enriched with `optimization`. +source: api-schemas#experience/event/UniversalEventProperties.ts#UniversalEventProperties; core-sdk#queues/ExperienceQueue.ts#send; core-sdk#queues/InsightsQueue.ts#send; core-sdk#events/OptimizationEventStreamEvent.ts#OptimizationEventStreamEvent Consent-blocked stateful events stop before API delivery or queueing: Experience methods return `{ accepted: false }`, Insights methods return without enqueueing, and Core writes only @@ -137,12 +160,6 @@ blocked attempts as accepted, so a later caller/effect can retry the same curren fresh payload under the current consent state. source: core-sdk#CoreStatefulEventEmitter.ts#sendExperienceEventWithResult; core-sdk#CoreStatefulEventEmitter.ts#sendInsightsEvent; core-sdk#CoreStatefulEventEmitter.ts#reportBlockedEvent; core-sdk#CoreStateful.ts#consent; core-sdk#tracking/AcceptedCurrentStateTracker.ts#AcceptedCurrentStateTracker; web-sdk#ContentfulOptimization.ts#trackCurrentPage; optimization-js-bridge#index.ts#Bridge -Event-stream `optimization` is runtime-only forwarding enrichment. Core attaches it only to the -`eventStream` value when an optimized-entry interaction has an `EventOptimizationContext`; -Experience and Insights API queues validate and send the original event payload without that -property. -source: core-sdk#events/OptimizationEventStreamEvent.ts#OptimizationEventStreamEvent; core-sdk#queues/ExperienceQueue.ts#send; core-sdk#queues/InsightsQueue.ts#send - ## Experience response payload An accepted Experience API request (page/identify/screen/track) returns `OptimizationData` @@ -222,3 +239,22 @@ fields are present, while preview overrides update their clean baselines only fr unchanged; own present `undefined` is a resettable baseline, and override derivation falls back to empty arrays only when no baseline exists. source: core-sdk#runtime/SnapshotRuntime.ts#SnapshotRuntime; core-sdk#preview-support/PreviewOverrideManager.ts#PreviewOverrideManager; core-sdk#CoreBase.ts#LifecycleInterceptors + +## Preview overrides + +The web preview panel attaches through the SDK bridge, registers a `PreviewOverrideManager`, and +mutates the stateful SDK's `selectedOptimizations` and `changes` signals from a clean API baseline +plus current overrides. Opening the panel sets `previewPanelOpen`, which forces optimized entries to +live-update while the panel is open. +source: preview-panel#attachOptimizationPreviewPanel.ts#attachOptimizationPreviewPanelToSdk; core-sdk#bridge-support/capabilities.ts#installCoreBridgeCapabilities; core-sdk#preview-support/PreviewOverrideManager.ts#syncOverridesToSignal; web-sdk#presentation/OptimizedEntryController.ts#resolveShouldLiveUpdate + +Audience overrides activate all associated experiences at variant index `1` or deactivate them at +variant index `0`; single-experience overrides replace that experience's `variantIndex`, appending a +selection with an empty `variants` map when the API baseline did not include it. Reset restores the +cached API baseline. +source: core-sdk#preview-support/PreviewOverrideManager.ts#activateAudience; core-sdk#preview-support/PreviewOverrideManager.ts#deactivateAudience; core-sdk#preview-support/PreviewOverrideManager.ts#setVariantOverride; core-sdk#preview-support/applyOptimizationOverrides.ts#applyOptimizationOverrides; core-sdk#preview-support/PreviewOverrideManager.ts#resetAll + +Inline-variable preview overrides are represented as `Variable` changes so `getFlag()` and +`states.flag(name)` resolve the preview-selected value; variant index `0` and out-of-range variant +indexes use the component baseline value. +source: core-sdk#preview-support/applyChangeOverrides.ts#applyChangeOverrides; core-sdk#preview-support/PreviewOverrideManager.ts#deriveChanges diff --git a/documentation/internal/sdk-knowledge/web/nextjs-app-router.md b/documentation/internal/sdk-knowledge/web/nextjs-app-router.md index c626c5c8d..f64ebffa9 100644 --- a/documentation/internal/sdk-knowledge/web/nextjs-app-router.md +++ b/documentation/internal/sdk-knowledge/web/nextjs-app-router.md @@ -202,6 +202,13 @@ source: `nextjs-sdk#app-router-server.tsx#renderBoundRootTree`; `nextjs-sdk#app- histories, blocked events are diagnostic only, and event-stream `optimization` is not sent to Experience/Insights API payloads. source: `nextjs-sdk#app-router-server.tsx#toClientRootConfig`; `react-web-sdk#provider/OptimizationProvider.tsx#OptimizationProvider`; kb:shared/concepts.md; kb:web/react-web.md +- Browser/client flags use the React Web stateful runtime: `sdk.getFlag()` and `sdk.states.flag()` + auto-attempt flag-view tracking, while `sdk.trackFlagView()` is the explicit/manual path. + source: `react-web-sdk#hooks/useOptimization.ts#useOptimizationContext`; core-sdk#CoreStatefulEventEmitter.ts#getFlag; core-sdk#CoreStatefulEventEmitter.ts#getFlagObservable; core-sdk#CoreStatefulEventEmitter.ts#trackFlagView +- Preview-panel attachment is a browser/client concern; once attached to the live client SDK, its + audience, variant, and inline-variable overrides use the shared preview override behavior and + force live updates while open. + source: preview-panel#attachOptimizationPreviewPanel.ts#attachOptimizationPreviewPanelToSdk; core-sdk#preview-support/PreviewOverrideManager.ts#setVariantOverride; core-sdk#preview-support/applyChangeOverrides.ts#applyChangeOverrides; react-web-sdk#provider/LiveUpdatesProvider.tsx#LiveUpdatesProvider ## Consent & persistence diff --git a/documentation/internal/sdk-knowledge/web/nextjs-pages-router.md b/documentation/internal/sdk-knowledge/web/nextjs-pages-router.md index 1fd20ba81..59ba2a20c 100644 --- a/documentation/internal/sdk-knowledge/web/nextjs-pages-router.md +++ b/documentation/internal/sdk-knowledge/web/nextjs-pages-router.md @@ -151,6 +151,13 @@ source: `nextjs-sdk#pages-router.ts#OptimizedEntry`; `react-web-sdk#optimized-en histories, blocked events are diagnostic only, and event-stream `optimization` is not sent to Experience/Insights API payloads. source: `nextjs-sdk#pages-router.ts#toClientRootConfig`; `react-web-sdk#provider/OptimizationProvider.tsx#OptimizationProvider`; kb:shared/concepts.md; kb:web/react-web.md +- Browser/client flags use the React Web stateful runtime: `sdk.getFlag()` and `sdk.states.flag()` + auto-attempt flag-view tracking, while `sdk.trackFlagView()` is the explicit/manual path. + source: `react-web-sdk#hooks/useOptimization.ts#useOptimizationContext`; core-sdk#CoreStatefulEventEmitter.ts#getFlag; core-sdk#CoreStatefulEventEmitter.ts#getFlagObservable; core-sdk#CoreStatefulEventEmitter.ts#trackFlagView +- Preview-panel attachment is a browser/client concern; once attached to the live client SDK, its + audience, variant, and inline-variable overrides use the shared preview override behavior and + force live updates while open. + source: preview-panel#attachOptimizationPreviewPanel.ts#attachOptimizationPreviewPanelToSdk; core-sdk#preview-support/PreviewOverrideManager.ts#setVariantOverride; core-sdk#preview-support/applyChangeOverrides.ts#applyChangeOverrides; react-web-sdk#provider/LiveUpdatesProvider.tsx#LiveUpdatesProvider ## Consent & persistence diff --git a/documentation/internal/sdk-knowledge/web/react-web.md b/documentation/internal/sdk-knowledge/web/react-web.md index 0a050e216..e10feb480 100644 --- a/documentation/internal/sdk-knowledge/web/react-web.md +++ b/documentation/internal/sdk-knowledge/web/react-web.md @@ -193,18 +193,15 @@ source: core-sdk#runtime/SnapshotRuntime.ts#SnapshotRuntime; core-sdk#runtime/Sn missing view/click/hover can mean consent policy, a root/per-entry opt-out, missing tracking attributes, or missing profile continuity. source: web-sdk#entry-tracking/EntryInteractionRuntime.ts#reconcileInteraction; web-sdk#entry-tracking/EntryInteractionRuntime.ts#isInteractionAllowed; core-sdk#queues/InsightsQueue.ts#send -- Flags: `getFlag(name)` nonreactive read (defaults to current changes signal, emits flag-view - tracking when consent+profile allow); `states.flag(name)` reactive. - source: core-sdk#CoreStatefulEventEmitter.ts#getFlag; core-sdk#CoreStatefulEventEmitter.ts#getFlagObservable; core-sdk#CoreStateful.ts#CoreStates -- Analytics forwarding: `states.eventStream.current?.messageId` + `.subscribe`; dedupe by - `messageId`; `states.blockedEventStream` for blocked-event diagnostics; - `subscription.unsubscribe()`. - source: api-schemas#experience/event/UniversalEventProperties.ts#UniversalEventProperties; core-sdk#CoreStateful.ts#CoreStates; core-sdk#signals/Observable.ts#Subscription; core-sdk#signals/Observable.ts#Observable -- Browser event forwarding and blocked-event behavior follow shared stateful Core semantics: - current-value observables, not durable history; consent-blocked events are diagnostic only and are - not replayed by later consent; event-stream `optimization` is not sent to Experience/Insights API - payloads. - source: kb:shared/concepts.md +- Flags: `getFlag(name)` nonreactive reads and `states.flag(name)` reactive reads auto-attempt + flag-view tracking; explicit/manual replacement is `trackFlagView()` on the live `sdk`. See + [`../shared/concepts.md`](../shared/concepts.md#custom-flag-views). + source: core-sdk#CoreStatefulEventEmitter.ts#getFlag; core-sdk#CoreStatefulEventEmitter.ts#getFlagObservable; core-sdk#CoreStatefulEventEmitter.ts#trackFlagView; react-web-sdk#hooks/useOptimization.ts#useOptimizationContext +- Analytics forwarding: subscribe to the live `sdk.states.eventStream` for accepted events and + `sdk.states.blockedEventStream` for consent-blocked diagnostics; dedupe accepted events by + `messageId` and clean up subscriptions with `.unsubscribe()`. See + [`../shared/concepts.md`](../shared/concepts.md#stateful-event-forwarding-streams). + source: core-sdk#CoreStateful.ts#CoreStates; core-sdk#events/OptimizationEventStreamEvent.ts#OptimizationEventStreamEvent; core-sdk#events/BlockedEvent.ts#BlockedEvent; core-sdk#signals/Observable.ts#Subscription; react-web-sdk#hooks/useOptimization.ts#useOptimizationContext; kb:shared/concepts.md - `useEventStreamState()` subscribes internally and returns only the latest accepted event payload; it has no `.subscribe()` method. Code that needs accepted and blocked diagnostics or forwarding subscriptions must obtain a guarded live `sdk` from `useOptimizationContext()`, then subscribe to @@ -257,6 +254,10 @@ source: core-sdk#runtime/SnapshotRuntime.ts#SnapshotRuntime; core-sdk#runtime/Sn Web SDK and its preview bridge. `entries`, when supplied, is used instead of fetching through `contentful`. source: react-web-sdk#provider/OptimizationProvider.tsx#OptimizationProvider; preview-panel#attachOptimizationPreviewPanel.ts#attachOptimizationPreviewPanel; preview-panel#attachOptimizationPreviewPanel.ts#attachOptimizationPreviewPanelToSdk; preview-panel#attachOptimizationPreviewPanel.ts#AttachOptimizationPreviewPanelArgs +- Preview overrides force audiences, variants, and inline-variable flag values by mutating + stateful SDK signals from an API baseline; panel-open state forces optimized entries to + live-update. See [`../shared/concepts.md`](../shared/concepts.md#preview-overrides). + source: preview-panel#attachOptimizationPreviewPanel.ts#attachOptimizationPreviewPanelToSdk; core-sdk#preview-support/PreviewOverrideManager.ts#setVariantOverride; core-sdk#preview-support/applyChangeOverrides.ts#applyChangeOverrides; react-web-sdk#provider/LiveUpdatesProvider.tsx#LiveUpdatesProvider - `locale` prop change updates the SDK's Experience/event locale; the app still refetches Contentful and re-emits page events itself. source: react-web-sdk#provider/OptimizationProvider.tsx#OptimizationProvider; core-sdk#CoreStateful.ts#setLocale diff --git a/documentation/internal/sdk-knowledge/web/web.md b/documentation/internal/sdk-knowledge/web/web.md index 260577068..8260215d6 100644 --- a/documentation/internal/sdk-knowledge/web/web.md +++ b/documentation/internal/sdk-knowledge/web/web.md @@ -159,17 +159,6 @@ None (imperative class + Web Components; no React surface). Web Components eleme `tracking.enableElement('views', el, { data, dwellTimeMs })` / `disableElement` / `clearElement` (manual data precedes attributes). Uses RESOLVED entry id. source: web-sdk#entry-tracking/EntryInteractionRuntime.ts#EntryInteractionRuntime; web-sdk#entry-tracking/resolveAutoTrackEntryInteractionOptions.ts#EntryInteractionApi; web-sdk#presentation/OptimizedEntryTrackingAttributes.ts#resolveOptimizedEntryTrackingAttributes -- Flags: `getFlag(name)` one-off; `states.flag(name)` reactive; flag-view emission gated on - consent+profile and deduped. - source: core-sdk#CoreStatefulEventEmitter.ts#getFlag -- Analytics forwarding: `states.eventStream.current?.messageId` + `.subscribe`; dedupe by - `messageId`; every event carries `messageId` + `type`. - source: api-schemas#experience/event/UniversalEventProperties.ts#UniversalEventProperties; api-schemas#experience/event/PageViewEvent.ts#PageViewEvent -- Browser event forwarding and blocked-event behavior follow shared stateful Core semantics: - current-value observables, not durable history; consent-blocked events are diagnostic only and are - not replayed by later consent; event-stream `optimization` is not sent to Experience/Insights API - payloads. - source: kb:shared/concepts.md - Analytics-only handoff: `initializeOptimizationAnalyticsRuntime(config)` creates a narrow Web runtime with `tracking`, `trackCurrentPage`, `flush`, and `destroy`, but no content-resolution surface. It removes the global browser SDK reference if construction registered this analytics @@ -180,6 +169,16 @@ None (imperative class + Web Components; no React surface). Web Components eleme `static` and `public-permutation` analytics handoffs suppress durable continuity persistence the same way content handoffs do. source: web-sdk#analytics.ts#initializeOptimizationAnalyticsRuntime; web-sdk#analytics.ts#hydrateOptimizationAnalyticsHandoff; web-sdk#analytics.ts#warnSkippedInitialPageWithoutProfileContinuity; web-sdk#handoff.ts#shouldPreserveDurableContinuity +- Flags: `getFlag(name)` one-off and `states.flag(name)` reactive reads auto-attempt flag-view + tracking; explicit/manual replacement is `trackFlagView()`. See + [`../shared/concepts.md`](../shared/concepts.md#custom-flag-views). + source: core-sdk#CoreStatefulEventEmitter.ts#getFlag; core-sdk#CoreStatefulEventEmitter.ts#getFlagObservable; core-sdk#CoreStatefulEventEmitter.ts#trackFlagView +- Analytics forwarding: subscribe to `states.eventStream` for accepted events and + `states.blockedEventStream` for consent-blocked diagnostics; dedupe accepted events by + `messageId`. Event-stream payloads carry each event's normal schema plus universal fields such as + `messageId`, `channel`, `context`, and timestamps. See + [`../shared/concepts.md`](../shared/concepts.md#stateful-event-forwarding-streams). + source: core-sdk#CoreStateful.ts#CoreStates; core-sdk#events/OptimizationEventStreamEvent.ts#OptimizationEventStreamEvent; core-sdk#events/BlockedEvent.ts#BlockedEvent; api-schemas#experience/event/UniversalEventProperties.ts#UniversalEventProperties; kb:shared/concepts.md ## Consent & persistence @@ -233,5 +232,9 @@ None (imperative class + Web Components; no React surface). Web Components eleme are supplied, `entries` takes precedence and no Contentful fetch is made. Idempotent — re-invoking reuses the in-flight/completed attachment. source: preview-panel#attachOptimizationPreviewPanel.ts#attachOptimizationPreviewPanel; preview-panel#attachOptimizationPreviewPanel.ts#AttachOptimizationPreviewPanelArgs; preview-panel#lib/entries.ts#PreviewPanelEntries +- Preview overrides force audiences, variants, and inline-variable flag values by mutating + stateful SDK signals from an API baseline; panel-open state forces optimized entries to + live-update. See [`../shared/concepts.md`](../shared/concepts.md#preview-overrides). + source: preview-panel#attachOptimizationPreviewPanel.ts#attachOptimizationPreviewPanelToSdk; core-sdk#preview-support/PreviewOverrideManager.ts#setVariantOverride; core-sdk#preview-support/applyChangeOverrides.ts#applyChangeOverrides; web-sdk#presentation/OptimizedEntryController.ts#resolveShouldLiveUpdate - Validation: `pnpm implementation:run -- web-sdk typecheck`; `pnpm test:e2e:web-sdk`. source: impl:web-sdk#package.json diff --git a/scripts/validate-guide-authoring.ts b/scripts/validate-guide-authoring.ts index cb8c746ac..c1d6795ac 100644 --- a/scripts/validate-guide-authoring.ts +++ b/scripts/validate-guide-authoring.ts @@ -8,7 +8,11 @@ import { headingsOf, isTableDivider, parseTableRow } from './sdk-knowledge/markd const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const blueprintDir = path.join(rootDir, 'documentation/authoring/blueprints') const templatePath = path.join(blueprintDir, '_template.md') +const migrationBlueprintDir = path.join(rootDir, 'documentation/authoring/migration-blueprints') +const migrationTemplatePath = path.join(migrationBlueprintDir, '_template.md') +const migrationRecipePath = path.join(rootDir, 'documentation/authoring/recipes/migration.md') const requiredFrontmatter = ['sdk', 'archetype', 'kb', 'guide'] as const +const migrationRequiredFrontmatter = ['migration', 'archetype', 'source', 'guide'] as const const sectionMapColumns = [ 'Section', 'Category', @@ -16,6 +20,8 @@ const sectionMapColumns = [ 'Must teach or show', 'Fact sources', ] as const +const migrationRouteColumns = ['Legacy surface', 'Target route', 'Detail owner'] as const +const migrationSectionPlanColumns = ['Section', 'Purpose', 'Must route to', 'Fact sources'] as const const categories = new Set([ 'Required for first integration', 'Common but policy-dependent', @@ -109,7 +115,7 @@ function validateLinks(file: string, lines: string[]): void { }) } -function validateLinkRoles(file: string, lines: string[]): void { +function validateLinkRoles(file: string, lines: string[], allowCodeReferences = false): void { const headings = headingsOf(lines) const rolesHeading = headings.find( (heading) => heading.level === 2 && heading.text === 'Link roles', @@ -126,16 +132,19 @@ function validateLinkRoles(file: string, lines: string[]): void { return } roles.forEach((role, index) => { - if (!/\[[^\]]+\]\([^)]+\)/u.test(role)) { + if (!/\[[^\]]+\]\([^)]+\)/u.test(role) && !(allowCodeReferences && /`[^`]+`/u.test(role))) { addProblem(file, rolesHeading.line + index + 1, 'each Link role must contain a Markdown link') } }) } -function sectionMapTable(lines: string[]): { firstLine: number; lines: string[] } | undefined { +function tableSection( + lines: string[], + headingText: string, +): { firstLine: number; lines: string[] } | undefined { const headings = headingsOf(lines) const sectionHeading = headings.find( - (heading) => heading.level === 2 && heading.text === 'Section map', + (heading) => heading.level === 2 && heading.text === headingText, ) if (sectionHeading === undefined) { return undefined @@ -149,22 +158,24 @@ function sectionMapTable(lines: string[]): { firstLine: number; lines: string[] } } -function sectionMapHeader( +function tableHeader( file: string, table: { firstLine: number; lines: string[] }, + columns: readonly string[], + tableName: string, ): number | undefined { const { lines: tableLines } = table const headerOffset = tableLines.findIndex((line) => { const cells = parseTableRow(line) - return cells?.join('\0') === sectionMapColumns.join('\0') + return cells?.join('\0') === columns.join('\0') }) if (headerOffset === -1) { - addProblem(file, table.firstLine, 'Section map table has the wrong columns') + addProblem(file, table.firstLine, `${tableName} table has the wrong columns`) return undefined } const divider = parseTableRow(tableLines[headerOffset + 1] ?? '') if (divider === undefined || !isTableDivider(divider)) { - addProblem(file, table.firstLine + headerOffset + 1, 'Section map table lacks a divider') + addProblem(file, table.firstLine + headerOffset + 1, `${tableName} table lacks a divider`) return undefined } return headerOffset @@ -205,11 +216,11 @@ function sectionMapRow(file: string, line: number, cells: string[]): SectionRow } function sectionRows(file: string, lines: string[]): SectionRow[] { - const table = sectionMapTable(lines) + const table = tableSection(lines, 'Section map') if (table === undefined) { return [] } - const headerOffset = sectionMapHeader(file, table) + const headerOffset = tableHeader(file, table, sectionMapColumns, 'Section map') if (headerOffset === undefined) { return [] } @@ -284,13 +295,123 @@ function compareRows(blueprint: string, guide: string, expected: SectionRow[]): } } +function sectionsOf(lines: string[]): string[] { + return headingsOf(lines) + .filter((heading) => heading.level === 2) + .map((heading) => heading.text) +} + +function validateSections(file: string, lines: string[], expected: string[], label: string): void { + const sections = sectionsOf(lines) + if (sections.join('\0') !== expected.join('\0')) { + addProblem(file, 1, `${label} headings do not match _template.md: ${sections.join(' -> ')}`) + } +} + +function tableRows( + file: string, + lines: string[], + headingText: string, + columns: readonly string[], +): Array<{ line: number; cells: string[] }> { + const table = tableSection(lines, headingText) + if (table === undefined) { + return [] + } + const headerOffset = tableHeader(file, table, columns, headingText) + if (headerOffset === undefined) { + return [] + } + return table.lines + .slice(headerOffset + tableHeaderRowCount) + .map((tableLine, index) => { + const cells = parseTableRow(tableLine) + const line = table.firstLine + headerOffset + tableHeaderRowCount + index + 1 + if (cells === undefined) { + return undefined + } + if (cells.length !== columns.length) { + addProblem( + file, + line, + `${headingText} row has ${cells.length} cells; expected ${columns.length}`, + ) + return undefined + } + if (cells.some((cell) => cell === '')) { + addProblem(file, line, `${headingText} cells must all be non-empty`) + } + return { line, cells } + }) + .filter((row): row is { line: number; cells: string[] } => row !== undefined) +} + +function validateMigrationTables(file: string, lines: string[]): number { + tableRows(file, lines, 'Migration route', migrationRouteColumns) + const rows = tableRows(file, lines, 'Section plan', migrationSectionPlanColumns) + rows.forEach(({ line, cells }) => { + const facts = cells[migrationSectionPlanColumns.indexOf('Fact sources')] ?? '' + if (!/\[[^\]]+\]\([^)]+\)/u.test(facts)) { + addProblem(file, line, 'Fact sources must contain at least one knowledge link') + } + }) + return rows.length +} + +function validateTarget( + file: string, + frontmatter: Map, + key: string, + label: string, +): string | undefined { + const target = frontmatter.get(key) + if (target === undefined) { + return undefined + } + const resolved = path.resolve(path.dirname(file), target) + if (!existsSync(resolved)) { + addProblem(file, 1, `${label} target does not exist: ${target}`) + return undefined + } + return resolved +} + +function migrationGuideSections(): string[] { + const lines = readFileSync(migrationRecipePath, 'utf8').split(/\r?\n/u) + const headings = headingsOf(lines) + const templateHeading = headings.find( + (heading) => heading.level === 2 && heading.text === 'Template', + ) + return headings + .filter( + (heading) => + heading.level === 2 && templateHeading !== undefined && heading.line > templateHeading.line, + ) + .map((heading) => heading.text) +} + +function validateMigrationGuideSpine(file: string, lines: string[], expected: string[]): void { + const sections = sectionsOf(lines) + if (sections.join('\0') !== expected.join('\0')) { + addProblem( + file, + 1, + `migration guide headings do not match migration recipe: ${sections.join(' -> ')}`, + ) + } +} + const templateLines = readFileSync(templatePath, 'utf8').split(/\r?\n/u) -const templateSections = headingsOf(templateLines) - .filter((heading) => heading.level === 2) - .map((heading) => heading.text) +const templateSections = sectionsOf(templateLines) +const migrationTemplateLines = readFileSync(migrationTemplatePath, 'utf8').split(/\r?\n/u) +const migrationTemplateSections = sectionsOf(migrationTemplateLines) +const expectedMigrationGuideSections = migrationGuideSections() const blueprintFiles = readdirSync(blueprintDir) .filter((name) => name.endsWith('.md') && name !== '_template.md') .sort() +const migrationBlueprintFiles = readdirSync(migrationBlueprintDir) + .filter((name) => name.endsWith('.md') && name !== '_template.md') + .sort() let sectionCount = 0 for (const name of blueprintFiles) { @@ -302,32 +423,44 @@ for (const name of blueprintFiles) { addProblem(file, 1, `missing frontmatter key: ${key}`) } }) - const sections = headingsOf(lines) - .filter((heading) => heading.level === 2) - .map((heading) => heading.text) - if (sections.join('\0') !== templateSections.join('\0')) { - addProblem(file, 1, `blueprint headings do not match _template.md: ${sections.join(' -> ')}`) - } + validateSections(file, lines, templateSections, 'blueprint') validateLinks(file, lines) validateLinkRoles(file, lines) const rows = sectionRows(file, lines) sectionCount += rows.length - const guideTarget = frontmatter.get('guide') - const kbTarget = frontmatter.get('kb') - if (guideTarget !== undefined) { - const guide = path.resolve(path.dirname(file), guideTarget) - if (!existsSync(guide)) { - addProblem(file, 1, `guide target does not exist: ${guideTarget}`) - } else { - const guideLines = readFileSync(guide, 'utf8').split(/\r?\n/u) - validateLinks(guide, guideLines) - validateGuideMarkers(guide, guideLines) - compareRows(file, guide, rows) - } + const guide = validateTarget(file, frontmatter, 'guide', 'guide') + if (guide !== undefined) { + const guideLines = readFileSync(guide, 'utf8').split(/\r?\n/u) + validateLinks(guide, guideLines) + validateGuideMarkers(guide, guideLines) + compareRows(file, guide, rows) } - if (kbTarget !== undefined && !existsSync(path.resolve(path.dirname(file), kbTarget))) { - addProblem(file, 1, `KB target does not exist: ${kbTarget}`) + validateTarget(file, frontmatter, 'kb', 'KB') +} + +let migrationSectionCount = 0 +for (const name of migrationBlueprintFiles) { + const file = path.join(migrationBlueprintDir, name) + const lines = readFileSync(file, 'utf8').split(/\r?\n/u) + const frontmatter = parseFrontmatter(file, lines) + migrationRequiredFrontmatter.forEach((key) => { + if (!frontmatter.has(key)) { + addProblem(file, 1, `missing frontmatter key: ${key}`) + } + }) + validateSections(file, lines, migrationTemplateSections, 'migration blueprint') + validateLinks(file, lines) + validateLinkRoles(file, lines, true) + migrationSectionCount += validateMigrationTables(file, lines) + + validateTarget(file, frontmatter, 'source', 'source') + const guide = validateTarget(file, frontmatter, 'guide', 'guide') + if (guide !== undefined) { + const guideLines = readFileSync(guide, 'utf8').split(/\r?\n/u) + validateLinks(guide, guideLines) + validateGuideMarkers(guide, guideLines) + validateMigrationGuideSpine(guide, guideLines, expectedMigrationGuideSections) } } @@ -342,6 +475,6 @@ if (problems.length > 0) { process.exitCode = 1 } else { process.stdout.write( - `✓ Guide authoring: ${blueprintFiles.length} blueprint(s), ${sectionCount} planned section(s), links and rendered guide structure valid.\n`, + `✓ Guide authoring: ${blueprintFiles.length} integration blueprint(s), ${sectionCount} planned integration section(s), ${migrationBlueprintFiles.length} migration blueprint(s), ${migrationSectionCount} planned migration section(s), links and rendered guide structure valid.\n`, ) }