feat(seo): site meta composable + Open Graph image generation

Adds `useSiteMeta()` composable that wraps useSeoMeta with site defaults
(title template, canonical URL, og/twitter image, og:site_name) and
absolute-URL handling via runtimeConfig.public.appUrl.

Adds /og/events/[slug].png route that renders per-event OG images via
satori + @resvg/resvg-js, cached on disk by slug + updatedAt. Bundles
Brygada 1918 + Commit Mono fonts as server assets, ships a fallback
default.png, and patches @shuding/opentype.js via patch-package.

Converts ~25 pages from useHead to useSiteMeta and adds noindex on
private/auth/admin pages.
This commit is contained in:
Jennie Robinson Faber 2026-05-21 17:50:34 +01:00
parent 877ef1a220
commit 31144617d7
36 changed files with 1182 additions and 53 deletions

View file

@ -0,0 +1,29 @@
// GET /og/events/[slug].png — generated Open Graph image for an event.
// Cached on disk by slug + event.updatedAt so any admin edit busts the cache.
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
if (!slug) {
throw createError({ statusCode: 400, statusMessage: 'Missing slug' })
}
// .png suffix is part of the route filename, but slugs in DB don't include it.
const cleanSlug = slug.replace(/\.png$/, '')
const eventDoc = await loadPublicEvent(event, cleanSlug, { lean: true })
const key = eventCacheKey(eventDoc)
let png = await getCachedOG(key)
if (!png) {
png = await renderEventOG(eventDoc)
await setCachedOG(key, png)
}
setHeader(event, 'Content-Type', 'image/png')
setHeader(
event,
'Cache-Control',
'public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400'
)
return png
})