feat: og image and seo metadata - #11
Merged
Merged
Conversation
Build-time OG card rendering the same domain-warped field as the live background, sampled per character cell into the site's ASCII ramp and set in Geist Pixel Square, with the middle cleared for the wordmark. Adds full metadata (openGraph, twitter, canonical, robots, keywords), a sitemap, robots.txt, and a pixel favicon. Two things the font needed: geist's exports map blocks deep paths under Node, so the woff2 is read by path; and takumi ignores a raw Buffer, so it is passed as FontDetails over a Uint8Array. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQy3ZJ5MyUxo4qjZwA93Na
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQy3ZJ5MyUxo4qjZwA93Na
eliahilse
added a commit
that referenced
this pull request
Jul 31, 2026
#11 shipped an OG card that rendered perfectly at build time and **404'd in production**. Two causes: 1. `@takumi-rs/image-response`'s Node build uses a **native binary that cannot run on Cloudflare Workers**, so the route could never execute there. 2. OpenNext does not ship a prerendered route-handler body as a static asset, so the build-time PNG was discarded — the route was expected to run at request time. `/sitemap.xml` and `/icon.svg` 404'd for the same class of reason (route-handler and app-convention outputs that never became assets). ## Fix Everything is now a plain static asset, so nothing has to execute on the edge: - `scripts/generate-og.tsx` renders the card in Node at build time and writes `public/og.png`; wired into `build`, `deploy`, and `preview` so it can never drift from the design - `sitemap.xml` and `icon.svg` moved to `public/` - metadata points at `/og.png`, plus an explicit `icons` entry - the generator **fails the build** if the pixel font can't be found or the render comes back suspiciously small — the earlier font bug was silent, and this makes that class of failure loud - `public/og.png` is gitignored since it is reproducible from the script ## Verified live on kyora.sh | path | status | | --- | --- | | `/` | 200 text/html | | `/og.png` | 200 image/png, 126,193 b | | `/icon.svg` | 200 image/svg+xml | | `/sitemap.xml` | 200 application/xml | | `/robots.txt` | 200 text/plain | `og:image` and `twitter:image` both resolve to the live PNG, `twitter:card` is `summary_large_image`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01CQy3ZJ5MyUxo4qjZwA93Na Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
OG image
A build-time card at
/ogthat is the site: the same domain-warped fbm field as the live WebGL background, sampled once per character cell into the same ASCII ramp, set in Geist Pixel Square, with a radial clearing so the wordmark sits on black.Prerendered as a static PNG (
force-static), so Cloudflare serves it as an asset with no runtime cost.Two non-obvious things the font needed, both worth recording since they fail silently:
exportsmap blocks deep paths.require.resolve("geist/dist/fonts/…")and evengeist/package.jsonthrow under Node — Bun happily resolves both, so this only breaks in the real build. The woff2 is now read by path with fallbacks.Buffer. Passingfonts: [buffer]renders byte-identical output to passing no font at all — no error, no warning. It has to beFontDetails({ name, data }) over aUint8Array. Verified by byte-diffing renders with and without.SEO
metadataBase, title template, openGraph (type, url, siteName, image), twittersummary_large_imagewith@eliahilse, canonical, keywords, author, robots withmax-image-preview: largesitemap.xmlandrobots.txtas static routesicon.svg) matching the wordmark aestheticThe previous description ("Runtime observability SDK for coding agents") predated review and council; it now covers all three.
Stacked on nothing — branches off main, safe to merge whenever.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CQy3ZJ5MyUxo4qjZwA93Na