Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1497feb
Refactor PuzzlePageLayout to mirror CrosswordLayout
andresilva-guardian Sep 16, 2026
1997a7f
Update Puzzle Page formatting, nav, and theming
andresilva-guardian Sep 17, 2026
acb9a9e
Refactor puzzle date formatting to use dateline-style and update rela…
andresilva-guardian Sep 17, 2026
6e8acec
Refactor iframe styles to use per-slug min-height configurations and …
andresilva-guardian Sep 17, 2026
169032f
Refactor min-height configurations for puzzles to include touch-devic…
andresilva-guardian Sep 17, 2026
ecb27c9
Refactor buildFrameStyles tests to update min-height values and add t…
andresilva-guardian Sep 17, 2026
afbc814
Adjust Puzzle Page layout by adding bottom margin to frame container …
andresilva-guardian Sep 17, 2026
ab6ce1a
Enable comments and disable ads on Puzzle Page layout
andresilva-guardian Sep 17, 2026
d341f8d
Add isAdFreeUser field to FEPuzzlePageType and update ad rendering logic
andresilva-guardian Sep 17, 2026
cd8b8b6
Disable comments and update ad rendering structure on Puzzle Page layout
andresilva-guardian Sep 17, 2026
1dbee8f
Refactor ad rendering structure in Puzzle Page layout for improved re…
andresilva-guardian Sep 17, 2026
f0a7760
Allow development environment to bypass variant checks in puzzles hub…
andresilva-guardian Sep 17, 2026
e3fa4fb
Add PrintButton component and integrate into PuzzlePageLayout for pri…
andresilva-guardian Sep 17, 2026
5fa42e6
Refactor PrintButton component and integrate into PuzzlePageLayout fo…
andresilva-guardian Sep 17, 2026
a1ed34e
Refactor PrintButton component to use SvgDocument and simplify button…
andresilva-guardian Sep 17, 2026
273ee50
Include AB testing Puzzle Page gate on DCR
andresilva-guardian Sep 18, 2026
86bf1b7
More from Puzzles and Games cards
andresilva-guardian Sep 18, 2026
faec646
Update PuzzlePageLayout.tsx
andresilva-guardian Sep 18, 2026
70e1834
More Puzzles and Games Responsive Design improved
andresilva-guardian Sep 18, 2026
66ff274
Puzzle Page print improvements
andresilva-guardian Sep 18, 2026
495fb98
Improved and implemented Graphics printing for Puzzle Game
andresilva-guardian Sep 18, 2026
7053074
Fix CI: stylelint !important on PrintButton, stale PuzzlePageLayout t…
andresilva-guardian Sep 18, 2026
fb5ebcd
masthead-print-mono printing solution on Puzzles Page
andresilva-guardian Sep 18, 2026
ebd3e91
Puzzle Page Print only Logo
andresilva-guardian Sep 18, 2026
6c3336a
Update PuzzlePageLayout.tsx
andresilva-guardian Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 36 additions & 38 deletions dotcom-rendering/docs/puzzle-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ stays at `/puzzles-and-games`. None of this is DCR's own routing, it's
endpoint/contract at all; it's mentioned here only so example URLs
elsewhere in this doc stay accurate.

**Access control lives entirely on the `frontend` side, not here.** DCR's
own `/PuzzlePage` endpoint is, and remains, ungated (see "Hitting it
locally" below). `frontend` gates reader access to these routes via its
existing `PuzzlesHubExperiment`/`puzzles-new-hub` AB test before it ever
POSTs to DCR. DCR does not re-implement or duplicate that gating.
**Access control is enforced on both sides.** `frontend` gates reader
access to these routes via its existing `PuzzlesHubExperiment`/
`puzzles-new-hub` AB test before it ever POSTs to DCR. DCR's own
`/PuzzlePage` endpoint additionally checks the same `puzzles-new-hub`
participation itself (via `isPuzzlesHubEnabled`, mirroring
`/PuzzlesPage`'s hub gate) and returns `404` when it isn't enabled for the
request, so the endpoint isn't left relying solely on `frontend` never
calling it (see "Hitting it locally" below).

### The V0 puzzle set

Expand Down Expand Up @@ -117,12 +120,13 @@ make dev
This starts webpack-dev-server on `http://localhost:3030`
(`webpack/webpack.config.dev-server.js`).

There is currently **no AB gate** on this route.
`src/server/handler.puzzlePage.web.ts` validates the body
(`validateAsPuzzlePageType`), looks up the `PuzzleConfig` for the request's
`slug` (`404` if unknown), and renders unconditionally otherwise, with no
`serverSideABTests`/participation check of any kind (see "Open questions"
below for the AB-gate/kill-switch situation).
(`validateAsPuzzlePageType`), checks the request's `puzzles-new-hub`
participation via `isPuzzlesHubEnabled` (`404` if not enabled), looks up
the `PuzzleConfig` for the request's `slug` (`404` if unknown), and only
then renders. Fixtures generated below set `serverSideABTests` to
`{ 'puzzles-new-hub': 'variant' }` so they pass this gate; `NODE_ENV=development`
also bypasses it locally.

Generate fixture JSON for all 6 slugs using the `tsx` devDependency (no
extra install needed) and `fixtures/manual/puzzlePage.ts`'s
Expand Down Expand Up @@ -601,17 +605,15 @@ darkMode: boolean, puzzleDate: string | null } }` and the
value is `frontend`'s responsibility at the route level (per its own
task); true calendar/business-logic validity (e.g. "did this puzzle
actually exist on this date") is not validated anywhere in the stack yet.
- **DCR's `/PuzzlePage` endpoint itself still has no route-level access
control** (unchanged from before). `frontend`'s existing
`PuzzlesHubExperiment`/`puzzles-new-hub` AB test gate decides whether a
reader ever reaches one of these puzzle-page URLs in the first place;
DCR's endpoint renders unconditionally for any request with a known
`slug`. **What has changed**: DCR now has a real, cumulative,
code-change-free kill-switch for individual _feature tiers_ within the
rendered page, see "Feature-tier rollout gating (v0/v1/v2)" below. This
addresses the previous "no kill-switch" limitation for feature-level
rollback; it does not add route-level gating to `/PuzzlePage` itself
(that remains `frontend`'s responsibility, unchanged).
- **DCR's `/PuzzlePage` endpoint now has its own route-level access
control**, checking `puzzles-new-hub` participation via
`isPuzzlesHubEnabled` and returning `404` when it isn't enabled, in
addition to (not instead of) `frontend`'s existing
`PuzzlesHubExperiment`/`puzzles-new-hub` gate that decides whether a
reader ever reaches one of these puzzle-page URLs in the first place.
DCR also has a real, cumulative, code-change-free kill-switch for
individual _feature tiers_ within the rendered page, see "Feature-tier
rollout gating (v0/v1/v2)" below.
- **The Puzzles Hub (`src/layouts/PuzzlesLayout.tsx` and friends) is a
separate, unrelated feature** (a directory/listing page) and is not
documented in this file.
Expand Down Expand Up @@ -660,26 +662,22 @@ The corresponding gate-check helpers live in DCR:
above.

**Current state**: all three tiers sit at `audienceSize: 0/100`, hidden
from the public entirely, same as before this structure existed. Today,
only one DCR-rendered feature actually checks a tier gate:
`PuzzlePageLayout.tsx`'s "More from Puzzles & Games" rail, gated behind
`isPuzzlesHubV1Enabled` (since that rail is v1-scoped, not v0). Every
other v0-scoped feature currently in this codebase renders unconditionally
at the DCR level. v0's "gating" today is really just `frontend`'s
route-level `PuzzlesHubExperiment` check deciding whether a request
reaches `/PuzzlePage` at all, not a DCR-side render-time check. When
future v1/v2 work is implemented (calendar, progress indicators, sign-in
message, on-the-ball/film-reveal, etc.), it should be gated behind
from the public entirely. v0 is now enforced at both layers: `frontend`'s
route-level `PuzzlesHubExperiment` check decides whether a request reaches
`/PuzzlePage` at all, and DCR's `handlePuzzlePage` independently checks
`isPuzzlesHubEnabled` before rendering, so the endpoint isn't left relying
solely on `frontend` never calling it. On top of that v0 gate,
`PuzzlePageLayout.tsx`'s "More from Puzzles & Games" rail is further gated
behind `isPuzzlesHubV1Enabled` (since that rail is v1-scoped, not v0).
When future v1/v2 work is implemented (calendar, progress indicators,
sign-in message, on-the-ball/film-reveal, etc.), it should be gated behind
`isPuzzlesHubV1Enabled`/`isPuzzlesHubV2Enabled` respectively, using the
helpers above, the same way the related-content rail already is.

**No `frontend` repo changes are needed for any of this.** `frontend`
doesn't render Puzzle Page UI itself, so feature-tier gating naturally
lives entirely on the DCR side. `frontend`'s existing route-level
`PuzzlesHubExperiment` gate (already reusing `puzzles-new-hub`) is
unaffected by `puzzles-new-hub-v1`/`puzzles-new-hub-v2` and doesn't need
to check them. It only ever needed to decide whether a reader reaches
`/PuzzlePage` at all, which is still governed by v0 alone.
**No `frontend` repo changes are needed for any of this.** `frontend`'s
existing route-level `PuzzlesHubExperiment` gate (already reusing
`puzzles-new-hub`) is unaffected by `puzzles-new-hub-v1`/
`puzzles-new-hub-v2` and doesn't need to check them.

### SEO risks to revisit before shipping calendar/archive features

Expand Down
8 changes: 2 additions & 6 deletions dotcom-rendering/fixtures/manual/puzzlePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,7 @@ export const createPuzzlePage = (
config: {
...Standard.config,
contentType: 'Game',
// DCR no longer gates /PuzzlePage on any AB test participation
// (the former 'game-page-experiment' gate was removed; routes
// will be mapped/exposed via a different project instead), so
// this is left empty rather than implying any particular value
// is required.
serverSideABTests: {},
serverSideABTests: { 'puzzles-new-hub': 'variant' },
},
nav: Standard.nav,
pageFooter: Standard.pageFooter,
Expand All @@ -77,6 +72,7 @@ export const createPuzzlePage = (
puzzleDate: '2026-09-11',
moreFromPuzzlesAndGames: sampleMoreFromPuzzlesAndGames,
},
isAdFreeUser: false,
...overrides,
};
};
Expand Down
8 changes: 7 additions & 1 deletion dotcom-rendering/src/components/ArticleMeta.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type Props = {
isCommentable: boolean;
mainMediaElements?: FEElement[];
crossword?: FEArticle['crossword'];
hidePreferredSourceOnPrint?: boolean;
};

const meta = (format: ArticleFormat) => {
Expand Down Expand Up @@ -306,6 +307,7 @@ export const ArticleMeta = ({
isCommentable,
mainMediaElements,
crossword,
hidePreferredSourceOnPrint,
}: Props) => {
const { renderingTarget } = useConfig();
const showPreferredSource = hasPreferredSourceButton(
Expand Down Expand Up @@ -563,7 +565,11 @@ export const ArticleMeta = ({
</div>
</div>
</div>
{showPreferredSource ? <PreferredSourceButton /> : null}
{showPreferredSource ? (
<PreferredSourceButton
hidePreferredSourceOnPrint={hidePreferredSourceOnPrint}
/>
) : null}
{isImmersive && mainMediaElements?.[0] && (
<Hide until="leftCol">
<div
Expand Down
9 changes: 8 additions & 1 deletion dotcom-rendering/src/components/PreferredSourceButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ import {
import { LinkButton, SvgGoogleBrand } from '@guardian/source/react-components';
import { palette } from '../palette';

export const PreferredSourceButton = () => (
export const PreferredSourceButton = ({
hidePreferredSourceOnPrint,
}: {
hidePreferredSourceOnPrint?: boolean;
}) => (
<LinkButton
data-print-layout={
hidePreferredSourceOnPrint === true ? 'hide' : undefined
}
priority="tertiary"
icon={<SvgGoogleBrand />}
size="small"
Expand Down
45 changes: 45 additions & 0 deletions dotcom-rendering/src/components/PrintButton.island.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { css } from '@emotion/react';
import { Button, SvgDocument } from '@guardian/source/react-components';
import { palette as themePalette } from '../palette';

const buttonStyles = css`
transition: none;
border: 1px solid ${themePalette('--share-button-border')};
color: ${themePalette('--share-button')};
svg {
fill: ${themePalette('--share-button')};
}
:hover {
background-color: ${themePalette('--share-button')};
border-color: ${themePalette('--share-button')};
color: ${themePalette('--share-button-hover')};
svg {
fill: ${themePalette('--share-button-hover')};
}
}
:focus {
/* stylelint-disable-next-line declaration-no-important */
outline: 0 !important;
}
`;

/**
* `window.print()` only runs client-side, so this must be hydrated via
* `Island` (see `PuzzlePageLayout.tsx`) rather than rendered inline - a
* plain server-rendered `onClick` never gets attached in the browser.
*/
export const PrintButton = () => {
return (
<Button
onClick={() => window.print()}
size="small"
type="button"
priority="tertiary"
iconSide="left"
icon={<SvgDocument />}
cssOverrides={css(buttonStyles)}
>
Print version
</Button>
);
};
Loading
Loading