~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2

grain-serve draws the OpenGraph cards links unfurl into

Merged
Merged chadtmiller.com asked to merge into feature/rust from grain.social/grain:og/cards

What changes

grain-serve draws the OpenGraph cards at the paths the web's pages already name in og:image and the embed route builds: /og/profile/<actor>, /og/profile/<actor>/gallery/<rkey> and /og/profile/<actor>/story/<rkey>, the actor a handle or a DID. Each answer is a 1200x630 PNG with Cache-Control: public, max-age=300 and Access-Control-Allow-Origin: *, and a card is kept in process for five minutes, the 200 newest (App::cards).

The cards look as they do today. Each is an SVG written in crates/grain-serve/src/og/ and rasterized in process with resvg, as knotless's cards are, with the fonts compiled in from crates/grain-serve/fonts/ (Inter Regular, the face the cards have always set text in, Syne ExtraBold for grain's name, Noto Sans JP and Noto Emoji for what Inter lacks; OFL, in fonts/OFL.txt). The collage layout is the old one step for step, including the order Node's sort asks its comparator for random numbers in, so a gallery's photos land where they land today: og::collage's test pins the old layout's output for the same photos, and the attached gallery card, drawn from production's own images, matches production's pixel for pixel in placement.

What a label touches stays off a card. A card cannot blur or warn:

  • an account taken down, or gone from its host, a handle the index does not know, or a gallery or story with a label that hides (HIDDEN_LABELS, grain's or a self-label): a "not found" card;
  • a gallery or story with any label standing: its title and author, no photos;
  • an account with any label (on its DID or its profile record): no photos and no avatar, on any of its cards;
  • a photo with a label of its own is left out of a collage.

read::moderation::unlabeled is the new filter: no grain label standing, whatever it says, expiry and negation as Reader::labels reads them.

Why

The step of the plan that moves the cards off the old server. resvg over a headless browser or a satori port: the cards are a few boxes, images and lines of text, which an SVG says directly, and knotless already draws its cards this way.

How it was checked

  • tests/og.rs, against a fake imgproxy CDN serving solid-color PNGs: each card's photos and avatar land where expected (pixels read back), photos the CDN cannot serve are left out, every moderation case above, the not-found cards, the in-process cache, no index (404) and an index that fails to read (500).
  • Unit tests: the collage against the old layout's output for the same input (6, 2, 10 and 1 photos), the sort's comparator calls against Node's, toFixed(3), font fallback and truncation, the cache's expiry and cap.
  • Drawn locally from production's images and compared by eye with production's cards for the same gallery and story (attached: the Rust gallery card, the Rust story card, and a long CJK title with one photo).

Shortcomings

  • Differences from the old server. It never sends a card for a taken-down account, or for a story labeled to hide; the story card's shade along the foot is drawn (the old renderer dropped the gradient); a profile card for a handle the index does not know says "Profile not found" rather than drawing the handle as a DID; a long story author name is cut with an ellipsis rather than wrapped; photo labels are honored. The old server also wrote og: meta tags into the SPA fallback's HTML; the SvelteKit pages set their own, so that is not ported.
  • Text is measured without kerning, so an ellipsis may land a few pixels from where the old renderer put it.
  • Nothing routes /og/* to grain-serve in production yet; that is the cutover's Caddy change.
  • The fonts add about 6 MB to the binary (Noto Sans JP is most of it); the files were already in the repository under server/og/, so git stores no new blobs for three of the four.

Review

og/mod.rs for what reads the index and the moderation rules, og/collage.rs is a port and is pinned by its test.

🤖 Generated with Claude Code

rust-gallery.png

rust-story.png

rust-solo.png

Approved

This adds the three /og/... card routes to grain-serve. It draws each card as an SVG, rasterizes it with resvg, and keeps it in process for five minutes with a 200-card cap. Cards go through the moderation filters the description lists.

I read og/mod.rs in full. I did not read collage.rs, draw.rs, unlabeled in read/moderation.rs, or the tests, so I can't confirm the collage port or the test coverage. I found nothing to block on.

  • Moderation: it matches the description. A card is "not found" for an unknown or taken-down account. The same goes for a gallery or story that hides under HIDDEN_LABELS or a self-label. Any label on the account, gallery or story removes photos and avatar. Photo labels are filtered in the SQL. Self-labels exist only on galleries and stories, so checking self_label only on those is correct.
  • Cache and load: the cache key is the request path, the cache is capped, and drawing is limited to one card per core by a semaphore. Unknown paths do get drawn and cached, but the cap and the semaphore bound the cost.
  • Checks: all four (check, rust, test, harness) were still queued when I looked, so nothing has failed or passed yet. The harness check enforces the coverage gate and the answers check that AGENTS.md describes, so read its result before merging.
Approved

My earlier review asked for nothing. Since then the pull has gained a merge of feature/rust, a clippy chunking fix (as_chunks in the tests), a Cargo.toml format fix, and a simplification of the collage's floor to one condition. I did not re-read those changes line by line, so I'm relying on the collage test that pins the old layout's output to catch any change in placement.

The checks on this commit are still running or queued (harness and check running, test and rust queued), so nothing has failed or passed yet. Read test and harness before merging, since they run the collage and og tests and the coverage gate.

Approved

Carried forward: the change this pull makes at 291b3724fa1aaffd4162f9c95cab29f0c151054d is the one reviewed at 4c21d180d7d79116786b0937ac53d1b4675a32be, so that review stands. What is new comes from its base.

Approved

Carried forward: the change this pull makes at 5868302700c1f85c8bf2d62e44f37cb5f3a2615d is the one reviewed at 291b3724fa1aaffd4162f9c95cab29f0c151054d, so that review stands. What is new comes from its base.

Merged as 98e27d3
Part of a stack of 2
  1. grain-serve draws the OpenGraph cards links unfurl into Landed as 98e27d3
  2. main