-
Notifications
You must be signed in to change notification settings - Fork 0
Nt 2570 create a react web vanilla reference implementation based on the web sdk #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
phobetron
merged 28 commits into
main
from
NT-2570-create-a-react-web-vanilla-reference-implementation-based-on-the-web-sdk
Feb 25, 2026
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
ed6827e
Add React web reference implementation scaffolding
Lotfi-Arif 6e423c5
Add Contentful client, env config, and types
Lotfi-Arif 5747be1
feat: Add createOptimization utility for Optimization SDK integration
Lotfi-Arif 21efd7c
feat: Add OptimizationProvider context for optimization SDK
Lotfi-Arif bdcd5ed
feat: Add useOptimization hook for optimization context
Lotfi-Arif db63126
feat: Add useOptimizationState hook for optimization state management
Lotfi-Arif 1c69cfa
refactor: App to load and render Contentful entries
Lotfi-Arif 5c0a7aa
feat: Add usePersonalization hook for entry personalization logic
Lotfi-Arif fdf7226
feat: Add content entry and analytics components with hooks
Lotfi-Arif 9d07279
feat: Update Contentful client and env config, add contentful dep
Lotfi-Arif 58a1168
feat: Add Playwright e2e tests and restructure entry display
Lotfi-Arif 6a2a421
refactor: format code for readability and fix import order issues
Lotfi-Arif e572489
Add Contentful rich text dependencies and update e2e test script
Lotfi-Arif 64e192c
refactor: Contentful types and client usage for clarity
Lotfi-Arif 5c13552
refactor: RichTextRenderer to use Contentful renderer package
Lotfi-Arif 40b6192
feat: remove env config module and use import.meta.env with defaults
Lotfi-Arif a410c0f
feat: add React Router v7 SPA navigation and page event tracking
Lotfi-Arif b2cc5af
Refactor web-react: minor formatting and type improvements
Lotfi-Arif c2b878c
feat: simplify embedded entry node condition in extractTextContent
Lotfi-Arif f8a29ea
feat: Add OptimizationInitializationError for better error handling
Lotfi-Arif 2577683
test: reload and verify profile reset button in e2e test
Lotfi-Arif bf1fb77
fix: formatting of error handling in createOptimization
Lotfi-Arif fa938e9
feat(test): Add tests for consent gating and unidentified state
Lotfi-Arif 45a055a
refactor: type guards, improve env var checks, and update docs
Lotfi-Arif 6784439
feat: Add E2E Web React job to CI pipeline
Lotfi-Arif 9cd094d
feat: add steps to start, stop, and log Web React app in CI
Lotfi-Arif dfa1e45
feat update Web React app startup and logging in CI workflow
Lotfi-Arif 1623ce3
Merge branch 'main' into NT-2570-create-a-react-web-vanilla-reference…
Lotfi-Arif File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| DOTENV_CONFIG_QUIET=true | ||
|
|
||
| PUBLIC_NINETAILED_CLIENT_ID="mock-client-id" | ||
| PUBLIC_NINETAILED_ENVIRONMENT="main" | ||
|
|
||
| PUBLIC_EXPERIENCE_API_BASE_URL="http://localhost:8000/experience/" | ||
| PUBLIC_INSIGHTS_API_BASE_URL="http://localhost:8000/insights/" | ||
|
|
||
| PUBLIC_CONTENTFUL_TOKEN="mock-token" | ||
| PUBLIC_CONTENTFUL_PREVIEW_TOKEN="mock-preview-token" | ||
| PUBLIC_CONTENTFUL_ENVIRONMENT="master" | ||
| PUBLIC_CONTENTFUL_SPACE_ID="mock-space-id" | ||
|
|
||
| PUBLIC_CONTENTFUL_CDA_HOST="localhost:8000" | ||
| PUBLIC_CONTENTFUL_BASE_PATH="contentful" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| shared-workspace-lockfile=false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| # Web React Reference Implementation | ||
|
|
||
| Reference implementation demonstrating `@contentful/optimization-web` usage in a React web | ||
| application. | ||
|
|
||
| > **Note:** This implementation uses [Rsbuild](https://rsbuild.dev/) for consistency with the SDK | ||
| > build tooling. If you're creating your own React application, you can use any build tool you | ||
| > prefer (Vite, Create React App, Next.js, etc.) — the SDK integration patterns demonstrated here | ||
| > will work the same way. | ||
|
|
||
| ## Overview | ||
|
|
||
| This implementation provides a thin React adapter layer over `@contentful/optimization-web`, | ||
| demonstrating: | ||
|
|
||
| - `OptimizationProvider` context for SDK state management | ||
| - React hooks for SDK state subscriptions | ||
| - Personalization resolution and variant rendering | ||
| - Rich Text rendering via `@contentful/rich-text-react-renderer` | ||
| - Analytics event tracking | ||
| - Live updates behavior | ||
| - SPA navigation tracking with React Router v7 | ||
| - Offline queue/recovery handling | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - Node.js >= 16.20.0 | ||
| - pnpm 10.x | ||
|
|
||
| ## Setup | ||
|
|
||
| From the **repository root**: | ||
|
|
||
| ```bash | ||
| # Build SDK packages (required for local development) | ||
| pnpm build:pkgs | ||
|
|
||
| # Install implementation dependencies | ||
| pnpm run implementation:run -- web-react implementation:install | ||
| ``` | ||
|
|
||
| ## Development | ||
|
|
||
| From the **repository root**: | ||
|
|
||
| ```bash | ||
| # Start development server | ||
| pnpm run implementation:web-react dev | ||
|
|
||
| # Build for production | ||
| pnpm run implementation:web-react build | ||
|
|
||
| # Preview production build | ||
| pnpm run implementation:web-react preview | ||
|
|
||
| # Type checking | ||
| pnpm run implementation:web-react typecheck | ||
| ``` | ||
|
|
||
| Or from the **implementation directory** (`implementations/web-react`): | ||
|
|
||
| ```bash | ||
| pnpm dev | ||
| pnpm build | ||
| pnpm preview | ||
| pnpm typecheck | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| ### E2E Tests | ||
|
|
||
| ```bash | ||
| # In terminal 1: start mocks + app preview | ||
| pnpm run implementation:web-react serve | ||
|
|
||
| # In terminal 2: run Playwright tests | ||
| pnpm run implementation:web-react test:e2e | ||
|
|
||
| # Interactive Playwright UI | ||
| pnpm run implementation:web-react test:e2e:ui | ||
|
|
||
| # Generate tests with Playwright codegen | ||
| pnpm run implementation:web-react test:e2e:codegen | ||
| ``` | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| Copy `.env.example` to `.env` and configure: | ||
|
|
||
| ```bash | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| See `.env.example` for available configuration options. The implementation reads from | ||
| `import.meta.env` directly and falls back to local mock-safe defaults, so it can run without extra | ||
| env wiring. To use local mock Contentful endpoints, set `PUBLIC_CONTENTFUL_CDA_HOST=localhost:8000` | ||
| and `PUBLIC_CONTENTFUL_BASE_PATH=contentful`. | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| web-react/ | ||
| ├── src/ | ||
| │ ├── main.tsx # Application entry point | ||
| │ ├── App.tsx # Root component | ||
| │ ├── optimization/ # SDK React adapter | ||
| │ │ ├── OptimizationProvider.tsx | ||
| │ │ ├── hooks/ | ||
| │ │ └── components/ | ||
| │ ├── pages/ # Route pages | ||
| │ └── components/ # UI components | ||
| ├── e2e/ # Playwright E2E tests | ||
| ├── public/ # Static assets | ||
| ├── index.html # HTML template | ||
| ├── rsbuild.config.ts # Rsbuild configuration | ||
| ├── tsconfig.json # TypeScript configuration | ||
| └── package.json | ||
| ``` | ||
|
|
||
| ## SDK Integration Patterns | ||
|
|
||
| This implementation demonstrates how to build a React adapter for `@contentful/optimization-web`. | ||
| Key patterns include: | ||
|
|
||
| ### Provider Setup | ||
|
|
||
| ```tsx | ||
| import { OptimizationProvider } from './optimization' | ||
|
|
||
| function App() { | ||
| return ( | ||
| <OptimizationProvider> | ||
| <YourApp /> | ||
| </OptimizationProvider> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| ### Using Hooks | ||
|
|
||
| ```tsx | ||
| import { usePersonalization, useOptimization } from './optimization' | ||
|
|
||
| function MyComponent() { | ||
| const { sdk, isReady } = useOptimization() | ||
| const { resolveEntry } = usePersonalization() | ||
| const resolved = resolveEntry(baseEntry) | ||
|
|
||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
| ### Analytics Tracking | ||
|
|
||
| ```tsx | ||
| import { useAnalytics } from './optimization' | ||
|
|
||
| function TrackedComponent() { | ||
| const { trackView } = useAnalytics() | ||
|
|
||
| useEffect(() => { | ||
| void trackView({ componentId: 'component-id' }) | ||
| }, []) | ||
|
|
||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
| ## Related | ||
|
|
||
| - [React Native Implementation](../react-native/) - Reference implementation for React Native | ||
| - [Web Vanilla Implementation](../web-vanilla/) - Reference implementation for vanilla JavaScript | ||
| - [@contentful/optimization-web](../../../platforms/javascript/web/) - Web SDK package |
42 changes: 42 additions & 0 deletions
42
implementations/web-react/e2e/displays-identified-user-variants.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { expect, test } from '@playwright/test' | ||
|
|
||
| test.describe('identified user', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto('/') | ||
| await page.waitForLoadState('domcontentloaded') | ||
| await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() | ||
|
|
||
| await page.getByRole('button', { name: 'Identify' }).click() | ||
| await expect(page.getByRole('button', { name: 'Reset Profile' })).toBeVisible() | ||
|
|
||
| await page.reload() | ||
| await page.waitForLoadState('domcontentloaded') | ||
| await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() | ||
| await expect(page.getByRole('button', { name: 'Reset Profile' })).toBeVisible() | ||
| }) | ||
|
|
||
| test('renders identified variants', async ({ page }) => { | ||
| await expect( | ||
| page.getByText('This is a variant content entry for identified users.'), | ||
| ).toBeVisible() | ||
| await expect(page.getByText('This is a level 0 nested variant entry.')).toBeVisible() | ||
| }) | ||
|
|
||
| test('reset persists unidentified state across reload', async ({ page }) => { | ||
| await page.getByRole('button', { name: 'Reset Profile' }).click() | ||
| await expect(page.getByRole('button', { name: 'Identify' })).toBeVisible() | ||
|
|
||
| await page.reload() | ||
| await page.waitForLoadState('domcontentloaded') | ||
| await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() | ||
|
|
||
| await expect(page.getByRole('button', { name: 'Identify' })).toBeVisible() | ||
| await expect( | ||
| page.getByText('This is a baseline content entry for all identified or unidentified users.'), | ||
| ).toBeVisible() | ||
| await expect(page.getByText('This is a level 0 nested baseline entry.')).toBeVisible() | ||
| await expect( | ||
| page.getByText('This is a variant content entry for identified users.'), | ||
| ).toHaveCount(0) | ||
| }) | ||
| }) |
24 changes: 24 additions & 0 deletions
24
implementations/web-react/e2e/displays-unidentified-user-variants.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { expect, test } from '@playwright/test' | ||
|
|
||
| test.describe('unidentified user', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto('/') | ||
| await page.waitForLoadState('domcontentloaded') | ||
| await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() | ||
| }) | ||
|
|
||
| test('renders utility panel and entries', async ({ page }) => { | ||
| await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible() | ||
| await expect(page.getByRole('heading', { name: 'Auto Observed Entries' })).toBeVisible() | ||
| await expect(page.getByRole('heading', { name: 'Manually Observed Entries' })).toBeVisible() | ||
|
|
||
| await expect( | ||
| page.getByText( | ||
| 'This is a merge tag content entry that displays the visitor\'s continent "EU" embedded within the text.', | ||
| ), | ||
| ).toBeVisible() | ||
|
|
||
| await expect(page.getByText('This is a level 0 nested baseline entry.')).toBeVisible() | ||
| await expect(page.getByText('This is a variant content entry for new visitors.')).toBeVisible() | ||
| }) | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check warning
Code scanning / CodeQL
Unpinned tag for a non-immutable Action in workflow Medium