Files
garden-astro/src/pages/og/[...slug].png.ts
T
wesandClaude Fable 5 8f48656e91 Build stamp, live listening strip, OG cards, closer shiki ground
- footer: 'built <ts> · <hash> · <n> notes', hash linking to Gitea
  (GARDEN_COMMIT passed in by build.sh; '+' marks dirty-tree builds)
- homepage header: live strip fed by api.c0smere.net/spotify/summary
  (weekly aggregate + monthly top artist/genre), fail-closed like the
  odometer and header quote
- /og/<slug>.png satori+resvg cards for every note + the homepage;
  og:/twitter: meta in Base
- shiki github-dark-default (#0d1117 sits closer to the page ground)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:08:53 -04:00

129 lines
3.8 KiB
TypeScript

// Build-time OG images: 1200x630 dark cards matching the site design,
// one per note (plus /og/index.png for the homepage). satori renders an
// element tree to SVG, resvg rasterizes to PNG. Base.astro points
// og:image here via the ogSlug prop.
import fs from 'node:fs';
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import { slugForId } from '../../lib/quartz-compat.mjs';
const FONT_DIR = 'node_modules/@fontsource/jetbrains-mono/files';
const mono400 = fs.readFileSync(`${FONT_DIR}/jetbrains-mono-latin-400-normal.woff`);
const mono800 = fs.readFileSync(`${FONT_DIR}/jetbrains-mono-latin-800-normal.woff`);
export async function getStaticPaths() {
const entries = await getCollection('garden', (e: any) => !e.data.draft);
return entries.map((e: any) => {
const isIndex = e.id === 'index';
const title = isIndex
? 'Wesley Ray'
: (e.data.title ?? e.id.split('/').pop());
const dir = e.id.includes('/') ? e.id.slice(0, e.id.lastIndexOf('/')) : '';
return {
params: { slug: isIndex ? 'index' : slugForId(e.id) },
props: {
title,
dir,
subtitle: isIndex ? 'projects, notes, and experiments' : dir,
},
};
});
}
export const GET: APIRoute = async ({ props }) => {
const { title, subtitle } = props as { title: string; subtitle: string };
const size = title.length > 55 ? 44 : title.length > 32 ? 56 : 68;
const svg = await satori(
{
type: 'div',
props: {
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: '#0b0d12',
borderLeft: '14px solid #218107',
padding: '64px 72px',
fontFamily: 'JetBrains Mono',
color: '#ffffff',
},
children: [
{
type: 'div',
props: {
style: {
display: 'flex',
fontSize: '26px',
color: 'rgba(255,255,255,0.64)',
letterSpacing: '2px',
},
children: 'the garden // Wesley Ray',
},
},
{
type: 'div',
props: {
style: {
display: 'flex',
fontSize: `${size}px`,
fontWeight: 800,
lineHeight: 1.25,
letterSpacing: '-1px',
},
children: title,
},
},
{
type: 'div',
props: {
style: {
display: 'flex',
justifyContent: 'space-between',
fontSize: '26px',
},
children: [
{
type: 'div',
props: {
style: {
display: 'flex',
color: 'rgba(255,255,255,0.64)',
},
children: subtitle || ' ',
},
},
{
type: 'div',
props: {
style: { display: 'flex', color: '#2fa50f' },
children: 'garden.c0smere.net',
},
},
],
},
},
],
},
},
{
width: 1200,
height: 630,
fonts: [
{ name: 'JetBrains Mono', data: mono400, weight: 400, style: 'normal' },
{ name: 'JetBrains Mono', data: mono800, weight: 800, style: 'normal' },
],
},
);
const png = new Resvg(svg, {
fitTo: { mode: 'width', value: 1200 },
}).render().asPng();
return new Response(png, { headers: { 'Content-Type': 'image/png' } });
};