diff --git a/.gitattributes b/.gitattributes index 7818898..d57e87a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,6 +10,8 @@ *.pdf binary *.woff binary *.woff2 binary +*.ttf binary +*.otf binary *.ico binary # Shell scripts and container files must stay LF even on Windows checkout. diff --git a/site/src/assets/og-fonts/Inter.ttf b/site/src/assets/og-fonts/Inter.ttf new file mode 100644 index 0000000..047c92f Binary files /dev/null and b/site/src/assets/og-fonts/Inter.ttf differ diff --git a/site/src/assets/og-fonts/JetBrainsMono.ttf b/site/src/assets/og-fonts/JetBrainsMono.ttf new file mode 100644 index 0000000..aa310be Binary files /dev/null and b/site/src/assets/og-fonts/JetBrainsMono.ttf differ diff --git a/site/src/assets/og-fonts/SpaceGrotesk.ttf b/site/src/assets/og-fonts/SpaceGrotesk.ttf new file mode 100644 index 0000000..a1b2e6c Binary files /dev/null and b/site/src/assets/og-fonts/SpaceGrotesk.ttf differ diff --git a/site/src/pages/og/[image].png.ts b/site/src/pages/og/[image].png.ts new file mode 100644 index 0000000..cb4c052 --- /dev/null +++ b/site/src/pages/og/[image].png.ts @@ -0,0 +1,76 @@ +import type { APIRoute } from 'astro'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { Resvg } from '@resvg/resvg-js'; +import { PAGE_IDS, type PageId } from '@i18n/slugMap'; +import { LOCALES, type Locale } from '@i18n/locales'; +import { getProfile, getMeta } from '@lib/content'; + +/* + Build-time OpenGraph images (SEO_SPEC §4): one per page per locale, styled in the + design system (dark surface, name, role line, trace motif, monogram + page tag). + The SVG is hand-built (the design is simple and fully controlled) and rasterised by + resvg, which handles the vendored variable TTFs via its own font DB — satori's + opentype parser cannot read variable fonts. + Fonts read from the project source (cwd = site/ at build). +*/ + +const fontBuffers = ['SpaceGrotesk.ttf', 'Inter.ttf', 'JetBrainsMono.ttf'].map((n) => + readFileSync(resolve(process.cwd(), 'src/assets/og-fonts', n)), +); + +export function getStaticPaths() { + const paths: { params: { image: string }; props: { pageId: PageId; locale: Locale } }[] = []; + for (const pageId of PAGE_IDS) { + for (const locale of LOCALES) { + paths.push({ params: { image: `${pageId}-${locale}` }, props: { pageId, locale } }); + } + } + return paths; +} + +const esc = (s: string) => + s.replace(/&/g, '&').replace(//g, '>'); + +export const GET: APIRoute = ({ props }) => { + const { pageId, locale } = props as { pageId: PageId; locale: Locale }; + const profile = getProfile(locale); + const roleLine = profile.roleLine; + const tag = pageId === 'home' ? '' : (getMeta(pageId, locale).title.split(' · ')[0] ?? ''); + const tagW = Math.min(tag.length * 15 + 44, 1000); + + const svg = ` + + + CB + cesnimda.co.uk + + ${esc(profile.name)} + ${esc(roleLine)} + + + + + + ${ + tag + ? ` + ${esc(tag)}` + : `systems developer · tønsberg, norway` + } +`; + + const png = new Resvg(svg, { + fitTo: { mode: 'width', value: 1200 }, + font: { fontBuffers, loadSystemFonts: false, defaultFontFamily: 'Inter' }, + }) + .render() + .asPng(); + + return new Response(png, { + headers: { + 'Content-Type': 'image/png', + 'Cache-Control': 'public, max-age=31536000, immutable', + }, + }); +};