From de517813f071cce3ec801d568805b7f9b12bdf42 Mon Sep 17 00:00:00 2001 From: Carlos Saito Date: Tue, 15 Apr 2025 14:36:07 +0200 Subject: [PATCH 1/8] Install React --- packages/optimizely-cms-sdk/package.json | 9 +++++++++ packages/optimizely-cms-sdk/tsconfig.json | 3 ++- pnpm-lock.yaml | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/optimizely-cms-sdk/package.json b/packages/optimizely-cms-sdk/package.json index 5ef820a8..1cd3a711 100644 --- a/packages/optimizely-cms-sdk/package.json +++ b/packages/optimizely-cms-sdk/package.json @@ -11,8 +11,17 @@ "author": "", "license": "ISC", "packageManager": "pnpm@10.7.0", + "peerDependencies": { + "react": "^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + }, "devDependencies": { "@types/node": "^22.13.14", + "@types/react": "^19", "typescript": "^5.8.2", "vitest": "^3.1.1" } diff --git a/packages/optimizely-cms-sdk/tsconfig.json b/packages/optimizely-cms-sdk/tsconfig.json index a3cb5253..a7cdd61b 100644 --- a/packages/optimizely-cms-sdk/tsconfig.json +++ b/packages/optimizely-cms-sdk/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "jsx": "react-jsx", "target": "es2016", "module": "commonjs", "moduleResolution": "node", @@ -7,7 +8,7 @@ "declaration": true, "sourceMap": true, "outDir": "./dist", - "esModuleInterop": false, + "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa3a0973..b1b69996 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,10 +52,17 @@ importers: version: 5.8.2 packages/optimizely-cms-sdk: + dependencies: + react: + specifier: ^19.0.0 + version: 19.1.0 devDependencies: '@types/node': specifier: ^22.13.14 version: 22.13.14 + '@types/react': + specifier: ^19 + version: 19.0.12 typescript: specifier: ^5.8.2 version: 5.8.2 From 6c52675b996fe731322aac1565e30d86119673ea Mon Sep 17 00:00:00 2001 From: Carlos Saito Date: Tue, 15 Apr 2025 14:38:34 +0200 Subject: [PATCH 2/8] Create a component registry --- .../src/render/component-registry.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/optimizely-cms-sdk/src/render/component-registry.ts diff --git a/packages/optimizely-cms-sdk/src/render/component-registry.ts b/packages/optimizely-cms-sdk/src/render/component-registry.ts new file mode 100644 index 00000000..6b0b0883 --- /dev/null +++ b/packages/optimizely-cms-sdk/src/render/component-registry.ts @@ -0,0 +1,21 @@ +export type ComponentResolver = + | Record + | ((contentType: string) => C); + +/** A registry mapping content type names and components */ +export class ComponentRegistry { + resolver: ComponentResolver; + + constructor(resolver: ComponentResolver) { + this.resolver = resolver; + } + + /** Returns the component given its content type name. Returns `undefined` if not found */ + getComponent(contentType: string): T { + if (typeof this.resolver === 'object') { + return this.resolver[contentType]; + } else { + return this.resolver(contentType); + } + } +} From f22ea070b82f8e97773e530d8643e132d56f3f53 Mon Sep 17 00:00:00 2001 From: Carlos Saito Date: Tue, 15 Apr 2025 14:39:24 +0200 Subject: [PATCH 3/8] Add react-specific implementation of renderer --- .../optimizely-cms-sdk/src/render/react.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 packages/optimizely-cms-sdk/src/render/react.tsx diff --git a/packages/optimizely-cms-sdk/src/render/react.tsx b/packages/optimizely-cms-sdk/src/render/react.tsx new file mode 100644 index 00000000..84b215d4 --- /dev/null +++ b/packages/optimizely-cms-sdk/src/render/react.tsx @@ -0,0 +1,36 @@ +'use server'; +import type React from 'react'; +import { ComponentRegistry, ComponentResolver } from './component-registry'; + +type ComponentType = React.ComponentType; + +let componentRegistry: ComponentRegistry; + +type InitOptions = { + resolver: ComponentResolver; +}; + +type Props = { + opti: { + __typename: string; + }; +}; + +export function initReactComponentRegistry(options: InitOptions) { + componentRegistry = new ComponentRegistry(options.resolver); +} + +export async function OptimizelyComponent({ opti, ...props }: Props) { + if (!componentRegistry) { + throw new Error('You should call `initReactComponentRegistry` first'); + } + + const contentType = opti.__typename; + const Component = await componentRegistry.getComponent(contentType); + + if (!Component) { + return
No component found for content type {contentType}
; + } + + return ; +} From 5851ffac0a330beed3f5d7a7056c8757cf3e7d31 Mon Sep 17 00:00:00 2001 From: Carlos Saito Date: Tue, 15 Apr 2025 14:40:09 +0200 Subject: [PATCH 4/8] Query __typename --- packages/optimizely-cms-sdk/src/graph/createQuery.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/optimizely-cms-sdk/src/graph/createQuery.ts b/packages/optimizely-cms-sdk/src/graph/createQuery.ts index cb81c994..3712a92f 100644 --- a/packages/optimizely-cms-sdk/src/graph/createQuery.ts +++ b/packages/optimizely-cms-sdk/src/graph/createQuery.ts @@ -84,6 +84,7 @@ export async function createQuery(contentType: string, customImport: Importer) { query FetchContent($filter: _ContentWhereInput) { _Content(where: $filter) { item { + __typename ...${contentType} } } From 3a000c75e69d373b2d464c8fb6c9f917c1c8da6d Mon Sep 17 00:00:00 2001 From: Carlos Saito Date: Tue, 15 Apr 2025 14:40:36 +0200 Subject: [PATCH 5/8] Add render page in the sample site --- .../src/app/render/[slug]/page.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 samples/nextjs-template/src/app/render/[slug]/page.tsx diff --git a/samples/nextjs-template/src/app/render/[slug]/page.tsx b/samples/nextjs-template/src/app/render/[slug]/page.tsx new file mode 100644 index 00000000..c8679d44 --- /dev/null +++ b/samples/nextjs-template/src/app/render/[slug]/page.tsx @@ -0,0 +1,32 @@ +import { GraphClient } from 'optimizely-cms-sdk/dist/graph'; +import { + initReactComponentRegistry, + OptimizelyComponent, +} from 'optimizely-cms-sdk/dist/render/react'; +import React from 'react'; + +initReactComponentRegistry({ + resolver(contentType) { + return React.lazy(() => import(`../../../components/${contentType}.tsx`)); + }, +}); + +async function myImport(contentType: string) { + return import(`../../../components/${contentType}.tsx`).then( + (r) => r.ContentType + ); +} + +type Props = { + params: Promise<{ + slug: string; + }>; +}; + +export default async function Page({ params }: Props) { + const { slug } = await params; + const client = new GraphClient(process.env.GRAPH_SINGLE_KEY!, myImport); + const c = await client.fetchContent(`/${slug}/`); + + return ; +} From 9a30c1f42bff704703f0f98e34280edf4760f13a Mon Sep 17 00:00:00 2001 From: Carlos Saito Date: Tue, 15 Apr 2025 14:41:00 +0200 Subject: [PATCH 6/8] Add React component for Landing content type --- samples/nextjs-template/src/components/Landing.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/samples/nextjs-template/src/components/Landing.tsx b/samples/nextjs-template/src/components/Landing.tsx index c252461d..316cd0d0 100644 --- a/samples/nextjs-template/src/components/Landing.tsx +++ b/samples/nextjs-template/src/components/Landing.tsx @@ -1,4 +1,4 @@ -import { contentType } from 'optimizely-cms-sdk'; +import { contentType, Infer } from 'optimizely-cms-sdk'; export const ContentType = contentType({ key: 'Landing', @@ -27,3 +27,11 @@ export const ContentType = contentType({ }, }, }); + +type Props = { + opti: Infer; +}; + +export default function LandingComponent({ opti }: Props) { + return
{opti.heading}
; +} From afe51a635cca4ba0506e58744c1d954bc27229b7 Mon Sep 17 00:00:00 2001 From: Carlos Saito Date: Tue, 15 Apr 2025 14:57:46 +0200 Subject: [PATCH 7/8] Add LandingSection --- .../src/components/Landing.tsx | 11 ++++++++- .../src/components/LandingSection.tsx | 24 ++++++++++++------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/samples/nextjs-template/src/components/Landing.tsx b/samples/nextjs-template/src/components/Landing.tsx index 316cd0d0..11c93940 100644 --- a/samples/nextjs-template/src/components/Landing.tsx +++ b/samples/nextjs-template/src/components/Landing.tsx @@ -1,4 +1,5 @@ import { contentType, Infer } from 'optimizely-cms-sdk'; +import { OptimizelyComponent } from 'optimizely-cms-sdk/dist/render/react'; export const ContentType = contentType({ key: 'Landing', @@ -33,5 +34,13 @@ type Props = { }; export default function LandingComponent({ opti }: Props) { - return
{opti.heading}
; + return ( +
+

{opti.heading}

+

{opti.summary}

+ {opti.sections.map((section) => ( + + ))} +
+ ); } diff --git a/samples/nextjs-template/src/components/LandingSection.tsx b/samples/nextjs-template/src/components/LandingSection.tsx index b901b4d8..4d7afaa5 100644 --- a/samples/nextjs-template/src/components/LandingSection.tsx +++ b/samples/nextjs-template/src/components/LandingSection.tsx @@ -1,4 +1,4 @@ -import { contentType, displayTemplate } from 'optimizely-cms-sdk'; +import { contentType, displayTemplate, Infer } from 'optimizely-cms-sdk'; export const ContentType = contentType({ key: 'LandingSection', @@ -11,13 +11,6 @@ export const ContentType = contentType({ subtitle: { type: 'string', }, - features: { - type: 'array', - items: { - type: 'content', - allowedTypes: ['SmallFeatureGrid'], - }, - }, }, }); @@ -25,7 +18,7 @@ export const DisplayTemplate = displayTemplate({ key: 'LandingSectionDisplayTemplate', isDefault: true, displayName: 'LandingSectionDisplayTemplate', - contentType: 'LandingSection', + baseType: 'component', settings: { background: { editor: 'select', @@ -44,3 +37,16 @@ export const DisplayTemplate = displayTemplate({ }, }, }); + +type Props = { + opti: Infer; +}; + +export default function LandingSection({ opti }: Props) { + return ( +
+

{opti.heading}

+

{opti.subtitle}

+
+ ); +} From 7bc34bc16896f294617bd1c9b7f29198e75d50b3 Mon Sep 17 00:00:00 2001 From: Carlos Saito Date: Thu, 17 Apr 2025 09:01:45 +0200 Subject: [PATCH 8/8] Add React `key` --- samples/nextjs-template/src/components/Landing.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/nextjs-template/src/components/Landing.tsx b/samples/nextjs-template/src/components/Landing.tsx index 11c93940..d532388f 100644 --- a/samples/nextjs-template/src/components/Landing.tsx +++ b/samples/nextjs-template/src/components/Landing.tsx @@ -38,8 +38,8 @@ export default function LandingComponent({ opti }: Props) {

{opti.heading}

{opti.summary}

- {opti.sections.map((section) => ( - + {opti.sections.map((section, i) => ( + ))}
);