Skip to content

Commit 50bb1db

Browse files
committed
CMS-42559 Add InferredContentReference type and enhance src function for ContentReference handling
1 parent a045d1b commit 50bb1db

File tree

5 files changed

+98
-11
lines changed

5 files changed

+98
-11
lines changed

packages/optimizely-cms-sdk/src/graph/__test__/createQueryDAM.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('createFragment() with damEnabled for contentReference properties', ()
1717
});
1818
initContentTypeRegistry([ct1]);
1919

20+
// DAM disabled
2021
const result = await createFragment('ct1', new Set(), '', true, false);
2122

2223
// Should not include ContentReferenceItem fragments
@@ -57,6 +58,7 @@ describe('createFragment() with damEnabled for contentReference properties', ()
5758
});
5859
initContentTypeRegistry([ct1]);
5960

61+
// DAM enabled
6062
const result = await createFragment('ct1', new Set(), '', true, true);
6163

6264
// Should include all ContentReferenceItem fragments
@@ -96,6 +98,7 @@ describe('createFragment() with damEnabled for contentReference properties', ()
9698
});
9799
initContentTypeRegistry([ct1]);
98100

101+
// DAM disabled
99102
const result = await createFragment('ct1', new Set(), '', true, false);
100103

101104
expect(result.some((line) => line.includes('ContentReferenceItem'))).toBe(

packages/optimizely-cms-sdk/src/infer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ export type ContentReferenceItem =
9393
| PublicVideoAsset
9494
| PublicRawFileAsset;
9595

96+
export type InferredContentReference = {
97+
url: InferredUrl;
98+
item: ContentReferenceItem | null;
99+
};
100+
96101
/** Infers the Typescript type for each content type property */
97102
// prettier-ignore
98103
export type InferFromProperty<T extends AnyProperty> =
@@ -106,7 +111,7 @@ export type InferFromProperty<T extends AnyProperty> =
106111
: T extends LinkProperty ? { text: string | null, title: string | null, target: string | null, url: InferredUrl }
107112
: T extends IntegerProperty ? number
108113
: T extends FloatProperty ? number
109-
: T extends ContentReferenceProperty ? { url: InferredUrl, item: ContentReferenceItem | null }
114+
: T extends ContentReferenceProperty ? InferredContentReference
110115
: T extends ArrayProperty<infer E> ? InferFromProperty<E>[]
111116
: T extends ContentProperty ? {__typename: string, __viewname: string}
112117
: T extends ComponentProperty<infer E> ? Infer<E>

packages/optimizely-cms-sdk/src/react/server.tsx

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
ExperienceComponentNode,
1111
DisplaySettingsType,
1212
ExperienceCompositionNode,
13+
InferredContentReference,
1314
} from '../infer.js';
15+
import { Renditions } from '../model/assets.js';
1416
import { isComponentNode } from '../util/baseTypeUtil.js';
1517
import { parseDisplaySettings } from '../model/displayTemplates.js';
1618
import { getDisplayTemplateTag } from '../model/displayTemplateRegistry.js';
@@ -308,6 +310,15 @@ export function OptimizelyGridSection({
308310

309311
/** Get context-aware functions for preview */
310312
export function getPreviewUtils(opti: OptimizelyComponentProps['opti']) {
313+
/** Helper function to append preview token to URL */
314+
const appendPreviewToken = (url: string): string => {
315+
if (opti.__context?.preview_token) {
316+
const separator = url.includes('?') ? '&' : '?';
317+
return `${url}${separator}preview_token=${opti.__context.preview_token}`;
318+
}
319+
return url;
320+
};
321+
311322
return {
312323
/** Get the HTML data attributes required for a property */
313324
pa(property?: string | { key: string }) {
@@ -328,13 +339,81 @@ export function getPreviewUtils(opti: OptimizelyComponentProps['opti']) {
328339
}
329340
},
330341

331-
/** Appends the preview token to the provided image URL */
332-
src(url: string) {
333-
if (opti.__context?.preview_token) {
334-
const separator = url.includes('?') ? '&' : '?';
335-
return `${url}${separator}preview_token=${opti.__context.preview_token}`;
342+
/** Appends the preview token to the provided image URL or ContentReference */
343+
src(
344+
input: string | InferredContentReference | null | undefined,
345+
options?: { renditionName?: string }
346+
): string {
347+
if (!input) return '';
348+
349+
let url: string;
350+
if (typeof input === 'string') {
351+
url = input;
352+
} else {
353+
// If renditionName is specified, find the matching rendition
354+
if (
355+
options?.renditionName &&
356+
input.item &&
357+
'Renditions' in input.item
358+
) {
359+
const rendition = input.item.Renditions?.find(
360+
(r) => r.Name === options.renditionName
361+
);
362+
if (rendition?.Url) {
363+
url = rendition.Url;
364+
} else {
365+
// Fallback to default URL if rendition not found
366+
url = input.item?.Url ?? input.url?.default ?? '';
367+
}
368+
} else {
369+
// Prefer item.Url if available (DAM asset), otherwise use url.default
370+
url = input.item?.Url ?? input.url?.default ?? '';
371+
}
372+
}
373+
374+
return appendPreviewToken(url);
375+
},
376+
377+
/** Generates srcset from ContentReference renditions */
378+
srcset(input: InferredContentReference | null | undefined): string {
379+
if (!input?.item || !('Renditions' in input.item)) return '';
380+
381+
const renditions = input.item.Renditions;
382+
if (!renditions || renditions.length === 0) return '';
383+
384+
// Track seen widths to avoid duplicate width descriptors
385+
const seenWidths = new Set<number>();
386+
387+
const srcsetEntries = renditions
388+
.filter((r) => {
389+
if (!r.Url || !r.Width) return false;
390+
// Skip if we've already seen this width
391+
if (seenWidths.has(r.Width)) return false;
392+
seenWidths.add(r.Width);
393+
return true;
394+
})
395+
.map((r) => {
396+
const url = appendPreviewToken(r.Url!);
397+
return `${url} ${r.Width}w`;
398+
});
399+
400+
return srcsetEntries.join(', ');
401+
},
402+
403+
/** Gets the alt text from a ContentReference or returns the string as-is */
404+
alt(input: string | InferredContentReference | null | undefined): string {
405+
if (!input) return '';
406+
407+
if (typeof input === 'string') {
408+
return input;
409+
}
410+
411+
// Check if item has AltText property (PublicImageAsset or PublicVideoAsset)
412+
if (input.item && 'AltText' in input.item) {
413+
return input.item.AltText ?? '';
336414
}
337-
return url;
415+
416+
return '';
338417
},
339418
};
340419
}

samples/nextjs-template/src/components/AboutUs.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ const customHeadingTwo = (props: ElementProps) => {
3333
};
3434

3535
export default function AboutUs({ opti }: AboutUsProps) {
36-
const { src } = getPreviewUtils(opti);
36+
const { src, srcset } = getPreviewUtils(opti);
3737
return (
3838
<section className="about-us">
39-
{opti?.image?.url?.default && (
39+
{opti.image && (
4040
<div className="about-us-image">
41-
<Image src={src(opti.image.url.default)} alt="" fill={true} />
41+
<img src={src(opti.image)} alt="" />
4242
</div>
4343
)}
4444
<h2>{opti.heading}</h2>

samples/nextjs-template/src/components/SmallFeature.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default function SmallFeature({ opti }: Props) {
3131
<h3 {...pa('heading')}>{opti.heading}</h3>
3232
{opti.image?.url?.default && (
3333
<div style={{ position: 'relative' }}>
34-
<img src={src(opti.image.url.default)} alt="" {...pa('image')} />
34+
<img src={src(opti.image)} alt="" {...pa('image')} />
3535
</div>
3636
)}
3737
<div

0 commit comments

Comments
 (0)