title: OpenGraph Images description: Generate dynamic OpenGraph images for link previews.#
Hatk generates dynamic OpenGraph images so your pages get rich previews when shared. You define a generate function that returns a virtual DOM tree, and Hatk renders it to a 1200x630 PNG using Satori.
Defining an OG route#
Create a file in server/og/ that exports defineOG() with a path pattern and a generate function:
// server/og/artist.ts
import { defineOG } from '$hatk'
export default defineOG('/og/artist/:artist', async (ctx) => {
const { db, params, fetchImage } = ctx
const rows = await db.query(
`SELECT CAST(COUNT(*) AS INTEGER) AS play_count
FROM "fm.teal.alpha.feed.play__artists"
WHERE artist_name = ?`,
[params.artist],
)
const stats = rows[0] || { play_count: 0 }
return {
element: {
type: 'div',
props: {
style: {
display: 'flex',
width: '100%',
height: '100%',
background: '#070a11',
color: 'white',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
},
children: [
{ type: 'div', props: { children: params.artist, style: { fontSize: 58, fontWeight: 700 } } },
{ type: 'div', props: { children: `${stats.play_count} plays`, style: { fontSize: 28, color: '#94a3b8', marginTop: '16px' } } },
],
},
},
meta: { title: params.artist },
}
})
How it works#
The path field uses Express-style route parameters. The /og prefix is significant:
GET /og/artist/radioheadserves the generated PNGGET /artist/radiohead(the page route) automatically getsog:imagemeta tags injected pointing to the OG image URL
This keeps page routes and OG routes in sync. You don't need to add meta tags manually.
Generate context#
The generate function receives an OpengraphContext with:
| Field | Description |
|---|---|
db.query(sql, params?) |
Run SQL queries against SQLite |
params |
URL path parameters (e.g. { artist: 'Radiohead' }) |
fetchImage(url) |
Fetch a remote image and return it as a base64 data URL for use in img elements |
lookup(collection, field, values) |
Look up records by field value |
count(collection, field, values) |
Count records by field value |
labels(uris) |
Query labels for record URIs |
blobUrl(did, cid) |
Resolve a blob reference to a URL |
Return value#
Return an OpengraphResult:
| Field | Required | Description |
|---|---|---|
element |
Yes | A Satori virtual DOM tree |
options |
No | Override width (default 1200), height (default 630), or provide custom fonts |
meta |
No | title and description for the injected meta tags |
Virtual DOM#
Satori uses a React-like virtual DOM. Elements are plain objects with type and props containing style and children:
{
type: 'div',
props: {
style: { display: 'flex', flexDirection: 'column', gap: '16px' },
children: [
{ type: 'div', props: { children: 'Hello', style: { fontSize: 48 } } },
{ type: 'img', props: { src: imageDataUrl, width: 300, height: 300 } },
],
},
}
All layouts must use display: 'flex'. See the Satori docs for supported CSS properties.
Using fetchImage#
Remote images must be converted to base64 data URLs before Satori can render them:
const artUrl = await ctx.fetchImage('https://example.com/image.jpg')
// Returns "data:image/jpeg;base64,..." or null on failure
Then pass it as the src on an img element.
Caching#
Generated images are cached in memory for 5 minutes (up to 200 entries). A default Inter font is bundled; custom fonts can be provided via options.fonts.