Skip to content

Commit 36da187

Browse files
codercatdevclaude
andcommitted
feat: OG image generation on Workers
Four Satori-backed endpoints — /api/og/{default,blog,podcast,person}.png — recovered from the pre-Next Astro app along with the two Inter TTFs and the rawFonts Vite plugin that inlines them as Uint8Array (Workers has no filesystem for Satori to read from). The load-bearing part of that recovery is its workaround, kept verbatim with its explanation: workers-og parses HTML with HTMLRewriter, which corrupts the HTML-to-VDOM conversion on Workers, so these build React element objects and hand them to ImageResponse directly. Wired in as a FALLBACK, not a replacement. Where a document has a cover image the card is still that image, byte-for-byte what the Next app served. Documents without one previously emitted no og:image at all; those now get a generated card. The home page and section indexes get one too. Verified on real workerd under `wrangler dev`, not `astro dev` — all four endpoints return valid 1200x630 PNGs, and the rendered images were inspected rather than just status-checked. That caught the one visual defect: the recovered logo used a cat emoji, and Satori renders only glyphs present in the fonts it is given, so it came out as a tofu box on every card. Replaced with a monogram tile in the brand gradient. Brand tokens are literal hex rather than the CSS custom properties the rest of the site uses: Satori resolves neither var() nor oklch(). Font imports are now typed via a `*.ttf` module declaration, since Astro's own asset typings declare them as URL strings — the opposite of what the rawFonts plugin actually returns. That removes the `@ts-ignore` pair and the `as any` in loadFonts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2
1 parent c5cc7a3 commit 36da187

21 files changed

Lines changed: 875 additions & 3 deletions

‎apps/site/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"groq": "^6.9.2",
3131
"react": "^19.2.6",
3232
"react-dom": "^19.2.6",
33-
"tailwindcss": "^4.2.1"
33+
"tailwindcss": "^4.2.1",
34+
"workers-og": "^0.0.27"
3435
},
3536
"devDependencies": {
3637
"@astrojs/check": "^0.9.10",
411 KB
Binary file not shown.
402 KB
Binary file not shown.

‎apps/site/src/env.d.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
/// <reference path="../.astro/types.d.ts" />
22

3+
/**
4+
* Font files imported for OG image generation.
5+
*
6+
* The `rawFonts` Vite plugin in astro.config.mjs intercepts these and returns
7+
* the file contents as a Uint8Array — Workers has no filesystem, so Satori
8+
* cannot read them at runtime. Astro's own asset typings declare `.ttf`
9+
* imports as a URL string, which is the opposite of what actually arrives, so
10+
* they are re-declared here.
11+
*/
12+
declare module "*.ttf" {
13+
const data: Uint8Array;
14+
export default data;
15+
}
16+
17+
declare module "*.otf" {
18+
const data: Uint8Array;
19+
export default data;
20+
}
21+
322
declare namespace App {
423
interface Locals {
524
sanity: import("./lib/sanity/context").SanityRequestContext;

‎apps/site/src/layouts/BaseLayout.astro‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Header from "@/components/Header.astro";
88
import JsonLd from "@/components/JsonLd.astro";
99
import SocialMeta from "@/components/SocialMeta.astro";
1010
import ThemeScript from "@/components/ThemeScript.astro";
11+
import { fallbackOgImage, type OgFallback } from "@/lib/og";
1112
import { absoluteUrl } from "@/lib/site";
1213
import {
1314
buildGraph,
@@ -25,6 +26,11 @@ interface Props {
2526
authors?: Array<string | null | undefined>;
2627
canonical?: string | URL;
2728
noindex?: boolean;
29+
/**
30+
* Used to generate an OG card when `image` is absent. Documents without a
31+
* cover image previously shipped no og:image at all.
32+
*/
33+
ogFallback?: OgFallback;
2834
/** Extra JSON-LD nodes merged into the site-wide @graph. */
2935
schema?: Array<Record<string, unknown> & { "@type": string }>;
3036
/** Full-bleed pages opt out of the default container. */
@@ -41,13 +47,17 @@ const {
4147
authors,
4248
canonical,
4349
noindex,
50+
ogFallback,
4451
schema = [],
4552
bare = false,
4653
} = Astro.props;
4754
4855
const origin = Astro.locals.siteUrl.origin;
4956
const preview = Astro.locals.sanity.preview.enabled;
5057
58+
const socialImage =
59+
image ?? (ogFallback ? fallbackOgImage(origin, ogFallback) : undefined);
60+
5161
const graph = buildGraph([
5262
organizationSchema(origin, absoluteUrl("/icon.svg", origin)),
5363
websiteSchema(origin),
@@ -67,7 +77,7 @@ const graph = buildGraph([
6777
<SocialMeta
6878
title={title}
6979
description={description}
70-
image={image}
80+
image={socialImage}
7181
type={type}
7282
publishedAt={publishedAt}
7383
modifiedAt={modifiedAt}

0 commit comments

Comments
 (0)