Add share cards
You'll give each page of a small recipe site its own share card: the picture a chat app or a social site shows when someone pastes the link. Each card is drawn from the page's own data by the server that serves the page.
Before you start
Install from crates.io. You need no Node and no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
What a card is#
When a link is pasted, the site reading it fetches the page and looks for og:image in the head. That URL is the card. Here a component beside the route draws one for each page, with the same props the page gets.
Start with a recipe page#
Some recipes to draw, in app/src/recipes.ts:
;
;The recipe route's loader finds the recipe or fails with a 404:
;
;
;
;;
Name the origin#
A site reading og:image only follows an absolute URL. Tell the host which origin this deployment answers on, under [document] in config/app.toml, below title:
[]
= "cards"
= "http://127.0.0.1:3000"In production this is your real domain,
https://cards.example. The value is the scheme and the host only; a trailing slash is refused at boot.
Draw the card#
Put opengraph-image.tsx beside the page:
;
;
;
The props are the page's: RecipeSlugProps, the loader's data. size and alt are literals because the build reads them without running anything. Every element is a flex box unless it says otherwise, the same default Next's ImageResponse uses, so a card written for Next draws the same here.
Start the server:
Look at what a crawler sees#
The build saw the component beside the page and the host wrote those tags with the size, type and alt, against the origin you named. The v changes when the card's component changes, so a crawler that cached the old design fetches the new one after a deploy. It doesn't follow the data: every recipe carries the same v and a renamed recipe keeps it.
Open http://127.0.0.1:3000/recipe/shakshuka/opengraph-image.png in the browser. It's a 1200 by 630 card in cream and rust: "cards" small at the top, "Shakshuka for a slow Sunday" large in the middle and "35 min · serves 4 · by Dani" along the bottom. Change the URL to /recipe/dal/opengraph-image.png and the same component draws the dal.
A recipe that doesn't exist has no card either:
The host ran the page's loader to draw the card, so the loader's fail("not_found", ...) answers the image too.
One card for everything else#
The index has no card yet. An image beside a layout covers every page under it and a nearer one replaces it. So a card in app/routes/ covers the index while each recipe keeps its own. It needs the recipe count, which the index page doesn't load, so it gets a loader of its own beside it:
;
;
;
RootOpengraphImageProps is generated from what that loader returns, the way a page's props are from its own. size is left out, so it's the default 1200 by 630. Check both pages:
/opengraph-image.png is a dark card reading "cards" and "3 recipes, each with its own card". Add a fourth recipe to RECIPES and reload it: it says 4.
Draw them once, at build#
In development the host draws a card on every request, so an edit shows up on reload. In production a page that prerenders gets its card written beside it. The recipe route needs paths to prerender, as in 110. Add it as the last line of app/routes/recipe/[slug]/page.loader.ts:
;Then point [server] in config/app.toml at a directory, below listen:
[]
= "127.0.0.1:3000"
= "dist/prerender"Stop fsr dev, then build and prerender:
The fsr client modules the pages load are written alongside under static/js/fsr/; they're left out above. Every card is a file now, so production serves it from disk. A recipe added between deploys still gets a card, drawn on request until the next prerender.
Try it#
Give the recipe card's outer div a width: "wide". The image answers 500 and the body names the element and the value: the image did not draw: div(1): `width: wide` does not parse: `wide` is not a length. A value that isn't CSS fails the card rather than drawing something you didn't ask for.
Then add a twitter-image.tsx beside the recipe page with a different layout. The head gains twitter:image rows for it while og:image keeps the first card. X reads og:image when there's no twitter-image, so you only need the second file when the two should differ.