Add OG image auto-generation for writings, portfolio, and bookshelf

Replaces the legacy Zola script, which was hard-coded to the retired
"CASE FILE" branding, with scripts/generate-og-images.astro-site.mjs
built on the new design system's dark palette + Fraunces/JetBrains Mono.
Base.astro now renders og:image/twitter:image from a per-page ogImage
prop, wired into the writings and bookshelf detail pages.

Uses static-weight Fraunces/Newsreader font packages rather than the
variable fonts the site ships elsewhere, since satori's opentype.js
parser throws on their fvar table.
This commit is contained in:
2026-09-01 08:59:42 +03:00
parent 9ee9795645
commit c035aca132
32 changed files with 410 additions and 11 deletions
+15 -2
View File
@@ -11,22 +11,35 @@ interface Props {
description?: string;
/** Widens .container for dashboard-style layouts (currently just the homepage). */
wide?: boolean;
/** Site-root-relative path to a per-page OG image, e.g. /images/og/writings/foo.png */
ogImage?: string;
}
const { title, description, wide = false } = Astro.props;
const { title, description, wide = false, ogImage } = Astro.props;
const metaDescription = description ?? siteConfig.description;
const currentPath = Astro.url.pathname;
const copyrightRange = await copyrightYearRange();
const pageTitle = `${title} — ${siteConfig.title}`;
const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title} — {siteConfig.title}</title>
<title>{pageTitle}</title>
<meta name="description" content={metaDescription} />
<link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/rss.xml" />
<link rel="alternate" type="application/feed+json" title={siteConfig.title} href="/feed.json" />
<link rel="webmention" href="https://webmention.io/andwati.com/webmention" />
<meta property="og:type" content="website" />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={metaDescription} />
<meta property="og:url" content={Astro.url.href} />
<meta property="og:image" content={ogImageUrl} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:image" content={ogImageUrl} />
<script is:inline>
// Runs before paint to avoid a flash of the wrong theme.
(function () {
@@ -21,7 +21,7 @@ const items = (await getCollection("bookshelf")).sort(
);
---
<Base title={entry.data.title} wide>
<Base title={entry.data.title} wide ogImage={`/images/og/bookshelf/${entry.id}.png`}>
<BookSpines
items={items.map((e) => ({ slug: e.id, title: e.data.title, coverImage: e.data.cover_image }))}
activeSlug={entry.id}
+1 -1
View File
@@ -51,7 +51,7 @@ const description =
const tocHeadings = headings.filter((h) => h.depth <= 3);
---
<Base title={entry.data.title} description={description}>
<Base title={entry.data.title} description={description} ogImage={`/images/og/writings/${entry.id}.png`}>
<h1>{entry.data.title}</h1>
<p class="meta">
{entry.data.date.toDateString()}