You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Depends on: #135 (land first -- this reuses the summary_large_image + full
OG/Twitter tag pattern it establishes, so race pages aren't built against the
old inconsistent pattern).
Outcome
Sharing a specific race (via the existing "Share link" button on each race
card) produces a link that unfurls with that race's own title, description,
and a build-time-generated image showing the race name, consensus pick/grade,
and source count -- not the site-wide brand card every link shares today.
Why this needs more than an image
Investigated the current "Share link" flow: it copies window.location.href
with link.hash set to race-<id> (guide.html.j2:1284), producing .../e/<election_id>/#race-<id>. URL fragments are never sent as part of the
HTTP request, so link-unfurling crawlers (Slack, iMessage, Twitter/X,
Facebook) fetch the same guide page and get the same site-wide og:image/ og:title no matter which race was "shared." A distinct per-race preview
requires a distinct, path-based, crawlable URL per race -- this is a routing
change, not just an image-generation change.
Scope
New static route per race: /e/<election_id>/races/<race_id>/index.html,
generated at hosting stage time (pages.pystage_pages_site / _stage_verified_bundles) alongside the existing per-election index.html
and sources/index.html. One page per race in the current bundle's
inventory.
Page content: a server-rendered summary of that one race (name,
consensus grade/pick, source count, notable alternatives) sufficient to
stand alone for crawlers and no-JS visitors, plus a prominent link back to
the full guide (/e/<id>/#race-<id>) that also opens the existing
race-detail dialog for JS visitors. Reuses the data already computed for
the dialog (e.g. race_detail_accessible_summary, guide.html.j2:291) --
no new data source.
Per-race OG image: generated at build time (not per-request -- no
server compute added), using Pillow (pyproject.toml:15, already a
dependency) to render race name + consensus pick + grade onto the existing
brand template. One PNG per race, written into that race's output
directory.
Meta tags: each race page gets its own og:title (" --
Seattle Elections Guide"), og:description (a consensus summary sentence), og:url (the canonical race URL), and og:image pointing at its own PNG,
following the summary_large_image + full tag-set pattern from Unfurling quick wins: summary_large_image + complete OG/Twitter tags per page #135.
Update "Share link": the copy-link button (guide.html.j2:1279-1308)
points at the new canonical race URL instead of the fragment-only link.
Preserved behavior
The single-page guide with its race-detail dialog is unchanged for anyone
landing on /e/<id>/ directly -- this adds new pages, it doesn't replace
the existing dialog UX.
No scoring/consensus logic changes; race pages render already-computed
data.
Everything remains static: no Cloudflare Pages Function, no per-request
rendering.
Non-goals
Personalized ("My sources") unfurls -- explicitly out of scope, would
require server compute.
Depends on: #135 (land first -- this reuses the summary_large_image + full
OG/Twitter tag pattern it establishes, so race pages aren't built against the
old inconsistent pattern).
Outcome
Sharing a specific race (via the existing "Share link" button on each race
card) produces a link that unfurls with that race's own title, description,
and a build-time-generated image showing the race name, consensus pick/grade,
and source count -- not the site-wide brand card every link shares today.
Why this needs more than an image
Investigated the current "Share link" flow: it copies
window.location.hrefwith
link.hashset torace-<id>(guide.html.j2:1284), producing.../e/<election_id>/#race-<id>. URL fragments are never sent as part of theHTTP request, so link-unfurling crawlers (Slack, iMessage, Twitter/X,
Facebook) fetch the same guide page and get the same site-wide
og:image/og:titleno matter which race was "shared." A distinct per-race previewrequires a distinct, path-based, crawlable URL per race -- this is a routing
change, not just an image-generation change.
Scope
/e/<election_id>/races/<race_id>/index.html,generated at
hosting stagetime (pages.pystage_pages_site/_stage_verified_bundles) alongside the existing per-electionindex.htmland
sources/index.html. One page per race in the current bundle'sinventory.
consensus grade/pick, source count, notable alternatives) sufficient to
stand alone for crawlers and no-JS visitors, plus a prominent link back to
the full guide (
/e/<id>/#race-<id>) that also opens the existingrace-detail dialog for JS visitors. Reuses the data already computed for
the dialog (e.g.
race_detail_accessible_summary,guide.html.j2:291) --no new data source.
server compute added), using Pillow (
pyproject.toml:15, already adependency) to render race name + consensus pick + grade onto the existing
brand template. One PNG per race, written into that race's output
directory.
og:title(" --Seattle Elections Guide"),
og:description(a consensus summary sentence),og:url(the canonical race URL), andog:imagepointing at its own PNG,following the
summary_large_image+ full tag-set pattern from Unfurling quick wins: summary_large_image + complete OG/Twitter tags per page #135.guide.html.j2:1279-1308)points at the new canonical race URL instead of the fragment-only link.
Preserved behavior
landing on
/e/<id>/directly -- this adds new pages, it doesn't replacethe existing dialog UX.
data.
rendering.
Non-goals
require server compute.
round 4 (Epic: UI polish round 4 — one shell, one answer per quantity, less ink #128) work.