Skip to content

Commit 975c1e6

Browse files
docs(puzzle-page): reflect combined guardian-puzzle-context mechanism
Updates docs/puzzle-page.md to match the previous commit's replacement of the userId-only iframe mechanism with a single, combined PuzzleContext. - Retitled "User identity passed to the puzzle iframe" to "User/context info passed to the puzzle iframe" and rewrote it to describe the new PuzzleContext shape ({ userId: string | null; darkMode: boolean }), the new guardian-puzzle-context query param (always included, JSON-encoded) and postMessage shape, and exactly how darkMode is derived: the existing darkModeAvailable server-side AB flag (already threaded through PuzzlePage.tsx -> rootStyles() for the page chrome) AND the reader's real OS/browser prefers-color-scheme preference (via DCR's existing, generic useMatchMedia hook) - both reused, no new mechanism invented for either half. - Updated the "message shape needs confirming" open question to reference PuzzleContextMessage/guardian-puzzle-context instead of the old PuzzleUserMessage/guardian-puzzle-user, and to note the dark-mode signal specifically also needs confirming with providers. - Updated the dark mode open-question bullet to reflect that a dark-mode signal is now actually sent to the iframe (previously it said no such signal existed), while still flagging that whether AmuseLabs/Wordiply read or honour it at all remains unconfirmed and unverified. No functional/code changes in this commit - documentation only, following the implementation commit. tsc --noEmit clean, full-repo eslint clean, full test suite passing (175 suites / 1268 tests). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b60a73d commit 975c1e6

1 file changed

Lines changed: 78 additions & 45 deletions

File tree

dotcom-rendering/docs/puzzle-page.md

Lines changed: 78 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -149,44 +149,76 @@ iframe-based slug:
149149
| `instance.puzzleDate` | `string?` (e.g. `"2026-09-11"`) | Which day's puzzle the reader wants to see. Accepted and validated as an optional string only — **not yet wired into any rendering or the iframe URL** (see "Open questions"). Prep work for a future V1 calendar-navigation feature; unrelated to the removed crossword-only `date` display-string field. |
150150
| `instance.moreFromPuzzlesAndGames` | `PuzzleItem[]?` (from `src/types/puzzlesPage.ts`) | Rendered as a plain "More from Puzzles & games" list when present and non-empty. |
151151

152-
### User identity passed to the puzzle iframe
153-
154-
`src/components/PuzzleIframe.island.tsx` passes the current signed-in
155-
reader's identity to the puzzle provider two ways:
156-
157-
- As a `userId` query parameter on the iframe `src` (e.g.
158-
`?set=guardian-sudoku-easy&embed=1&idx=1&userId=<id>`), present from the
159-
iframe's very first request.
160-
- Via `window.postMessage({ type: 'guardian-puzzle-user', userId }, '*')`,
161-
sent to the iframe once it has loaded.
162-
163-
`userId` is the reader's `idToken.claims.legacy_identity_id` (resolved via
164-
`src/lib/identity.ts`'s `getAuthStatus()`), the same identifier already used
165-
to build MyAccount links elsewhere in DCR (`TopBarMyAccount.tsx`) — **not**
166-
the OIDC `sub` claim some other, newer API integrations in DCR use instead.
167-
`userId` is omitted (both from the URL and the message) when the reader is
168-
signed out.
169-
170-
The iframe reloads automatically when the reader's sign-in state changes
171-
while already on the page (sign in, sign out, or switching accounts): the
172-
component subscribes to auth state changes via
173-
`src/lib/identity.ts`'s `subscribeToAuthStateChange()` (a thin wrapper
174-
around the `@guardian/identity-auth` client's own `authStateManager.subscribe`),
175-
and since the iframe's `src` is derived directly from the current user ID,
176-
React gives the `<iframe>` a new `src` value whenever that changes, which
177-
the browser treats as a fresh navigation — no manual reload call needed. The
178-
`postMessage` above fires again after every such reload too.
152+
### User/context info passed to the puzzle iframe
153+
154+
`src/components/PuzzleIframe.island.tsx` passes a combined `PuzzleContext`
155+
about the current reader to the puzzle provider two ways:
156+
157+
- As a single JSON-encoded `guardian-puzzle-context` query parameter on the
158+
iframe `src` (e.g.
159+
`?set=guardian-sudoku-easy&embed=1&idx=1&guardian-puzzle-context=%7B%22userId%22%3Anull%2C%22darkMode%22%3Afalse%7D`,
160+
which decodes to `{"userId":null,"darkMode":false}`), present from the
161+
iframe's very first request. Unlike the parameter's previous `userId`-only
162+
form, this is always included — the context shape always carries both
163+
fields, so there's no "nothing to add" case to omit it for.
164+
- Via `window.postMessage({ type: 'guardian-puzzle-context', context }, '*')`
165+
(the `PuzzleContextMessage` shape), sent to the iframe once it has loaded.
166+
167+
```ts
168+
interface PuzzleContext {
169+
userId: string | null;
170+
darkMode: boolean;
171+
}
172+
```
173+
174+
- **`userId`** is the reader's `idToken.claims.legacy_identity_id` (resolved
175+
via `src/lib/identity.ts`'s `getAuthStatus()`), the same identifier
176+
already used to build MyAccount links elsewhere in DCR
177+
(`TopBarMyAccount.tsx`) — **not** the OIDC `sub` claim some other, newer
178+
API integrations in DCR use instead. `null` when the reader is signed out.
179+
- **`darkMode`** is whether dark mode is currently actually active for this
180+
reader — both of the following must be true:
181+
1. `darkModeAvailable`, the existing server-side `webx-dark-mode-web` AB
182+
test flag for this page/request, already read via `useConfig()` in
183+
`PuzzlePage.tsx` and threaded down through `PuzzlePageLayout.tsx` to
184+
`PuzzleIframe` the same way it already reaches `rootStyles()` for the
185+
page chrome's own dark mode support (see `src/lib/rootStyles.ts`) — no
186+
new source of truth was introduced for this.
187+
2. The reader's OS/browser actually preferring dark
188+
(`prefers-color-scheme: dark`), checked reactively via DCR's existing,
189+
generic `src/lib/useMatchMedia.ts` hook (already used elsewhere in DCR,
190+
e.g. `ArticleMeta.web.tsx`) — not a new media-query mechanism.
191+
192+
When `darkModeAvailable` is `false`, `darkMode` is always `false` and the
193+
media query isn't even consulted.
194+
195+
The iframe reloads automatically whenever either half of the context
196+
changes while the reader is already on the page — sign in, sign out,
197+
switching accounts, or the reader's OS switching light/dark theme: the
198+
component subscribes to both auth state changes
199+
(`src/lib/identity.ts`'s `subscribeToAuthStateChange()`, a thin wrapper
200+
around the `@guardian/identity-auth` client's own
201+
`authStateManager.subscribe`) and colour-scheme changes (via
202+
`useMatchMedia`'s own reactivity), and since the iframe's `src` is derived
203+
directly from the current context, React gives the `<iframe>` a new `src`
204+
value whenever either changes, which the browser treats as a fresh
205+
navigation — no manual reload call needed. The `postMessage` above fires
206+
again after every such reload too.
179207

180208
## Open questions / known limitations
181209

182-
- **The `PuzzleUserMessage` shape needs confirming with AmuseLabs/Wordiply.**
183-
`{ type: 'guardian-puzzle-user', userId: string | undefined }` and the
184-
`?userId=<id>` query parameter are DCR's proposal, documented in code
210+
- **The `PuzzleContextMessage` shape needs confirming with
211+
AmuseLabs/Wordiply.**
212+
`{ type: 'guardian-puzzle-context', context: { userId: string | null,
213+
darkMode: boolean } }` and the `?guardian-puzzle-context=<JSON>` query
214+
parameter are DCR's proposal, documented in code
185215
(`src/components/PuzzleIframe.island.tsx`), but neither has been confirmed
186216
against what AmuseLabs or Wordiply actually expect to receive — including
187217
whether `legacy_identity_id` (rather than the OIDC `sub` claim) is the
188-
right identifier format for them. This needs external coordination before
189-
relying on it for anything beyond best-effort personalisation.
218+
right identifier format for them, and whether either provider's iframe
219+
even supports a dark-mode signal in the first place (see the dark-mode
220+
bullet below). This needs external coordination before relying on it for
221+
anything beyond best-effort personalisation.
190222
- **The auth-state-change subscription is a new mechanism in this
191223
codebase.** `subscribeToAuthStateChange()` uses the underlying
192224
`@guardian/identity-auth` client's own public `authStateManager.subscribe`
@@ -211,19 +243,20 @@ the browser treats as a fresh navigation — no manual reload call needed. The
211243
a verified production AmuseLabs archive URL. The correct URL needs to be
212244
sourced from the team before an archive feature can be built on top of
213245
`hasArchive`; do not guess or reuse the POC URL as-is.
214-
- **Dark mode: the page chrome supports it, but the puzzle content is
215-
unverified and possibly unstyled.** DCR has genuine, pre-existing dark
216-
mode support (`src/lib/rootStyles.ts`, gated behind the
217-
`webx-dark-mode-web` server-side AB test flag via `darkModeAvailable`),
218-
and Puzzle Page wires this through identically to every other DCR page
219-
type (`render.puzzlePage.web.tsx``PuzzlePage.tsx``rootStyles()`), so
220-
the masthead/footer/text/background chrome should follow dark mode
221-
correctly when that flag is enabled. However: the actual puzzle content
222-
is a third-party iframe (AmuseLabs/Wordiply) that DCR has no control over
223-
and no visibility into — whether either provider supports dark mode, and
224-
if so how to request it (e.g. a documented URL parameter), is unconfirmed
225-
and not wired up here. This has not been visually verified in either
226-
light or dark mode.
246+
- **Dark mode: the page chrome supports it, and a dark-mode signal is now
247+
sent to the puzzle iframe, but whether the provider actually honours it is
248+
unverified.** DCR has genuine, pre-existing dark mode support
249+
(`src/lib/rootStyles.ts`, gated behind the `webx-dark-mode-web`
250+
server-side AB test flag via `darkModeAvailable`), and Puzzle Page wires
251+
this through identically to every other DCR page type
252+
(`render.puzzlePage.web.tsx``PuzzlePage.tsx``rootStyles()`), so the
253+
masthead/footer/text/background chrome should follow dark mode correctly
254+
when that flag is enabled. `PuzzleIframe` also now sends `darkMode` (see
255+
"User/context info passed to the puzzle iframe" above) via the
256+
`guardian-puzzle-context` query parameter and `postMessage` — but whether
257+
AmuseLabs or Wordiply actually read or honour that signal at all is
258+
unconfirmed (see the `PuzzleContextMessage` open question above). This has
259+
not been visually verified in either light or dark mode.
227260
- **Responsive/mobile layout has not been explicitly verified** for Puzzle
228261
Page or the puzzle iframes themselves (which are entirely provider-
229262
controlled content).

0 commit comments

Comments
 (0)