Skip to content

feat(core): support heterogeneous optimized entry types - #413

Merged
Charles Hudson (phobetron) merged 1 commit into
mainfrom
NT-3870_allow-different-baseline-variant-types
Aug 5, 2026
Merged

feat(core): support heterogeneous optimized entry types#413
Charles Hudson (phobetron) merged 1 commit into
mainfrom
NT-3870_allow-different-baseline-variant-types

Conversation

@phobetron

@phobetron Charles Hudson (phobetron) commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Allow selected nt_variants entries to resolve to any linked Contentful content type instead of requiring the baseline and variant content types to match.
  • Represent every possible baseline and variant with one skeleton union, distributed through EntryFor<S, M, L> so Contentful chain modifiers and locales are preserved.
  • Propagate modeled entry unions through the Core, Node, Web, React Web, Next.js, and React Native resolution and presentation APIs.
  • Add isEntryOfContentType for safe TypeScript narrowing and expose contentTypeId through the iOS CTEntry adapter.
  • Keep long-lived event streams model-agnostic and separate reusable Web snapshot state and equality logic from controller lifecycle concerns.
  • Update reference implementations, SDK knowledge, READMEs, concepts, blueprints, and integration guides for TypeScript, GraphQL, and native consumers.

Consumer typing and compatibility

  • Same-content-type calls infer their entry type without explicit generic arguments.

  • Known heterogeneous models provide the baseline and all possible variant skeletons through the first generic argument:

    sdk.resolveOptimizedEntry<PageSkeleton | HeroSkeleton | CtaSkeleton>(baselineEntry)
  • The baseline, resolved entry, and optimized-entry metadata use that same distributed entry union. Consumers narrow at the rendering or handling boundary with isEntryOfContentType or sys.contentType.sys.id.

  • Chain modifiers and locale types are retained for every union member.

  • Open-ended integrations can use EntrySkeletonType when the complete content model is not known at compile time.

  • Existing generic parameter positions and resolver call shapes remain intact.

  • Baseline fallback, empty variants, selection ownership, tracking, metadata, and presentation retain their existing contracts.

Coverage

  • Core runtime coverage exercises cross-content resolution, same-type resolution, empty variants, unresolved links, and baseline fallback.
  • Package suites verify union distribution, modifiers, locales, managed fetching, metadata, handoff, snapshots, hooks, components, server rendering, and content-type narrowing.
  • Reference integrations exercise modeled entry rendering across Web, React Web, Next.js App Router, Next.js Pages Router, and React Native.
  • Native coverage verifies iOS content-type access and heterogeneous Android and iOS rendering guidance.

Validation

  • Formatting, package lint, implementation lint, package typechecks, and implementation typechecks.
  • Unit tests, package builds, package tarballs, generated declaration documentation, and bundle-size checks.
  • iOS Swift tests and Android SDK tests.
  • Downstream implementation typechecks and targeted React Web, Web SDK React, Next.js Pages Router, and Next.js App Router browser coverage.
  • SDK knowledge pointer validation and guide structure/link validation.

Line change breakdown

Compared with main, this branch changes 83 files: 2,427 additions and 1,014 deletions.

Category Added Deleted Net Churn Share
Runtime code 287 218 +69 505 14.7%
Types 610 207 +403 817 23.7%
Comments 55 75 -20 130 3.8%
Test code 425 13 +412 438 12.7%
Documentation 1,003 480 +523 1,483 43.1%
Blank lines 47 21 +26 68 2.0%
Total 2,427 1,014 +1,413 3,441 100%

Type counts cover declarations erased from emitted JavaScript; type annotations embedded in executable implementations count as runtime code. Test files and Markdown/READMEs are classified wholly as test code and documentation, respectively.

[NT-3870]

@phobetron
Charles Hudson (phobetron) force-pushed the NT-3870_allow-different-baseline-variant-types branch 2 times, most recently from 3d0b2d9 to bb2ae3f Compare August 4, 2026 19:39
@phobetron
Charles Hudson (phobetron) marked this pull request as ready for review August 5, 2026 07:54
@phobetron
Charles Hudson (phobetron) force-pushed the NT-3870_allow-different-baseline-variant-types branch 2 times, most recently from 0fb9ebd to 1f0fcb4 Compare August 5, 2026 13:10
@phobetron

Copy link
Copy Markdown
Collaborator Author

/review

@bito-code-review

Copy link
Copy Markdown

Functional Validation by Bito

SourceRequirement / Code AreaStatusNotes
NT-3870Support heterogeneous content types in optimized entry resolution so that variant entries can resolve to entries with different content types than the baseline✅ MetThe resolver in packages/universal/core-sdk/src/resolvers/OptimizedEntryResolver.ts no longer enforces content-type matching between baseline and variant entries. The removed check at step 11 (formerly requiring variant content type to match baseline) allows heterogeneous content types. A new test case 'resolves a linked variant with a different content type' (line 3893) confirms that when `baselineEntry` is type 'page' and `variantEntry` is type 'hero', resolution correctly returns the variant entry. The concept documentation in documentation/concepts/entry-personalization-and-variant-resolution.md explicitly states 'Selected variant entries can use any Contentful content type' and removes the previous constraint row 'Variant entry content type differs from baseline' from the resolution miss table.
NT-3870Provide TypeScript utilities for safe content type narrowing when working with optimized entry unions✅ MetThe isEntryOfContentType function is implemented in packages/universal/api-schemas/src/contentful/typeGuards.ts and exported from `/api-schemas` entrypoints across all SDK packages (react-web, nextjs, node, react-native). The function checks `sys.contentType.sys.id` to narrow entry union types. Usage examples appear throughout documentation guides (integrating-the-web-sdk-in-a-web-app.md, using-contentful-graphql-data-with-the-optimization-sdks.md) showing pattern like `isEntryOfContentType(entry, 'hero')` followed by type-safe field access. Comprehensive test coverage exists in packages/universal/api-schemas/src/contentful/typeGuards.test.ts.
NT-3870Expose content type identification through native platform entry adapters✅ MetThe CTEntry adapter in packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Contentful/CTEntry.swift now exposes a `contentTypeId` property (lines 2739-2742) that returns `entry.sys?.contentType?.sys.id`. This property enables content type identification on iOS, consistent with the TypeScript implementation. Test coverage is provided in packages/ios/ContentfulOptimization/Tests/ContentfulOptimizationTests/CTEntryTests.swift with test cases 'testContentTypeIdReturnsNestedContentTypeId' and 'testContentTypeIdReturnsNilWhenAbsent'. The iOS runtime documentation in documentation/concepts/ios-sdk-runtime-and-interaction-mechanics.md includes an example (lines 329-339) showing how to use `contentTypeId` with `hasField` for type-specific rendering.
ContentEntry.tsx, LiveEntryCard.tsx, useOptimizationResolver.ts, OptimizedEntry.tsx, and other component filesRemoved explicit type casts (`as ContentEntry`) from render prop parameters across React implementations (react-web-sdk, nextjs-sdk, react-native-sdk, web-sdk). The resolved entry is now used directly without casting, enabled by the skeleton union generic parameters propagating through the type system. Affected files include implementations/react-web-sdk/src/sections/*.tsx, implementations/nextjs-sdk_app-router/components/*.tsx, implementations/nextjs-sdk_pages-router/components/*.tsx, and packages/react-native-sdk/src/components/OptimizedEntry.tsx.⭕ Out of ScopeRemoved explicit type casts (`as ContentEntry`) from render prop parameters across React implementations (react-web-sdk, nextjs-sdk, react-native-sdk, web-sdk). The resolved entry is now used directly without casting, enabled by the skeleton union generic parameters propagating through the type system. Affected files include implementations/react-web-sdk/src/sections/*.tsx, implementations/nextjs-sdk_app-router/components/*.tsx, implementations/nextjs-sdk_pages-router/components/*.tsx, and packages/react-native-sdk/src/components/OptimizedEntry.tsx.

@bito-code-review

Copy link
Copy Markdown

Impact Analysis by Bito

Cross-Repository Impact Analysis
What Changed Impact of Change Suggested Review Actions
TypeScript SDK type system enhanced with generic EntryFor type and isEntryOfContentType guard for heterogeneous content type support - intra-repo: All SDK packages (react-web-sdk, web-sdk, nextjs-sdk, react-native-sdk, core-sdk, api-schemas): Generic type parameters S (EntrySkeletonType), M (ChainModifiers), L (LocaleCode) added to all entry resolution APIs. Components now require explicit type arguments when using heterogeneous content models.
- intra-repo: Consumer applications using TypeScript with strict typing: Breaking type change: Existing code using type casts (entry as ContentEntry) will continue to work but new skeleton-union pattern with isEntryOfContentType narrowing is recommended. Type inference may produce different results.
- Verify TypeScript compilation passes in all example implementations/ (nextjs-sdk_app-router, nextjs-sdk_pages-router, react-web-sdk, web-sdk_react)
- Confirm that generic type defaults (EntrySkeletonType, ChainModifiers, LocaleCode) provide backward compatibility for existing consumers
- Test that isEntryOfContentType type guard correctly narrows union types at compile time
- Validate that removed content-type equality check in getSelectedVariantEntry does not break existing experiences
iOS SDK CTEntry Swift type added contentTypeId property for content type discrimination - intra-repo: iOS SDK consumers using SwiftUI and UIKit: New property enables branching on content type after entry resolution, matching TypeScript SDK's isEntryOfContentType capability. Existing code using raw dictionary access continues to work. - Verify CTEntry.contentTypeId returns nil for entries without contentType (backward compatibility)
- Confirm documentation updates in ios.md and ios-swiftui.md reflect new usage patterns
Documentation updated across all SDKs to describe skeleton union pattern and content-type narrowing - intra-repo: All SDK documentation (documentation/authoring/blueprints/, documentation/guides/, documentation/internal/sdk-knowledge/): Extensive documentation changes explain new type-safe patterns for handling variants with different content types. Examples now show contentTypeId branching and isEntryOfContentType usage. - Review documentation for consistency across all SDK platforms (Web, React, Next.js, Node, iOS, Android, React Native)
- Verify code examples compile and run correctly
- Confirm troubleshooting sections address common type errors with new generic patterns
Code Paths Analyzed

Impact:
Major TypeScript type system enhancement enabling type-safe handling of heterogeneous content types (variants with different content types than baseline). Runtime behavior unchanged; compile-time type safety improved.

Flow:
Entry resolution APIs now accept generic skeleton type parameter S (can be union of multiple skeletons) → Resolver returns EntryFor<S, M, L> (distributed union of Entry types) → Consumers use isEntryOfContentType to narrow before accessing fields → Type-safe rendering of different content types

Direct Changes (Diff Files):
• packages/universal/core-sdk/src/resolvers/OptimizedEntryResolver.ts [3914-4136] — Added EntryFor type, updated ResolvedData to use EntryFor, removed content-type equality check in getSelectedVariantEntry
• packages/universal/api-schemas/src/contentful/typeGuards.ts [3507-3536] — Added isEntryOfContentType type guard for narrowing resolved entries by content type
• packages/web/frameworks/react-web-sdk/src/optimized-entry/OptimizedEntry.tsx [4956-5140] — Made OptimizedEntry component generic with S, M, L type parameters; added function overloads for baseline vs managed props
• packages/web/web-sdk/src/presentation/OptimizedEntryController.ts [5531-5902] — Made controller generic; extracted OptimizedEntrySnapshot to separate file
• packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Contentful/CTEntry.swift [2733-2747] — Added contentTypeId property for Swift content type discrimination

Repository Impact:
TypeScript SDK packages (react-web-sdk, web-sdk, nextjs-sdk, react-native-sdk): All entry resolution components and hooks now use generic types. Existing code without explicit type parameters uses defaults for backward compatibility.
Core SDK (core-sdk, api-schemas): Fundamental type changes to ResolvedData, OptimizedEntryMetadata, and resolver functions. EntryFor type enables distributed unions for heterogeneous content models.
iOS SDK: CTEntry type enhanced with contentTypeId property to enable content-type-based branching in Swift/SwiftUI.
Documentation: Comprehensive updates across all blueprints, guides, and SDK knowledge docs to explain new type patterns and content-type narrowing.

Cross-Repository Dependencies:
No external repository dependencies detected - optimization repo not indexed in BitoAIArchitect: Repository appears to be a standalone SDK package. Cross-repo impact limited to consumers of the published NPM packages and iOS SDK.

Database/Caching Impact:
• None

API Contract Violations:
• None detected. All changes are additive or type-level; runtime contracts preserved.

Infrastructure Dependencies:
• TypeScript 5.0+ recommended for best generic type inference support
• contentful.js SDK types (EntrySkeletonType, ChainModifiers, LocaleCode) used throughout

Additional Insights:
Developer experience: New pattern requires explicit type annotations for heterogeneous content models but provides compile-time safety. Single-content-type use cases benefit from type inference.
Bundle size: Type-level changes only; no runtime code added except isEntryOfContentType guard (few bytes).

Testing Recommendations

Frontend Impact:
• Verify TypeScript compilation in all example implementations (nextjs-sdk_app-router, nextjs-sdk_pages-router, react-web-sdk, web-sdk_react)
• Test that OptimizedEntry component accepts both generic and non-generic usage patterns
• Confirm isEntryOfContentType correctly narrows types in IDE autocomplete

Service Integration:
• Test entry resolution with variants having different content types than baseline
• Verify fallback behavior when variant content type differs from expected skeleton union
• Confirm that removed content-type equality check does not affect existing experiences

Data Serialization:
• Validate that EntryFor type correctly distributes over union types
• Test serialization/deserialization of resolved entries with heterogeneous types
• Verify that runtime event stream (model-agnostic) still works with typed entries

Privacy Compliance:
• No privacy-related changes detected

Backward Compatibility:
• Test existing code without explicit type parameters continues to compile
• Verify that type casts (as ContentEntry) still work for gradual migration
• Confirm default type parameters (EntrySkeletonType, ChainModifiers, LocaleCode) provide expected behavior

OAuth Functionality:
• None

Cross-Service Communication:
• No cross-service communication changes

Reliability Testing:
• None

Additional Insights:
• Add type-level tests for EntryFor distribution behavior with complex union types
• Test that iOS CTEntry.contentTypeId handles entries without contentType (returns nil)
• Verify Android SDK documentation mentions contentTypeId accessor for parity

Analysis based on known dependency patterns and edges. Actual impact may vary.

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Agent Run #4cd235

Actionable Suggestions - 4
  • packages/react-native-sdk/src/components/OptimizedEntry.tsx - 1
    • Remove unused BivariantCallbacks class · Line 12-20
  • documentation/guides/using-contentful-graphql-data-with-the-optimization-sdks.md - 1
  • packages/web/frameworks/nextjs-sdk/src/bound-component-types.ts - 1
  • packages/universal/core-sdk/src/CoreStatelessRequest.ts - 1
Additional Suggestions - 12
  • packages/universal/core-sdk/src/CoreStateless.test.ts - 1
    • Reference equality vs deep equality · Line 317-317
      The assertion uses `.toBe()` which performs strict reference equality (`Object.is()`). If `fetchOptimizedEntry` returns a new object instance even with semantically identical content, this test will fail. Use `.toEqual()` for deep value comparison instead.
  • packages/web/frameworks/react-web-sdk/src/optimized-entry/useOptimizedEntry.ts - 2
    • Missing public type export · Line 2-6
      The diff adds `EntryFor` to the public API surface via the import on line 4 but does not show it being re-exported from the package's `index.ts`. Verify that `EntryFor` is reachable from the package's public barrel export, otherwise consumers cannot reference this type in their own generic parameters.
    • Unsafe type cast · Line 214-216
      The type assertion `useOptimizationContext() as { readonly sdk?: OptimizedEntrySdk }` unsafely narrows the context type, discarding type information about the `error`, `isLive`, and `prefetchedManagedEntries` properties. Consider extracting the context first, then narrowing only the `sdk` property: `const context = useOptimizationContext(); const sdk = context.sdk as OptimizedEntrySdk | undefined;` This preserves type safety for the full context while safely narrowing only the property you need.
  • packages/react-native-sdk/src/hooks/useOptimizedEntry.ts - 1
    • Unused generic parameters in callback · Line 63-63
      The `BivariantCallbacks` pattern at line 63 defines an untyped `onEntryResolved` for implementation, while line 27 defines a typed variant in `UseOptimizedEntrySharedParams`. The callback is invoked at line 307 with the untyped `metadata`, making the generic parameters `` on line 27 inert. This creates confusion about what types the callback receives.
  • packages/react-native-sdk/src/hooks/useTapTracking.ts - 1
    • Inconsistent generics across tracking hooks · Line 24-28
      The `UseTapTrackingOptions` interface now accepts a generic `TEntry`, but `useViewportTracking` (same file, same module) still uses the unparameterized `Entry` type directly. Inconsistent generic signatures across sibling tracking hooks create divergence risk: if callers abstract over tracking behavior, they'd need incompatible type signatures. Align `UseViewportTrackingOptions` with the same `` pattern for consistency.
  • documentation/internal/sdk-knowledge/native/ios.md - 1
    • CWE-1284: Vague Reference · Line 170-174
      The phrase 'exposes stable identifiers and fields from the resolver's JSON shape' is ambiguous — CTEntry exposes Contentful.Entry's own surface, not the resolver's JSON. 'Stable identifiers' is undefined terminology that adds no precision.
  • packages/web/frameworks/react-web-sdk/src/optimized-entry/OptimizedEntry.tsx - 1
    • Duplicate overload signature · Line 406-406
      Line 406 declares an overload signature that is identical to the implementation signature at line 407. This duplicate has no effect since the implementation immediately follows. Remove line 406 to eliminate redundant code.
  • documentation/internal/sdk-knowledge/web/web.md - 1
    • Inconsistent cross-reference anchor · Line 118-118
      Line 118 now links to `#entry-resolution`, but line 222 in the same file still links to `#baseline-fallback`. These should be consistent. Either update both to the same anchor or revert line 118 to use `#baseline-fallback`.
  • packages/web/frameworks/react-web-sdk/src/hooks/useEntryResolver.ts - 1
    • Potential API confusion · Line 39-39
      The interface declares `resolveEntryData` with the same type as `resolveOptimizedEntry` (line 18), but the JSDoc describes it as returning entry plus optimization metadata. Verify this is intentional — both methods return identical types, making the distinction unclear.
  • documentation/guides/integrating-the-react-web-sdk-in-a-react-app.md - 1
    • Ambiguous capability description · Line 467-470
      The statement "A selected variant can use any Contentful content type; selection depends on..." uses "can use" which implies capability rather than the intended meaning that variant content types are a separate concern from baseline content types. Reword to "A selected variant's content type is independent of the baseline's content type; selection depends on..." to make the design intent unambiguous.
  • packages/web/web-sdk/src/presentation/OptimizedEntryController.ts - 2
    • Generic class backward compatible · Line 219-269
      The class `OptimizedEntryController` is now generic with default type parameters ``. This ensures backward compatibility - existing code using `new OptimizedEntryController(options)` will continue to work without changes since defaults match the original non-generic behavior.
    • Dead code removal verified · Line 259-318
      Removed private comparison functions (`areHostAttributesEqual`, `areLoadingPresentationsEqual`, `areSnapshotMetadataEqual`, `areSnapshotValuesEqual`, `areSnapshotsEqual`) are not exported from the module. Verified via diff that no `export` keyword was present on these functions. Safe to remove with no external API impact.
Review Details
  • Files reviewed - 83 · Commit Range: 1f0fcb4..1f0fcb4
    • documentation/authoring/blueprints/ios-swiftui.md
    • documentation/authoring/blueprints/nextjs-app-router.md
    • documentation/authoring/blueprints/nextjs-pages-router.md
    • documentation/authoring/blueprints/node.md
    • documentation/authoring/blueprints/react-native.md
    • documentation/authoring/blueprints/react-web.md
    • documentation/concepts/android-sdk-runtime-and-interaction-mechanics.md
    • documentation/concepts/entry-personalization-and-variant-resolution.md
    • documentation/concepts/ios-sdk-runtime-and-interaction-mechanics.md
    • documentation/guides/integrating-the-node-sdk-in-a-node-app.md
    • documentation/guides/integrating-the-optimization-android-sdk-in-a-compose-app.md
    • documentation/guides/integrating-the-optimization-android-sdk-in-a-views-app.md
    • documentation/guides/integrating-the-optimization-ios-sdk-in-a-swiftui-app.md
    • documentation/guides/integrating-the-optimization-ios-sdk-in-a-uikit-app.md
    • documentation/guides/integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md
    • documentation/guides/integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md
    • documentation/guides/integrating-the-react-native-sdk-in-a-react-native-app.md
    • documentation/guides/integrating-the-react-web-sdk-in-a-react-app.md
    • documentation/guides/integrating-the-web-sdk-in-a-web-app.md
    • documentation/guides/using-contentful-graphql-data-with-the-optimization-sdks.md
    • documentation/internal/sdk-knowledge/native/ios.md
    • documentation/internal/sdk-knowledge/native/react-native.md
    • documentation/internal/sdk-knowledge/shared/concepts.md
    • documentation/internal/sdk-knowledge/web/nextjs-app-router.md
    • documentation/internal/sdk-knowledge/web/nextjs-pages-router.md
    • documentation/internal/sdk-knowledge/web/react-web.md
    • documentation/internal/sdk-knowledge/web/web.md
    • implementations/nextjs-sdk_app-router/app/(static)/analytics-only/[segment]/page.tsx
    • implementations/nextjs-sdk_app-router/app/(static)/selection-handoff/[segment]/page.tsx
    • implementations/nextjs-sdk_app-router/components/EntryCard.tsx
    • implementations/nextjs-sdk_app-router/components/LiveEntryCard.tsx
    • implementations/nextjs-sdk_pages-router/components/EntryCard.tsx
    • implementations/nextjs-sdk_pages-router/components/LiveEntryCard.tsx
    • implementations/nextjs-sdk_pages-router/pages/selection-handoff/[segment].tsx
    • implementations/react-web-sdk/src/sections/ContentEntry.tsx
    • implementations/react-web-sdk/src/sections/LiveUpdatesExampleEntry.tsx
    • implementations/react-web-sdk/src/sections/NestedContentItem.tsx
    • implementations/web-sdk_react/src/optimization/hooks/useOptimizationResolver.ts
    • packages/ios/ContentfulOptimization/README.md
    • packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Contentful/CTEntry.swift
    • packages/ios/ContentfulOptimization/Tests/ContentfulOptimizationTests/CTEntryTests.swift
    • packages/node/node-sdk/src/ContentfulOptimization.test.ts
    • packages/react-native-sdk/README.md
    • packages/react-native-sdk/src/components/OptimizedEntry.tsx
    • packages/react-native-sdk/src/hooks/useEntryResolver.ts
    • packages/react-native-sdk/src/hooks/useOptimizedEntry.test.tsx
    • packages/react-native-sdk/src/hooks/useOptimizedEntry.ts
    • packages/react-native-sdk/src/hooks/useTapTracking.ts
    • packages/universal/api-schemas/README.md
    • packages/universal/api-schemas/src/contentful/typeGuards.test.ts
    • packages/universal/api-schemas/src/contentful/typeGuards.ts
    • packages/universal/core-sdk/README.md
    • packages/universal/core-sdk/src/CoreBase.ts
    • packages/universal/core-sdk/src/CoreStateful.test.ts
    • packages/universal/core-sdk/src/CoreStateless.test.ts
    • packages/universal/core-sdk/src/CoreStatelessRequest.ts
    • packages/universal/core-sdk/src/OptimizedEntryMetadata.ts
    • packages/universal/core-sdk/src/handoff.ts
    • packages/universal/core-sdk/src/resolvers/OptimizedEntryResolver.test.ts
    • packages/universal/core-sdk/src/resolvers/OptimizedEntryResolver.ts
    • packages/universal/core-sdk/src/runtime/SnapshotRuntime.test.ts
    • packages/web/frameworks/nextjs-sdk/README.md
    • packages/web/frameworks/nextjs-sdk/src/app-router-client.ts
    • packages/web/frameworks/nextjs-sdk/src/app-router-server.tsx
    • packages/web/frameworks/nextjs-sdk/src/bound-component-types.ts
    • packages/web/frameworks/nextjs-sdk/src/pages-router.ts
    • packages/web/frameworks/nextjs-sdk/src/runtime-types.test.ts
    • packages/web/frameworks/nextjs-sdk/src/server-entry-renderer.tsx
    • packages/web/frameworks/nextjs-sdk/src/server.tsx
    • packages/web/frameworks/nextjs-sdk/src/tracking-attributes.ts
    • packages/web/frameworks/react-web-sdk/README.md
    • packages/web/frameworks/react-web-sdk/src/hooks/useEntryResolver.ts
    • packages/web/frameworks/react-web-sdk/src/index.ts
    • packages/web/frameworks/react-web-sdk/src/optimized-entry/OptimizedEntry.test.tsx
    • packages/web/frameworks/react-web-sdk/src/optimized-entry/OptimizedEntry.tsx
    • packages/web/frameworks/react-web-sdk/src/optimized-entry/optimizedEntryUtils.ts
    • packages/web/frameworks/react-web-sdk/src/optimized-entry/useOptimizedEntry.ts
    • packages/web/web-sdk/README.md
    • packages/web/web-sdk/src/presentation/OptimizedEntryController.test.ts
    • packages/web/web-sdk/src/presentation/OptimizedEntryController.ts
    • packages/web/web-sdk/src/presentation/OptimizedEntryLoadingPresentation.ts
    • packages/web/web-sdk/src/presentation/OptimizedEntrySnapshot.ts
    • packages/web/web-sdk/src/presentation/OptimizedEntryTrackingAttributes.ts
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Default Agent You can customize the agent settings here or contact your Bito workspace admin at jared.jolton@contentful.com.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines 12 to +20
import { useTapTracking } from '../hooks/useTapTracking'
import { useViewportTracking } from '../hooks/useViewportTracking'

declare class BivariantCallbacks {
onEntryResolved(metadata: OptimizedEntryMetadata): void
onTap(entry: Entry): void
render(entry: Entry, metadata: OptimizedEntryMetadata): ReactNode
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remove unused BivariantCallbacks class

The BivariantCallbacks class is declared but not exported or referenced anywhere in the visible code. This appears to be dead code or an incomplete refactoring artifact. Either remove it if unused, or ensure it's properly integrated.

Code suggestion
Check the AI-generated fix before applying
Suggested change
import { useTapTracking } from '../hooks/useTapTracking'
import { useViewportTracking } from '../hooks/useViewportTracking'
declare class BivariantCallbacks {
onEntryResolved(metadata: OptimizedEntryMetadata): void
onTap(entry: Entry): void
render(entry: Entry, metadata: OptimizedEntryMetadata): ReactNode
}
Suggested based on your custom review guideline "Review Posture"

Code Review Run #4cd235


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No change needed. BivariantCallbacks is referenced by OptimizedEntryImplementationProps for children, onEntryResolved, and onTap. Its method types intentionally provide bivariance at the erased implementation boundary while the public overloads retain their generic callback types. It is type-only and emits no runtime code; removing it would undo that compatibility mechanism.

Comment on lines +264 to 281
function toVariantEntry(variant: GraphQlVariant): Entry<EntrySkeletonType> {
switch (variant.__typename) {
case 'Hero':
return entryLike(variant, 'hero', {
headline: variant.headline,
})
case 'CallToAction':
return entryLike(variant, 'callToAction', {
label: variant.label,
})
case 'Page':
return entryLike(variant, 'page', {
title: variant.title,
slug: variant.slug,
heroHeadline: variant.heroHeadline,
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing default case in variant switch

The toVariantEntry function's switch statement handles 'Hero', 'CallToAction', and 'Page' cases but lacks a default case. If GraphQL data contains a variant with an unexpected __typename value, the function implicitly returns undefined, which could cause downstream errors when the variant is processed.

Code suggestion
Check the AI-generated fix before applying
Suggested change
function toVariantEntry(variant: GraphQlVariant): Entry<EntrySkeletonType> {
switch (variant.__typename) {
case 'Hero':
return entryLike(variant, 'hero', {
headline: variant.headline,
})
case 'CallToAction':
return entryLike(variant, 'callToAction', {
label: variant.label,
})
case 'Page':
return entryLike(variant, 'page', {
title: variant.title,
slug: variant.slug,
heroHeadline: variant.heroHeadline,
})
}
}
function toVariantEntry(variant: GraphQlVariant): Entry<EntrySkeletonType> {
switch (variant.__typename) {
case 'Hero':
return entryLike(variant, 'hero', {
headline: variant.headline,
})
case 'CallToAction':
return entryLike(variant, 'callToAction', {
label: variant.label,
})
case 'Page':
return entryLike(variant, 'page', {
title: variant.title,
slug: variant.slug,
heroHeadline: variant.heroHeadline,
})
default:
throw new Error(`Unknown variant type: ${variant.__typename}`)
}
}
Suggested based on your custom review guideline "Review Posture"

Code Review Run #4cd235


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No change needed. GraphQlVariant is a closed discriminated union containing exactly Page, Hero, and CallToAction, so the switch is exhaustive for its declared input. Adding the proposed default throw would turn unsupported runtime data into an exception, contrary to the resolver's fail-soft fallback. If this example needed runtime GraphQL validation, that validation would belong at the response boundary rather than as a default branch here.

Comment on lines +67 to +88
export type NextjsBoundOptimizedEntryBaselineProps<
S extends EntrySkeletonType = EntrySkeletonType,
M extends ChainModifiers = ChainModifiers,
L extends LocaleCode = LocaleCode,
> = Omit<OptimizedEntryBaselineProps<S, M, L>, 'liveUpdates' | 'loadingFallback'>

export type NextjsBoundOptimizedEntryManagedProps<
S extends EntrySkeletonType = EntrySkeletonType,
L extends LocaleCode = LocaleCode,
> = Omit<OptimizedEntryManagedProps<S, L>, 'liveUpdates' | 'loadingFallback'>

export type NextjsBoundOptimizedEntryProps<
S extends EntrySkeletonType = EntrySkeletonType,
M extends ChainModifiers = ChainModifiers,
L extends LocaleCode = LocaleCode,
> = DistributiveOmit<OptimizedEntryProps<S, M, L>, 'liveUpdates' | 'loadingFallback'>

export type NextjsServerOptimizedEntryProps<
S extends EntrySkeletonType = EntrySkeletonType,
M extends ChainModifiers = ChainModifiers,
L extends LocaleCode = LocaleCode,
> = NextjsBoundOptimizedEntryProps<S, M, L>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing type exports

Three types are defined in the diff but never exported from the package entry points. NextjsBoundOptimizedEntryBaselineProps (line 67), NextjsBoundOptimizedEntryManagedProps (line 73), and NextjsBoundOptimizedEntryComponent (line 90) are dead code in the public API — they cannot be imported by consumers. Add them to the export type { ... } block in app-router-server.tsx (line 75-85) alongside the existing NextjsBoundOptimizedEntryProps export.

Citations

Code Review Run #4cd235


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No change needed. These are internal construction types for the bound component. The consumer-facing NextjsBoundOptimizedEntryProps union is already exported; the baseline/managed branches and NextjsBoundOptimizedEntryComponent support the implementation and emitted component declaration. Exporting each helper separately would add public API without a consumer requirement.

options: FetchOptimizedEntryOptions = {},
): Promise<FetchOptimizedEntryResult<S, undefined, L>> {
return await this.core.fetchOptimizedEntry<S, L>(entryId, {
): Promise<FetchOptimizedEntryResult> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing type parameters on return type

The implementation overload returns FetchOptimizedEntryResult without type parameters. Since FetchOptimizedEntryResult is an interface requiring three type parameters, this becomes FetchOptimizedEntryResult<unknown, unknown, unknown>, breaking type inference for callers. Compare with CoreBase.fetchOptimizedEntry at line 354 which correctly uses FetchOptimizedEntryResult<EntrySkeletonType, ChainModifiers>.

Code Review Run #4cd235


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No change needed. FetchOptimizedEntryResult defaults all three parameters to EntrySkeletonType, undefined, and LocaleCode; it does not become <unknown, unknown, unknown>. Callers see the preceding generic overload, while this is the non-public implementation signature. The current typecheck and compile-time contract tests pass.

@bito-code-review

Copy link
Copy Markdown

✅ Review Settings Overridden

Status: Overridden Successfully

Guidelines:

  • Accepted:

    • General : Review Posture, Repo Truth And Boundaries, Domain Invariants

Note: Extra guidelines beyond 3 general purpose guidelines and 1 language specific guideline per language are not processed. Guidelines are fetched from the source branch.

@phobetron

Copy link
Copy Markdown
Collaborator Author

Bito review follow-up: secondary suggestions

I reviewed all twelve secondary notes that do not have individual review threads:

  • Reference equality vs deep equality — no change. The test intentionally verifies that explicit baseline selection returns the exact fetched baseline object. .toEqual() would weaken that contract.
  • Missing EntryFor root export — no change. An internal import does not require a React Web root export. EntryFor is already available through the package's public /core-sdk pass-through, and consumers do not need to name it to provide a skeleton union.
  • Whole-context type assertion — no change. The expression destructures only sdk; no other context field is read or lost at runtime. Narrowing the property in a second statement has the same type and runtime behavior.
  • React Native callback generics are inert — no change. The public callback retains <S, M, L>. Bivariance is used only at the non-public implementation boundary so the overloads can share one implementation.
  • Viewport tracking should copy tap-tracking generics — no change. Tap tracking returns the entry through onTap, so its generic is observable. Viewport tracking only consumes the entry; adding a generic there would be inert API surface.
  • iOS KB wording — accepted; pending branch update. The local follow-up removes the vague interface-oriented opening and retains only the precise contentTypeId runtime behavior.
  • Duplicate OptimizedEntry overload — no change. TypeScript implementation signatures are not visible to callers. The apparent duplicate is the catch-all public overload for values already typed as OptimizedEntryProps.
  • KB anchors should match — no change. The links intentionally target different shared facts: entry modeling uses #entry-resolution; fallback behavior uses #baseline-fallback. Both anchors exist.
  • resolveEntryData duplicates resolveOptimizedEntry — no change. This is a pre-existing compatibility alias for the full resolver payload. resolveEntry is the distinct entry-only helper.
  • “Can use any content type” is ambiguous — no change. The same sentence immediately defines the rule: selection depends on the selected variant ID and resolved linked entry, not baseline content-type equality.
  • Generic controller is backward compatible — confirmation only; no change requested.
  • Extracted comparison helpers are safe — confirmation only; no change requested.

The exact PR head has passing typecheck, unit-test, build, native, and relevant E2E checks.

@phobetron
Charles Hudson (phobetron) dismissed bito-code-review[bot]’s stale review August 5, 2026 16:20

Bito's feedback has been reviewed and largely rejected.

Allow optimized Contentful entries to resolve selected variants whose
content type differs from the baseline entry.

Represent every possible baseline and variant with one skeleton union.
Distribute EntryFor over that union so Contentful chain modifiers and
locales are preserved while same-type consumers retain precise inference.

Propagate modeled entries through Core, Node, Web, React Web, Next.js, and
React Native resolution and presentation APIs. Keep long-lived event
streams model-agnostic and preserve baseline fallback and empty-variant
behavior.

Add isEntryOfContentType for narrowing modeled entry unions and expose
CTEntry.contentTypeId for native content-type switching. Separate reusable
Web snapshot state and equality logic from controller lifecycle concerns.

Update reference implementations, SDK knowledge, concepts, blueprints,
integration guides, READMEs, and compile-time and runtime coverage.

Line-change rundown:

| Area                                    | Files | Added | Removed | Net  |
| --------------------------------------- | ----: | ----: | ------: | ---: |
| SDK production source                   |    27 |   973 |     481 | +492 |
| Tests                                   |    11 |   425 |      13 | +412 |
| Reference implementations               |    11 |    26 |      40 |  -14 |
| Package READMEs                         |     7 |    20 |       2 |  +18 |
| Documentation, guides, and SDK knowledge |    27 |   988 |     478 | +510 |
| Total                                   |    83 | 2,432 |   1,014 | +1,418 |

[[NT-3870](https://contentful.atlassian.net/browse/NT-3870)]
@phobetron
Charles Hudson (phobetron) force-pushed the NT-3870_allow-different-baseline-variant-types branch from 1f0fcb4 to 82a1f4a Compare August 5, 2026 16:23
@phobetron
Charles Hudson (phobetron) merged commit 2a3a9c8 into main Aug 5, 2026
42 checks passed
@phobetron
Charles Hudson (phobetron) deleted the NT-3870_allow-different-baseline-variant-types branch August 5, 2026 16:40
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.

1 participant