Skip to content

Decks: first-class deck abstraction + URL structure (#55) - #64

Merged
acharlop merged 1 commit into
mainfrom
feat/55-decks-abstraction
Jun 16, 2026
Merged

acharlop merged 1 commit into
mainfrom
feat/55-decks-abstraction

Conversation

@acharlop

Copy link
Copy Markdown
Contributor

Closes #55

What

Introduces a first-class Deck abstraction = metadata + presentation hints + a curated, ordered set of question references. /play, the new /play/[deck] route, and the hajnalig event page all now resolve their questions through a deck via a registry, instead of importing questions.json directly. No behavior change for existing pages.

This is foundational — it unblocks #57 (AI Check-In: wire the deck into /play) and the custom-decks half of #45.

Deck model (src/data/decks/)

  • types.ts — Deck (authored), ResolvedDeck (with a concrete QuestionSet), DeckPresentation, DeckSource.
  • library.ts — the full WhoCards library; source: {kind: 'library'} references the global pool by id (no text duplicated). Keeps the legacy hero bg + white text.
  • hajnalig.ts — source: {kind: 'inline', questions}. The hajnalig set (163 q's, hu/en) is not a subset of the global pool (66 q's) and its text differs even for shared ids, so it carries its own questions inline. The model supports both reference-by-id and inline so future decks can do either.
  • index.ts — registry + resolver:
    • getDeck(slug): ResolvedDeck | undefined — metadata + presentation + resolved QuestionSet.
    • getAllDecks(): ResolvedDeck[], isDeckSlug(slug), DEFAULT_DECK_SLUG.

A deck carries the presentation/config hints <Play>/Layout already use: languages, questionClassName, oldBg, languageStorageKey, userIdStorageKey, optional tracking. Pages just spread them through, so <Play> itself was unchanged.

URL decision

Decision: deck → path segment, lang + question → query params.

  • Deck = path segment — /play/[deck]. A deck is a marketable / linkable / ad-targetable entity, so it deserves a stable, SEO-friendly URL and its own OG share card. Kept inside the existing /play family rather than a new /decks/* tree so it shares the SSR play machinery.
  • Default/library deck stays at /play for back-compat. /play/library 301-redirects to /play (in-page Astro.redirect) so there's no duplicate URL for the same content.
  • lang + question stay query params (?lang=, ?q=) — ephemeral view-state, and this preserves the SSR-for-OG mechanism already in play.astro/Head.astro. Crawlers (no JS) hit the SSR page and get per-question og:image (/og/{lang}/{id}.png); with no ?q= they get the deck's own title/description card.
  • /play/[deck] is SSR (prerender = false), resolving the deck and emitting per-question OG via the existing Head id/language props. Unknown slugs fall back to /play (302).

Redirect implications (netlify.toml)

No netlify.toml change was needed and nothing was broken:

  • The existing /:language/question/:id → /play?lang=:language&q=:id (301) redirect is untouched.
  • /play?lang=&q= continues to work exactly as before.
  • The only new redirect (/play/library → /play) is handled in-page via Astro.redirect, keeping all path/back-compat logic colocated with the route rather than split into netlify.toml. Future decks need no redirect entries — they're served directly by the SSR /play/[deck] function.

Public interface (for #57)

// ~data/decks
export const DEFAULT_DECK_SLUG: string            // 'library'
export const getDeck: (slug: string) => ResolvedDeck | undefined
export const getAllDecks: () => ResolvedDeck[]
export const isDeckSlug: (slug: string) => slug is DeckSlug

type Deck = DeckPresentation & {
  slug: string; title: string; description: string
  source:
    | {kind: 'library'; ids?: string[]}            // ids omitted => all questions
    | {kind: 'inline'; questions: QuestionSet}
}
type ResolvedDeck = Deck & {questions: QuestionSet; questionIds: string[]}
type DeckPresentation = {
  languages: string[]; questionClassName?: string; oldBg?: boolean
  languageStorageKey?: string; userIdStorageKey?: string; tracking?: TrackingConfig
}

To add the AI Check-In deck (#57): drop an ai-at-work.ts deck into src/data/decks/, register it in index.ts, and it's instantly playable at /play/ai-at-work with OG resolving.

Verification

  • pnpm build — green (SSR /play and /play/[deck] functions emitted).
  • pnpm test (Playwright e2e) — 18 passed, 3 skipped (pre-existing skips: contact form needs a backend; SSR-OG test needs a functions-capable server). The hajnalig event-play suite exercises the deck refactor and stays green.
  • tsc — the deck/play files are type-clean. (Remaining repo-wide tsc errors are pre-existing and in untouched files: .astro module typings without @astrojs/check, and third-party satori/bidi-js/wawoff2 typings.)

Introduce a Deck concept (metadata + presentation hints + a resolved
QuestionSet) and a registry/resolver so /play and the hajnalig event page
draw questions through a deck instead of importing questions.json directly.

- src/data/decks/: Deck/ResolvedDeck types, library + hajnalig decks, and
  getDeck(slug)/getAllDecks resolver. Library deck references the global
  pool by id (no text duplication); hajnalig carries its own inline set
  (its questions are not a subset of the global pool).
- play.astro consumes the library deck; new SSR /play/[deck] serves any
  deck at a stable path with per-question OG preserved. Library redirects
  to /play for back-compat; unknown slugs fall back to /play.
- hajnalig wrapper resolves its deck via getDeck('hajnalig').

No behavior change for existing pages. Build + e2e green.
@netlify

netlify Bot commented Jun 16, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for whocards-calmly ready!

Name Link
🔨 Latest commit 57b9e98
🔍 Latest deploy log https://app.netlify.com/projects/whocards-calmly/deploys/6a317b16ebdb1500089e0610
😎 Deploy Preview https://deploy-preview-64--whocards-calmly.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 96
Accessibility: 87
Best Practices: 100
SEO: 100
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@acharlop
acharlop merged commit 6cbdc68 into main Jun 16, 2026
6 checks passed
@acharlop
acharlop deleted the feat/55-decks-abstraction branch June 16, 2026 16:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decks: introduce a first-class "deck" abstraction (+ revisit query-param vs path URL design)

1 participant