Skip to content

feat(invite): personal invitation page for each team member - #325

Open
ryzizub wants to merge 9 commits into
2026from
agent/astro-engineer/devf-45
Open

feat(invite): personal invitation page for each team member#325
ryzizub wants to merge 9 commits into
2026from
agent/astro-engineer/devf-45

Conversation

@ryzizub

@ryzizub ryzizub commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

An unlisted personal invitation page for each of the eleven people in src/content/team.json/invite/<slug>, built from the approved visual direction B ("The Plate"). DEVF-45.

Why

The team's personal network is the strongest channel the conference has and it currently has no home: a member shares a generic devfest.cz link and the recipient gets marketing, not an invitation. Each page speaks in one member's voice and carries exactly one action.

Behavior

  • Eleven routes, slug = the team.json entry key, so the roster and the URLs can't drift apart.
  • Unlisted, three ways at oncenoindex (BaseLayout also drops the canonical + JSON-LD), excluded from the sitemap, and linked from nowhere on the site. Not access control: the URLs are guessable and nothing here is sensitive.
  • One primary action. InviteCta resolves the ti.to href client-side from the cached /api/tickets endpoint (the live wave isn't known at build time) and reports begin_checkout with invite_member / invite_member_name. With no discount code in v1 that click is the only per-member attribution the channel has — checkout runs on ti.to and its redirect carries no source. Before the endpoint answers, and if it never does, the href is /#tickets.
  • Copy is the agreed generic v1 in src/lib/invite.ts, with one closing line varying by role so the eleven pages aren't one page with a name swapped. When members write their own paragraphs, only body changes.
  • Mobile scrim held. There is no side-by-side frame under 860px, so the wash over the read zone is what keeps the type off the face — a legibility condition, not a finish. The base plate is the LCP element (fetchpriority="high"); the colour layer is the enhancement (low).

Files

  • src/pages/invite/[member].astro — the route
  • src/lib/invite.ts — generic v1 copy + the role lines
  • src/components/InviteCta.tsx — the resolved, tracked CTA
  • src/components/Closer.astro — optional actions slot so the closing repeat can be that island
  • astro.config.mjs — sitemap exclusion
  • scripts/a11y.mjs — one invite page added to the sweep

Verification

  • npm run build — 26 pages, all eleven invite pages emitted, none in sitemap-0.xml, noindex present, no canonical.
  • npm run a11y — 16 pages pass WCAG 2.2 AA (axe-core + the custom control-contrast pass), including /invite/eliska-cejpova/.
  • Checked in a browser at 1440 and 390: both CTAs resolve to the ti.to target after hydration, mobile scrim holds, the colour bleed runs once.

🤖 Generated with Claude Code

@ryzizub
ryzizub force-pushed the agent/astro-engineer/devf-45 branch 3 times, most recently from 63fa6e9 to 475a929 Compare September 4, 2026 20:39
ryzizub and others added 8 commits September 5, 2026 23:35
An unlisted referral channel: one page per person in `src/content/team.json`,
written in their voice, which they share with their own network themselves.
Built from the approved direction B ("The Plate") — the portrait owns the right
half of the frame and the black-and-white → colour bleed is the page's only
effect.

Unlisted means three things at once, and dropping any one makes it cosmetic:
`noindex`, out of the sitemap, and linked from nowhere on the site.

`InviteCta` is the single primary action, and it is an island because it has to
do two things a plain link cannot: resolve the ti.to href from `/api/tickets`
(the live wave is not known at build time) and report `begin_checkout` carrying
the member's identity. With no discount code in v1, that click is the only
per-member attribution the channel has — checkout happens on ti.to and its
redirect carries no source. Until the endpoint answers, and if it never does,
the href falls back to the on-site ticket section.

Copy is the agreed generic v1 in `src/lib/invite.ts`, with one line varying by
role so the eleven pages are not the same page with a name swapped; when the
members write their own paragraphs, only `body` changes.

On a phone there is no side-by-side frame, so the scrim over the read zone is
what keeps the type off the face — a legibility condition, not a finish.

`Closer.astro` gains an optional `actions` slot so the closing repeat of the CTA
can be that same tracked island; the `actions` prop stays the path everywhere
else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The header's red `Tickets` action is dropped on `/invite/*`: an invitation
has exactly one primary action (`Take the ticket`, straight to ti.to), and
the bar action was a second one pointing off the page. The mark and the menu
toggle stay, so the bar is still a bar and every destination is one click
away behind it.

Also from review:

- `Tickets.tsx` no longer returns null when the cache has no event slugs
  (the state before `refreshTicketsScheduled` has ever run). It deleted
  `#tickets` from the home page after hydration, so every `/#tickets` link
  — the header action, an invitation's fallback CTA — dropped the visitor
  at the top of the page with no ticket section anywhere. Only the ti.to
  link is dropped now.
- The invite page's bleed script runs on `astro:page-load`, the repo
  convention: a soft navigation back to the page left the plate black and
  white, and that bleed is the page's only effect.
- The plate declares a real `sizes` (62% of the frame on desktop, full-bleed
  under 860px) and asks for widths that exist: the masters are 700x875, so
  1080/1440 only ever emitted one 700w candidate. The smaller widths are the
  saving actually available.
- `/api/tickets` is memoised for the page, so the invitation's two islands
  make one request (the endpoint sends `max-age=0`; the HTTP cache cannot
  coalesce them). The caller's `signal` aborts its own wait, not the shared
  request.
- `begin_checkout` fires only on the ti.to click. On the `/#tickets`
  fallback the visitor still presses Buy, which sends the event itself —
  reporting both double-counted the wave and filed an item-less event
  against the member.
- README: register `invite_member` / `invite_member_name` as GA4 custom
  dimensions. Unregistered custom parameters are collected and invisible,
  and registration is not retroactive.
- Title nit: no full stop before the em dash in the document title.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six full-page screenshots and a dev log landed on this branch as
`chore(agent): uncommitted changes from task` — ~15 MB of agent working
files in a public repo, not part of the change under review. Removed
from the tree; the blobs stay in this branch's history until the PR is
squashed (or the branch is rewritten).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BaseLayout gets an ogImage/ogImageAlt prop (absolute URL, default unchanged);
each /invite/<id> page now points its own og:image at a prerendered
og/invite/<id>.png built from the same team collection, so a new roster
member gets a card automatically instead of silently falling back to the
generic conference banner. Composed with satori + resvg using the two brand
faces (Bebas Neue, JetBrains Mono) embedded as static TTFs, no new fonts.
npm audit fix for GHSA-2v37-7h3g-55p8, pulled in by the new satori/resvg deps.
Ports the picked "poster, one field" study (proto-og/render.mjs,
188016e2 on agent/mika/devf-45) into the shipping
src/pages/og/invite/[member].png.ts: plate feathered by sharp
(satori has no mask-image), the page's own headline sentence with
red on "list." only, and the per-role line set in Special Elite —
never the member's name, since that face lacks the Czech diacritics
Bebas already carries (#305).

Vendors SpecialElite-Regular.ttf alongside the two faces already in
src/assets/fonts, for satori to embed at render time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… preview origin

- Rebased agent/astro-engineer/devf-45 onto 2026 (was missing checkoutUrl,
  dropped as a dead export upstream before InviteCta.tsx started importing it).
- Restore checkoutUrl in src/lib/tito.ts so the merge build stops failing
  with MISSING_EXPORT.
- OG invite card headline: "on the" + red "list." were two stacked lines
  instead of one row — group them as inline flex-row children with an
  explicit gap (satori collapses a trailing-space text node in a flex row).
- Add PUBLIC_SITE_URL (src/env.d.ts, .env.example), defaulting to
  https://devfest.cz, so the invite page's og:image can be built against a
  preview channel's own origin for verification without touching
  astro.config.mjs's site/canonical/sitemap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ryzizub
ryzizub force-pushed the agent/astro-engineer/devf-45 branch from a84e849 to 5912acf Compare September 5, 2026 21:42
DEVFEST.CZ was rendered as text, drifting from the site mark. Now draws
src/assets/logo.png (same file as Menu/Footer) as a data URI, pre-shrunk
2x by sharp so resvg doesn't fray the downscale.
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.

1 participant