Skip to content

Commit 4797dd7

Browse files
Wire optional PuzzleConfig.image into og:image/twitter:image + test coverage
- src/server/render.puzzlePage.web.tsx: extracted the description/ openGraphData/twitterData construction into a new, pure, exported buildPuzzlePageMetaData(webTitle, puzzleConfig) function (previously inline in renderPuzzlePage). This is specifically so it's directly unit testable without needing to invoke the full render pipeline, which requires a webpack build manifest not present in the test environment - there is no render.*.web.tsx unit test convention anywhere else in this repo to extend, so extracting the pure logic avoids inventing new render-pipeline test infrastructure just for this. openGraphData/ twitterData now conditionally spread in 'og:image'/'twitter:image' only when puzzleConfig.image is set - when it's unset, the keys are omitted entirely (not sent as an empty string or placeholder), matching how htmlPageTemplate's generateMetaTags() only emits a <meta> tag for keys actually present in the object (verified by reading its Object.entries()-based implementation before assuming this). - fixtures/manual/puzzlePage.ts: added samplePuzzleImageUrl (a clearly fixture-only placeholder image URL) and createPuzzleConfigWithImage(slug), a fixture-only helper returning a copy of a real registry PuzzleConfig with image set - used to exercise the with-image branch in tests without touching the real registry itself (which still has no image configured on any of the 6 V0 entries, per the previous commit). - src/server/render.puzzlePage.web.test.ts (new): 4 tests for buildPuzzlePageMetaData - description sourced from puzzleConfig, og:title/twitter:title from webTitle, og:image/twitter:image omitted entirely when image is unset, and included with the correct value when set (using the new fixture helper). tsc --noEmit clean, eslint clean, 5 suites / 51 tests passing across all Puzzle Page test files (no regressions). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 4af65b6 commit 4797dd7

3 files changed

Lines changed: 128 additions & 9 deletions

File tree

dotcom-rendering/fixtures/manual/puzzlePage.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { PuzzleConfig } from '../../src/model/puzzles/puzzleConfigs';
12
import { puzzleConfigs } from '../../src/model/puzzles/puzzleConfigs';
23
import type { FEPuzzlePageType } from '../../src/types/puzzlePage';
34
import type { PuzzleItem } from '../../src/types/puzzlesPage';
@@ -87,3 +88,29 @@ export const puzzlePageFixtures: Record<string, FEPuzzlePageType> = Object.keys(
8788
acc[slug] = createPuzzlePage(slug);
8889
return acc;
8990
}, {});
91+
92+
/**
93+
* A fixture-only illustrative preview/share image URL. None of the real
94+
* `puzzleConfigs` registry entries have a real image configured yet (see
95+
* docs/puzzle-page.md) - this exists purely so both the with-image and
96+
* without-image branches of Puzzle Page's OG/Twitter metadata have fixture
97+
* and test coverage, without inventing a placeholder image for the real
98+
* registry itself.
99+
*/
100+
export const samplePuzzleImageUrl =
101+
'https://i.guim.co.uk/img/media/fixture-only-example/puzzle-preview.jpg?width=1200&height=630&quality=85';
102+
103+
/**
104+
* Returns a copy of `slug`'s real `PuzzleConfig` with `image` set to
105+
* `samplePuzzleImageUrl` - a fixture-only variant for exercising the
106+
* with-image branch (the real registry entry itself is left untouched).
107+
*/
108+
export const createPuzzleConfigWithImage = (slug: string): PuzzleConfig => {
109+
const puzzleConfig = puzzleConfigs[slug];
110+
111+
if (!puzzleConfig) {
112+
throw new Error(`Unknown puzzle slug in fixture: ${slug}`);
113+
}
114+
115+
return { ...puzzleConfig, image: samplePuzzleImageUrl };
116+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
createPuzzleConfigWithImage,
3+
samplePuzzleImageUrl,
4+
} from '../../fixtures/manual/puzzlePage';
5+
import { puzzleConfigs } from '../model/puzzles/puzzleConfigs';
6+
import { buildPuzzlePageMetaData } from './render.puzzlePage.web';
7+
8+
describe('buildPuzzlePageMetaData', () => {
9+
const withoutImage = puzzleConfigs.wordiply!;
10+
const withImage = createPuzzleConfigWithImage('wordiply');
11+
12+
it('uses the puzzle config description as the page description', () => {
13+
const { description } = buildPuzzlePageMetaData(
14+
'wordiply | The Guardian',
15+
withoutImage,
16+
);
17+
expect(description).toBe(withoutImage.description);
18+
});
19+
20+
it('builds og:title/og:description and twitter:title/twitter:description from webTitle/description', () => {
21+
const { openGraphData, twitterData } = buildPuzzlePageMetaData(
22+
'wordiply | The Guardian',
23+
withoutImage,
24+
);
25+
26+
expect(openGraphData['og:title']).toBe('wordiply | The Guardian');
27+
expect(openGraphData['og:description']).toBe(withoutImage.description);
28+
expect(twitterData['twitter:title']).toBe('wordiply | The Guardian');
29+
expect(twitterData['twitter:description']).toBe(
30+
withoutImage.description,
31+
);
32+
});
33+
34+
it('omits og:image/twitter:image entirely when puzzleConfig.image is unset', () => {
35+
const { openGraphData, twitterData } = buildPuzzlePageMetaData(
36+
'wordiply | The Guardian',
37+
withoutImage,
38+
);
39+
40+
expect(openGraphData).not.toHaveProperty('og:image');
41+
expect(twitterData).not.toHaveProperty('twitter:image');
42+
});
43+
44+
it('includes og:image/twitter:image when puzzleConfig.image is set', () => {
45+
const { openGraphData, twitterData } = buildPuzzlePageMetaData(
46+
'wordiply | The Guardian',
47+
withImage,
48+
);
49+
50+
expect(openGraphData['og:image']).toBe(samplePuzzleImageUrl);
51+
expect(twitterData['twitter:image']).toBe(samplePuzzleImageUrl);
52+
});
53+
});

dotcom-rendering/src/server/render.puzzlePage.web.tsx

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,55 @@ import { renderToStringWithEmotion } from '../lib/emotion';
1111
import { polyfillIO } from '../lib/polyfill.io';
1212
import { extractNAV } from '../model/extract-nav';
1313
import { createGuardian } from '../model/guardian';
14+
import type { PuzzleConfig } from '../model/puzzles/puzzleConfigs';
1415
import type { Config } from '../types/configContext';
1516
import { htmlPageTemplate } from './htmlPageTemplate';
1617

1718
type Props = { puzzlePage: ResolvedPuzzlePage };
1819

20+
/**
21+
* Builds the SEO metadata for a Puzzle Page from its resolved
22+
* `PuzzleConfig` and `webTitle`: the `<meta name="description">` value,
23+
* plus `openGraphData`/`twitterData` for `htmlPageTemplate`'s
24+
* `generateMetaTags()`. Pulled out as a small, pure function (rather than
25+
* inlined in `renderPuzzlePage`) specifically so it's directly unit
26+
* testable without needing to invoke the full render pipeline (which
27+
* requires a webpack build manifest not present in the test environment -
28+
* there is no existing render.*.web.tsx unit test convention in this repo
29+
* to extend).
30+
*
31+
* `og:image`/`twitter:image` are only included when `puzzleConfig.image`
32+
* is set - DCR has no site-wide default/fallback share image for pages
33+
* without one (confirmed by investigation - see docs/puzzle-page.md), so
34+
* when `image` is unset these keys are omitted entirely rather than sent
35+
* empty or with a placeholder, matching `generateMetaTags()`'s behaviour
36+
* of only emitting a `<meta>` tag for keys actually present in the object.
37+
*/
38+
export const buildPuzzlePageMetaData = (
39+
webTitle: string,
40+
puzzleConfig: PuzzleConfig,
41+
): {
42+
description: string;
43+
openGraphData: Record<string, string>;
44+
twitterData: Record<string, string>;
45+
} => {
46+
const { description, image } = puzzleConfig;
47+
48+
return {
49+
description,
50+
openGraphData: {
51+
'og:title': webTitle,
52+
'og:description': description,
53+
...(image ? { 'og:image': image } : {}),
54+
},
55+
twitterData: {
56+
'twitter:title': webTitle,
57+
'twitter:description': description,
58+
...(image ? { 'twitter:image': image } : {}),
59+
},
60+
};
61+
};
62+
1963
export const renderPuzzlePage = ({
2064
puzzlePage,
2165
}: Props): { html: string; prefetchScripts: string[] } => {
@@ -68,15 +112,10 @@ export const renderPuzzlePage = ({
68112
unknownConfig: puzzlePage.config,
69113
});
70114

71-
const description = puzzlePage.puzzleConfig.description;
72-
const openGraphData = {
73-
'og:title': puzzlePage.webTitle,
74-
'og:description': description,
75-
};
76-
const twitterData = {
77-
'twitter:title': puzzlePage.webTitle,
78-
'twitter:description': description,
79-
};
115+
const { description, openGraphData, twitterData } = buildPuzzlePageMetaData(
116+
puzzlePage.webTitle,
117+
puzzlePage.puzzleConfig,
118+
);
80119

81120
return {
82121
html: htmlPageTemplate({

0 commit comments

Comments
 (0)