Skip to content

Commit 337c169

Browse files
Merge branch 'main' into puzzles/images-sizing-accesibility-setters
2 parents 638616d + f308726 commit 337c169

44 files changed

Lines changed: 4813 additions & 99 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ab-testing/config/abTests.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,26 @@ const ABTests: ABTest[] = [
144144
groups: ["control", "variant"],
145145
shouldForceMetricsCollection: false,
146146
},
147+
/**
148+
* Puzzles & Games rollout, tier v0 (the master switch).
149+
*
150+
* Gates the baseline Puzzles & Games experience: the new Puzzles Hub
151+
* page, and the 6 V0 puzzle pages (sudoku easy/medium/hard/killer,
152+
* word-wheel, wordiply). At v0, there is no archive, no calendar, no
153+
* progress indicators, no sign-in-to-track-progress prompt, no "more
154+
* from puzzles" rail, and the hub's sub-nav has no links yet.
155+
*
156+
* This is the master switch for the whole Puzzles & Games experience:
157+
* turning it off (or down to 0%) hides everything: the hub, the V0
158+
* puzzle pages, and (by the cumulative design below) every later tier
159+
* too, since v1/v2 only take effect when this is also enabled.
160+
*
161+
* See `puzzles-new-hub-v1`/`puzzles-new-hub-v2` below for the later
162+
* rollout tiers, and `src/lib/puzzlesHubVersionExperiment.ts` /
163+
* `src/lib/puzzlesHubExperiment.ts` in dotcom-rendering for the
164+
* corresponding cumulative gate-check helpers
165+
* (`isPuzzlesHubEnabled`/`isPuzzlesHubV1Enabled`/`isPuzzlesHubV2Enabled`).
166+
*/
147167
{
148168
name: "puzzles-new-hub",
149169
description: "Rollout of the new Puzzles Hub experience",
@@ -156,6 +176,68 @@ const ABTests: ABTest[] = [
156176
groups: ["control", "variant"],
157177
shouldForceMetricsCollection: false,
158178
},
179+
/**
180+
* Puzzles & Games rollout, tier v1 (w/c 12 Oct launch).
181+
*
182+
* Only takes effect when `puzzles-new-hub` (v0) is ALSO enabled for the
183+
* reader. This test does nothing on its own, by design, so the
184+
* rollout can never end up in an inconsistent state (e.g. v1 features
185+
* showing while the v0 baseline they build on is switched off).
186+
*
187+
* On top of v0, this tier activates: the full hub sub-nav links (to
188+
* /word-games, /logic-puzzles, /trivia-and-quizzes), a
189+
* sign-in-to-track-progress message, a calendar/archive view for
190+
* crosswords/logic-puzzles/word-games (not Wordiply, which has no
191+
* archive), progress indicators (Available/Completed), the "More from
192+
* Puzzles & Games" related-content rail, newsletter signup, and
193+
* changes to the existing crossword page (print CTA repositioning, a
194+
* "play other puzzles" container).
195+
*
196+
* To roll back from v1 to v0 without a deploy: flip this test's
197+
* `audienceSize` to `0 / 100` (or `status` to `"OFF"`) while leaving
198+
* `puzzles-new-hub` untouched.
199+
*/
200+
{
201+
name: "puzzles-new-hub-v1",
202+
description:
203+
"Rollout of the v1 Puzzles & Games features (w/c 12 Oct), on top of the puzzles-new-hub v0 baseline",
204+
owners: ["puzzles.team@guardian.co.uk"],
205+
status: "ON",
206+
expirationDate: "2026-12-31",
207+
type: "server",
208+
audienceSize: 0 / 100,
209+
audienceSpace: "A",
210+
groups: ["control", "variant"],
211+
shouldForceMetricsCollection: false,
212+
},
213+
/**
214+
* Puzzles & Games rollout, tier v2 (future, no launch date confirmed
215+
* yet as of this writing).
216+
*
217+
* Only takes effect when BOTH `puzzles-new-hub` (v0) AND
218+
* `puzzles-new-hub-v1` are ALSO enabled for the reader, same
219+
* cumulative-by-design principle as v1 above, applied one tier further.
220+
*
221+
* On top of v0+v1, this tier activates: the On the Ball and Film Reveal
222+
* iframe games (Trivia and Quizzes group), a "Most played" container,
223+
* EventKit-driven navigation, migrating existing crossword pages onto
224+
* the new Puzzle Page template, and search-engine mobile app nudges.
225+
*
226+
* Kept at 0% until that work begins; there is nothing to roll back yet.
227+
*/
228+
{
229+
name: "puzzles-new-hub-v2",
230+
description:
231+
"Rollout of the v2 Puzzles & Games features (no date confirmed yet), on top of the puzzles-new-hub/puzzles-new-hub-v1 baseline",
232+
owners: ["puzzles.team@guardian.co.uk"],
233+
status: "ON",
234+
expirationDate: "2026-12-31",
235+
type: "server",
236+
audienceSize: 0 / 100,
237+
audienceSpace: "A",
238+
groups: ["control", "variant"],
239+
shouldForceMetricsCollection: false,
240+
},
159241
{
160242
name: "identity-and-trust-consent-rr-banner-us",
161243
description:
@@ -215,7 +297,7 @@ const ABTests: ABTest[] = [
215297
status: "ON",
216298
audienceSize: 0 / 100,
217299
audienceSpace: "B",
218-
groups: ["control", "variant"],
300+
groups: ["control", "variant", "variant2"],
219301
shouldForceMetricsCollection: true,
220302
},
221303
{

dotcom-rendering/docs/puzzle-page.md

Lines changed: 716 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import type { PuzzleConfig } from '../../src/model/puzzles/puzzleConfigs';
2+
import { puzzleConfigs } from '../../src/model/puzzles/puzzleConfigs';
3+
import type { FEPuzzlePageType } from '../../src/types/puzzlePage';
4+
import type { PuzzleItem } from '../../src/types/puzzlesPage';
5+
import { Standard } from '../generated/fe-articles/Standard';
6+
7+
const sampleMoreFromPuzzlesAndGames: PuzzleItem[] = [
8+
{
9+
id: 'sudoku-easy-daily',
10+
title: 'Sudoku easy',
11+
type: 'sudoku-easy',
12+
set: 'all',
13+
cardVariant: 'compact',
14+
cadence: 'Daily',
15+
slug: 'sudoku-easy',
16+
},
17+
{
18+
id: 'word-wheel-daily',
19+
title: 'Word wheel',
20+
type: 'word-wheel',
21+
set: 'all',
22+
cardVariant: 'compact',
23+
cadence: 'Daily',
24+
slug: 'word-wheel',
25+
},
26+
];
27+
28+
/**
29+
* Illustrative canonical URL matching `frontend`'s public URL shape for
30+
* puzzle pages (top-level, mirroring crosswords, distinct from the
31+
* `/puzzles-and-games` hub), purely a fixture value, not something DCR
32+
* validates or enforces the shape of.
33+
*/
34+
const canonicalUrlForSlug = (slug: string): string => {
35+
const sudokuMatch = /^sudoku-(.+)$/.exec(slug);
36+
if (sudokuMatch) {
37+
return `https://www.theguardian.com/sudoku/${sudokuMatch[1]}`;
38+
}
39+
return `https://www.theguardian.com/${slug}`;
40+
};
41+
42+
/**
43+
* Builds a `FEPuzzlePageType` fixture for the given `slug`, defaulting to a
44+
* generic instance for that slug's `PuzzleConfig`. Pass `overrides` to
45+
* customise individual fields (deep-merged only one level for `instance`).
46+
*/
47+
export const createPuzzlePage = (
48+
slug: string,
49+
overrides: Partial<FEPuzzlePageType> = {},
50+
): FEPuzzlePageType => {
51+
const puzzleConfig = puzzleConfigs[slug];
52+
53+
if (!puzzleConfig) {
54+
throw new Error(`Unknown puzzle slug in fixture: ${slug}`);
55+
}
56+
57+
return {
58+
id: `puzzle-page-${slug}`,
59+
slug,
60+
webTitle: `${slug} | The Guardian`,
61+
config: {
62+
...Standard.config,
63+
contentType: 'Game',
64+
// DCR no longer gates /PuzzlePage on any AB test participation
65+
// (the former 'game-page-experiment' gate was removed; routes
66+
// will be mapped/exposed via a different project instead), so
67+
// this is left empty rather than implying any particular value
68+
// is required.
69+
serverSideABTests: {},
70+
},
71+
nav: Standard.nav,
72+
pageFooter: Standard.pageFooter,
73+
canonicalUrl: canonicalUrlForSlug(slug),
74+
editionId: Standard.editionId,
75+
instance: {
76+
title: `${slug} puzzle`,
77+
puzzleDate: '2026-09-11',
78+
moreFromPuzzlesAndGames: sampleMoreFromPuzzlesAndGames,
79+
},
80+
...overrides,
81+
};
82+
};
83+
84+
/** One fixture per supported slug, for local dev preview and tests. */
85+
export const puzzlePageFixtures: Record<string, FEPuzzlePageType> = Object.keys(
86+
puzzleConfigs,
87+
).reduce<Record<string, FEPuzzlePageType>>((acc, slug) => {
88+
acc[slug] = createPuzzlePage(slug);
89+
return acc;
90+
}, {});
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+
};

dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.stories.tsx

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,51 @@ const customSubNav: CustomSubnav = {
3333
pages: [],
3434
};
3535

36+
/** Distinct images per breakpoint so it's obvious which one is being served. */
37+
const customSubNavWithImages: CustomSubnav = {
38+
...customSubNav,
39+
images: [
40+
{
41+
breakpoint: 'mobile',
42+
platforms: ['web'],
43+
imageSrc:
44+
'https://media.guim.co.uk/6537e163c9164d25ec6102641f6a04fa5ba76560/0_210_5472_3283/master/5472.jpg?width=740&height=140&quality=85&fit=crop&s=none',
45+
},
46+
{
47+
breakpoint: 'tablet',
48+
platforms: ['web'],
49+
imageSrc:
50+
'https://media.guim.co.uk/56b42eef576bc04c820da710459acd91082bb37b/0_0_6720_4480/6720.jpg?width=980&height=140&quality=85&fit=crop&s=none',
51+
},
52+
{
53+
breakpoint: 'desktop',
54+
platforms: ['web'],
55+
imageSrc:
56+
'https://media.guim.co.uk/c981848745e482e03e23b2ec9402e1f5c5bee6a6/102_73_3282_1848/2000.jpg?width=1300&height=140&quality=85&fit=crop&s=none',
57+
},
58+
],
59+
};
60+
3661
const meta = {
3762
component: CustomSubNav,
3863
title: 'Components/Masthead/Titlepiece/CustomSubNav',
3964
decorators: [
40-
(Story) => (
41-
<div
42-
css={css`
43-
background-color: ${sourcePalette.brand[400]};
44-
padding: ${space[3]}px;
45-
`}
46-
>
47-
<Story />
48-
</div>
49-
),
65+
(Story, context) => {
66+
const hasImage =
67+
(context.args.customSubNav.images?.length ?? 0) > 0;
68+
return (
69+
<div
70+
css={css`
71+
background-color: ${hasImage
72+
? sourcePalette.neutral[100]
73+
: sourcePalette.brand[400]};
74+
padding: ${hasImage ? 0 : space[2]}px;
75+
`}
76+
>
77+
<Story />
78+
</div>
79+
);
80+
},
5081
],
5182
render: (args) => <CustomSubNav {...args} />,
5283
args: {
@@ -64,6 +95,14 @@ export const Front = {
6495
args: { renderingPage: 'front' },
6596
} satisfies Story;
6697

98+
/** On fronts, a web image is shown per breakpoint; resize the viewport to switch between mobile/tablet/desktop. */
99+
export const FrontWithImage = {
100+
args: {
101+
renderingPage: 'front',
102+
customSubNav: customSubNavWithImages,
103+
},
104+
} satisfies Story;
105+
67106
export const Article = {
68107
args: { renderingPage: 'article' },
69108
parameters: {

0 commit comments

Comments
 (0)