Repository navigation
Social card foundation: description rule, card route and the title-only Document card #664
Description
Part of #663 (slices 1 and 2).
Goal
Build the foundation of social preview cards: the page head description rule, the card route with its guards, and the title-only Document card. Every card kind ships off. A deploy changes nothing that a reader sees, except the description rule.
This slice builds on the recommended answers to R2 (description rule) and R10 (Takumi in REST) in #663. If either ruling goes the other way, the switch stays off and the change is reverted before switch-on.
Scope
1. Description rule (apps/webapp/src/pages/_app.tsx, DocumentHead)
- Compare trimmed, lowercased text.
- When the stored description is empty, or equals the Pad title, or equals the slug, send the generic
DOCUMENT_OG_DESCRIPTION. og:titledoes not change.
2. Card module (apps/hocuspocus.server/src/modules/social-card/)
- Eligibility: one pure function. A row gets a card only when it exists, is not Private, is not deleted, has an owner, and has a drawable Pad title. One unit test file pins it, because it is dense branching.
- Text rules: one line cleaner (URL to
[link], a line of only links dropped) and one drawable check against the card font's character map. - Frame: the approved frame from [Tracking] Social preview cards for pad links #663 (mark, kind label, sheet), at the approved sizes and existing design-system colours. No new token.
- Document card, title only: the Pad title at the large title step, cut at a word. A pad with stored blocks still draws title only in this slice.
- Renderer:
@takumi-rs/coreat an exact pin, object trees only, the card font and emoji assets bundled with the server.
3. Card route
GET /api/documents/:documentId/card.png?v=<cardVersion>:
- Shape check on
documentId. A bad shape gets the 404 with no read. - A rate bucket per address.
- Single-flight row read per
documentId. Nothing is kept after it settles. - Eligibility. Every refusal gets the identical 404 body and headers,
max-age=60. cardVersion= a hash of the template id and every drawn value.If-None-Matchequal to it gives 304.- An 8 MiB LRU per replica, keyed
kind:documentId:cardVersion, plus in-flight dedupe. - One render slot per replica with a queue of 4, over the fit and the render. A full queue answers 302 to the static image and counts a shed.
- 200
image/png, 1200 × 630,Cache-Control: public, max-age=3600,ETag,X-Robots-Tag: noindex.
A render error answers 302 to the static image, never 5xx. A counter with outcome = ok, fallback, shed and error is seeded at 0 on start.
The route returns raw PNG bytes, outside the JSON envelope.
4. Switch and page head
SOCIAL_CARD_KINDS(comma list, default empty). Withdocumentoff, nothing changes for readers.- The slug read returns
card: { kind, v }when the kind is on and the pad is eligible. DocumentHeadthen setsog:imageandtwitter:imageto the card URL, with width, height and alt text. With the kind off, the head is byte-identical to today, apart from the description rule.
Not in this slice
First blocks, outline, Heading, Chat, Message, Version and Filter cards. The static image swap (R5). Production reads and the amd64 timing check, which are owed before switch-on (#663 preconditions).
Acceptance checks
- With
SOCIAL_CARD_KINDSempty,curl -A 'Slackbot-LinkExpanding 1.0'on a local pad gives the same head as before, apart from the description rule. - With
documenton: the head points at the card URL; the route gives 200image/png1200 × 630 under 200 KB with the four headers. - Private, deleted, ownerless, draft and random ids give byte-identical 404s.
- A repeat request with
If-None-Matchgives 304 and no render. - The rendered title-only card matches the approved design.
bun run typecheck, lint and the hocuspocus test suite pass. The eligibility test is observed green.- A cold
bun install --frozen-lockfilepasses, and the server image installs the Takumi binary for linux amd64.
Activity
HMarzban commentedon Nov 1, 2026
Built on
mainand checked locally, not committed yet. Status and the checks owed before switch-on are in #663 (comment of 2026-11-01).