Skip to content

Commit 6f4ebbb

Browse files
docs: document the killer-sudoku URL fix and the confirmed uid param
Documents why the shared, slug-derived AmuseLabs URL template was removed (the killer-sudoku incident) and that each PuzzleConfig entry now carries its own complete, explicit iframe URL. Documents the new uid query parameter (source, when included/omitted) alongside the existing guardian-puzzle-context. Updates the open question about message-shape confirmation to note uid specifically is now confirmed via the native apps' integration, while guardian-puzzle-context (dark mode, puzzle date, and the JSON-blob mechanism itself) remains unconfirmed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent d5ff801 commit 6f4ebbb

1 file changed

Lines changed: 73 additions & 23 deletions

File tree

dotcom-rendering/docs/puzzle-page.md

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,22 @@ contains exactly 6 slugs, all rendered via the generic sandboxed
5353

5454
Codeword, futoshiki, suguru, and the trivia/quizzes puzzles (on-the-ball,
5555
film-reveal) were removed from the registry for V0 and may return later.
56-
All AmuseLabs-hosted entries share one URL template
57-
(`https://tg.amuselabs.com/guardian/date-picker?set=guardian-{slug}&embed=1&idx=1`),
58-
differing only by the `{slug}` substitution.
56+
**Each entry's `iframe.url` is its own complete, independently-written URL,
57+
there is deliberately no shared URL template or `{slug}`-style
58+
substitution mechanism.** There used to be one: every AmuseLabs-hosted
59+
entry's URL was built from a shared template
60+
(`https://tg.amuselabs.com/guardian/date-picker?set=guardian-{slug}&embed=1&idx=1`)
61+
by substituting DCR's own `slug` in for AmuseLabs' `set=guardian-*` query
62+
param. That was a real, live bug: nothing guarantees a provider's own
63+
naming convention matches our internal slug, and it already silently
64+
didn't for `sudoku-killer` (its real, confirmed AmuseLabs `set` is
65+
`killer-sudoku-medium`, not `sudoku-killer`, a different word order plus
66+
an unexplained "-medium" suffix that is genuinely part of the real,
67+
working identifier). The fix (confirmed against the native Android/iOS
68+
apps' own real, working AmuseLabs integration) was to remove the shared
69+
template entirely, not just patch that one instance: every entry now
70+
specifies its own complete, independent, hardcoded URL, so a future
71+
change to one entry can never silently or accidentally affect another.
5972

6073
### Hitting it locally
6174

@@ -117,16 +130,23 @@ Both steps are config-only. The layout does not need any changes for a new
117130
iframe-based slug:
118131

119132
1. Add a new key to `src/model/puzzles/puzzleConfigs.ts`'s `puzzleConfigs`
120-
record (`slug`, `puzzleGroup`, `iframe: { provider, urlTemplate }`,
133+
record (`slug`, `puzzleGroup`, `iframe: { provider, url }`,
121134
`shareEnabled`, `printEnabled`, `hasArchive`, `title`, `description`,
122135
optional `image`). If it's another AmuseLabs-hosted puzzle, reuse the
123-
`amuseLabsPuzzle(slug, puzzleGroup, title, description)` helper (note:
124-
this helper doesn't take `image`, set it afterwards on the returned
125-
object if/when a real image is available for that puzzle).
126-
`validatePuzzleConfigs` runs once at module load and throws immediately
127-
if the entry is malformed (mismatched `slug`, unknown `puzzleGroup`,
128-
empty `iframe.provider`/`iframe.urlTemplate`, empty `title`/
129-
`description`, or a present-but-empty `image`).
136+
`amuseLabsPuzzle(slug, puzzleGroup, title, description, url)` helper
137+
(note: this helper doesn't take `image`, set it afterwards on the
138+
returned object if/when a real image is available for that puzzle).
139+
**`iframe.url` must be that puzzle's own complete, explicit iframe URL,
140+
confirmed against the actual provider (or a source that has itself
141+
confirmed it against the provider, e.g. the native apps' own working
142+
integration), not derived from `slug` or copied from another entry.**
143+
Never assume a provider's own naming convention matches our internal
144+
slug: the killer-sudoku incident above is a direct, confirmed example
145+
of that assumption silently being wrong. `validatePuzzleConfigs` runs
146+
once at module load and throws immediately if the entry is malformed
147+
(mismatched `slug`, unknown `puzzleGroup`, missing `iframe.provider`,
148+
a missing/empty/non-absolute `iframe.url`, empty `title`/`description`,
149+
or a present-but-empty `image`).
130150
**Write real, curated copy for `title`/`description`**, sourced from
131151
the product team's SEO spreadsheet for that puzzle (see "SEO" below for
132152
the exact `{date}` templating mechanism). It becomes the page's
@@ -317,7 +337,25 @@ interface PuzzleContext {
317337
`frontend`'s own documentation for how it resolves and redirects on the
318338
date-in-URL structure.
319339

320-
The iframe reloads automatically whenever either half of the context
340+
**A separate, plain `uid=<userId>` query parameter is also appended
341+
alongside `guardian-puzzle-context`**, only when the reader is signed in
342+
(omitted entirely, not sent as `uid=null` or empty, when signed out). This
343+
is a genuinely different, independently-confirmed mechanism from
344+
`guardian-puzzle-context` above: the native (Android/iOS) apps' own real,
345+
working AmuseLabs integration appends `&uid=<value>` as a plain query
346+
parameter when the user is authenticated ("If the user is authenticated,
347+
we add &uid=<puzzleId>"), and DCR adopted the same query parameter
348+
name/pattern once confirmed. It is additive, not a replacement,
349+
`guardian-puzzle-context` still carries dark mode and puzzle date, for
350+
which there is no separately-confirmed mechanism yet. `uid`'s value is
351+
sourced identically to `guardian-puzzle-context.userId`
352+
(`idToken.claims.legacy_identity_id`); the native apps call their
353+
equivalent value a "puzzleId", but there is no independent confirmation
354+
that identifier format matches ours, only that this exact query parameter
355+
name/pattern is what they use for their own equivalent value (see "Open
356+
questions" below).
357+
358+
The iframe reloads automatically whenever any part of the context or `uid`
321359
changes while the reader is already on the page: sign in, sign out,
322360
switching accounts, or the reader's OS switching light/dark theme. The
323361
component subscribes to both auth state changes
@@ -332,18 +370,30 @@ fires again after every such reload too.
332370

333371
## Open questions / known limitations
334372

335-
- **The `PuzzleContextMessage` shape needs confirming with
336-
AmuseLabs/Wordiply.**
373+
- **The `PuzzleContextMessage` shape (`guardian-puzzle-context`) is still
374+
unconfirmed with AmuseLabs/Wordiply; `uid` specifically is now
375+
confirmed.**
337376
`{ type: 'guardian-puzzle-context', context: { userId: string | null,
338-
darkMode: boolean } }` and the `?guardian-puzzle-context=<JSON>` query
339-
parameter are DCR's proposal, documented in code
340-
(`src/components/PuzzleIframe.island.tsx`), but neither has been confirmed
341-
against what AmuseLabs or Wordiply actually expect to receive, including
342-
whether `legacy_identity_id` (rather than the OIDC `sub` claim) is the
343-
right identifier format for them, and whether either provider's iframe
344-
even supports a dark-mode signal in the first place (see the dark-mode
345-
bullet below). This needs external coordination before relying on it for
346-
anything beyond best-effort personalisation.
377+
darkMode: boolean, puzzleDate: string | null } }` and the
378+
`?guardian-puzzle-context=<JSON>` query parameter remain DCR's own
379+
proposal, documented in code (`src/components/PuzzleIframe.island.tsx`),
380+
neither has been confirmed against what AmuseLabs or Wordiply actually
381+
expect to receive, including whether `legacy_identity_id` (rather than
382+
the OIDC `sub` claim) is the right identifier format for the `userId`
383+
field within it, and whether either provider's iframe even supports a
384+
dark-mode signal in the first place (see the dark-mode bullet below).
385+
The separate, plain `uid=<userId>` query parameter (see "User/context
386+
info passed to the puzzle iframe" above), by contrast, **is** confirmed:
387+
sourced from the native (Android/iOS) apps' own real, working AmuseLabs
388+
integration, which uses this exact query parameter name/pattern for
389+
their own equivalent identity value. That confirmation does not extend
390+
to the identifier _format_: the native apps call their value a
391+
"puzzleId", and there is no independent confirmation that
392+
`legacy_identity_id` is the same format as whatever they send, only that
393+
the `uid` query parameter itself, and the pattern of "include only when
394+
signed in, omit entirely when signed out", is confirmed correct. This
395+
needs external coordination before relying on `guardian-puzzle-context`
396+
for anything beyond best-effort personalisation.
347397
- **The auth-state-change subscription is a new mechanism in this
348398
codebase.** `subscribeToAuthStateChange()` uses the underlying
349399
`@guardian/identity-auth` client's own public `authStateManager.subscribe`

0 commit comments

Comments
 (0)