AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

Select the types of activity you want to include in your feed.


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/radiohead serves the generated PNG
  • GET /artist/radiohead (the page route) automatically gets og:image meta 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.