Skip to content

♻️ refactor(web-sdk_angular): Align SSR with isomorphic runtime#357

Merged
Charles Hudson (phobetron) merged 5 commits into
mainfrom
NT-3467-angular-align-with-355
Jul 7, 2026
Merged

♻️ refactor(web-sdk_angular): Align SSR with isomorphic runtime#357
Charles Hudson (phobetron) merged 5 commits into
mainfrom
NT-3467-angular-align-with-355

Conversation

@fmamud

Copy link
Copy Markdown
Contributor

Summary

Refactor the Angular reference implementation to adopt the OptimizationRuntime / OptimizationSnapshot seam introduced in #354/#355, so Angular renders personalized content during SSR from snapshot state and hydrates into the live browser SDK using the same primitives React Web uses. Along the way, remove local machinery that now duplicates behavior available in @contentful/optimization-core / @contentful/optimization-web, and consolidate the merge-tag rendering pattern with Next.js.

To make one of those primitives Angular-consumable, promote WebOptimizationRuntime / createWebSnapshotRuntime from a React-internal file into @contentful/optimization-web/runtime so every framework layer (React Web, Angular, and future adapters) reads from one canonical seam.

Motivation

  • Angular was carrying its own platform: 'server' | 'browser' runtime discriminator, its own pre-resolved-entries TransferState payload, its own rich-text merge-tag walker, and its own
    ifBrowser / ifLive escape hatches — all things the isomorphic runtime primitives now cover uniformly.
  • Duplicating those layers in each framework SDK means every future runtime feature (consent gating, allowed event types, canOptimize semantics) has to land in N places instead of one.
  • Next.js 🔥 feat(nextjs): Bound Optimization components & Isomorphic Optimization Provider #355 established the target shape: components call runtime().* unconditionally, merge tags resolve at render time via a getMergeTagValue closure, and the framework layer only
    handles the constructor branch. Angular should match.

Changes

1. Angular reference implementation — adopt the isomorphic seam

  • services/optimization.ts — single Signal<WebOptimizationRuntime> seeded via createWebSnapshotRuntime(snapshot); promoted to the live ContentfulOptimization after hydrateOptimizationData resolves. Preflight simplified to emit one OptimizationSnapshot (matches the shape Next.js uses for serverOptimizationState). ifBrowser / ifLive escape hatches deleted — every SDK-facing call site now goes through runtime().* unconditionally.
  • services/entry.ts — collapsed the server/browser branches: one path calls runtime.resolveOptimizedEntry and runtime.getMergeTagValue. Deleted the TransferState lift, deleted mergeTagResolved from the entry-resolution layer, dropped isPlatformBrowser from setupManualTracking since runtime().tracking.* NOOPs on the snapshot runtime.
  • components/entry-card/index.ts — rich-text walker takes a getMergeTagValue closure captured from runtime() + profile() and substitutes INLINES.EMBEDDED_ENTRY merge-tag targets in place, matching Next.js's createRichTextRenderOptions pattern. One shared walk produces both the HTML and the merge-tag badge signal.
  • components/tracking-log/index.ts — the inline switch (raw?.type) became a 4-line dispatcher; each event branch is now a private handle* method typed with
    Extract<OptimizationEventStreamEvent, { type: '…' }>. Sequence counters moved from closure locals to private class fields.
  • services/live-updates.tspreviewPanelAttached / previewPanelOpen read from runtime().states.* instead of the previous ifBrowser wrapping.
  • components/control-panel/index.ts, pages/page-two/index.ts — call runtime().consent, runtime().identify, runtime().reset, runtime().page, runtime().trackView unconditionally.
  • Deleted files:
    • src/app/transfer-state-keys.ts — the two StateKeys now live next to their sole owners (SERVER_BASELINES_KEY in contentful-client.ts, SERVER_OPTIMIZATION_KEY internal to
      optimization.ts).
    • src/app/services/merge-tags.ts — the ~52-LOC resolveEntryMergeTags pre-walker; substitution now happens at render time.

2. Package change — publish WebOptimizationRuntime from optimization-web/runtime

WebOptimizationRuntime and createWebSnapshotRuntime moved from packages/web/frameworks/react-web-sdk/src/runtime/webRuntime.ts (internal) to packages/web/web-sdk/src/runtime.ts (public, alongside the existing pass-through of @contentful/optimization-core/runtime). This composes the universal OptimizationRuntime with the browser-only web SDK surface (tracking, trackCurrentPage) once, so every framework consumer imports the same isomorphic runtime object.

  • React Web OptimizationContext / OptimizationProvider re-pointed at @contentful/optimization-web/runtime.
  • Internal webRuntime.ts file deleted.

Design notes

  • Two isPlatformBrowser checks kept intentionally. In services/optimization.ts it gates new ContentfulOptimization() (the SDK reads localStorage at construction time — this is the root of the runtime seam, not a caller of it). In services/consent.ts it gates a raw document.cookie write (unrelated to the SDK). Both guard things the runtime seam explicitly does not cover.
  • Angular server preflight (runServerPreflight) is kept. Next.js's equivalent is baked into createNextjsAppRouterOptimization, which is Next.js-shaped. Angular's provideServerOptimizationInitializer() is the Angular flavor of the same per-framework concern; it emits one OptimizationSnapshot and stops there.
  • SERVER_RESOLVED_ENTRIES_KEY intentionally dropped. Server-resolved entries are recomputable from data.selectedOptimizations + baseline via the snapshot runtime's pure resolvers; shipping them wastes bytes and creates a second source of truth. Bit-identical output.
  • Extracted async work out of the constructor into module-scope helpers to satisfy SonarQube typescript:S7059 without introducing dead-code this on instance methods.

Breaking changes

None for consumers of Angular reference impl or the SDK packages. The moved WebOptimizationRuntime was internal to react-web-sdk; the new public location is additive.

Test plan

  • pnpm implementation:run -- web-sdk_angular typecheck — clean
  • pnpm exec eslint implementations/web-sdk_angular/src/ — clean
  • pnpm exec prettier --check implementations/web-sdk_angular/src/ — clean
  • pnpm --filter @contentful/optimization-web typecheck + test:unit — clean
  • pnpm --filter @contentful/optimization-react-web typecheck + test:unit — clean
  • pnpm build:pkgs — clean
  • pnpm setup:e2e:web-sdk_angular && pnpm test:e2e:web-sdk_angular — Playwright SSR/first-paint suite (please verify in CI)
  • Manual dev-server smoke: pnpm implementation:run -- web-sdk_angular dev, exercise home + page-two routes, consent toggle, identify/reset, live-updates toggle, preview panel

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🕺🏻

@phobetron Charles Hudson (phobetron) merged commit 0508673 into main Jul 7, 2026
34 checks passed
@phobetron Charles Hudson (phobetron) deleted the NT-3467-angular-align-with-355 branch July 7, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants