Skip to content

Social card foundation: description rule, card route and the title-only Document card #664

Open

Description

@HMarzban

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:title does 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/core at 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>:

  1. Shape check on documentId. A bad shape gets the 404 with no read.
  2. A rate bucket per address.
  3. Single-flight row read per documentId. Nothing is kept after it settles.
  4. Eligibility. Every refusal gets the identical 404 body and headers, max-age=60.
  5. cardVersion = a hash of the template id and every drawn value. If-None-Match equal to it gives 304.
  6. An 8 MiB LRU per replica, keyed kind:documentId:cardVersion, plus in-flight dedupe.
  7. 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.
  8. 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). With document off, nothing changes for readers.
  • The slug read returns card: { kind, v } when the kind is on and the pad is eligible.
  • DocumentHead then sets og:image and twitter:image to 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_KINDS empty, curl -A 'Slackbot-LinkExpanding 1.0' on a local pad gives the same head as before, apart from the description rule.
  • With document on: the head points at the card URL; the route gives 200 image/png 1200 × 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-Match gives 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-lockfile passes, and the server image installs the Takumi binary for linux amd64.

Activity

  1. HMarzban commented on Nov 1, 2026

    @HMarzban
    CollaboratorAuthor

    Built on main and checked locally, not committed yet. Status and the checks owed before switch-on are in #663 (comment of 2026-11-01).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions