When someone shares a link to your site, apps such as Slack, Discord, iMessage, and X show a preview card. The image of that card comes from the og:image meta tag of the page. This guide draws a card for each page with $assets.render, and adds the meta tags. Read Dynamic images first.
Install
bun add -d satori @resvg/resvg-jsAdd a font
Satori reads TTF, OTF, and WOFF files, but not WOFF2. Put the fonts of your card in assets/. Fontsource packages have WOFF files for each weight, such as @fontsource/inter/files/inter-latin-700-normal.woff. Copy the file into assets/fonts/:
cp node_modules/@fontsource/inter/files/inter-latin-700-normal.woff assets/fonts/inter-700.woffThe font files are not copied to public/. Only the cards use them.
Draw the card
A card is 1200 by 630 pixels, the default size of $assets.render. This partial prints the title and the summary of the page, and the name and the address of the site:
<style>
@font-face { font-family: Inter; src: url(fonts/inter-700.woff); font-weight: 700 }
</style>
<div style="display: flex; flex-direction: column; width: 100%; height: 100%; padding: 72px 80px; box-sizing: border-box; background: #141414; color: #fffdf7; font-family: Inter">
<div style="font-size: 32px; color: #ffd23f">{{ $site.title }}</div>
<div style="margin-top: auto; font-size: 88px; line-height: 1.05">{{ $page.title }}</div>
<div style="margin-top: 24px; font-size: 32px; color: #fffdf7b3">{{ $utils.excerpt($page.summary, 110) }}</div>
<div style="margin-top: 40px; font-size: 26px">{{ $page.url.replace(/^https?:\/\//, '') }}</div>
</div>In dev, open the URL that $assets.render returns to see the card. bongo draws it again when you change the partial.
Add the meta tags
Put the tags in the <head> of your layout. og:image must be a full URL, so give the path to $utils.url.abs(). It resolves against $site.url, so set $site.url to the address of the live site.
{{#var path = await $assets.render('og/card')}}
{{#var card = $utils.url.abs(path)}}
<meta property="og:type" content="website"/>
<meta property="og:title" content="{{ $page.title }}"/>
<meta property="og:description" content="{{ $page.summary }}"/>
<meta property="og:url" content="{{ $page.url }}"/>
<meta property="og:image" content="{{ card }}"/>
<meta property="og:image:type" content="image/png"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<meta property="og:image:alt" content="{{ $page.title }}"/>
<meta name="twitter:card" content="summary_large_image"/>X reads the og: tags too. twitter:card tells it to show the large image.
Name the cards
Do not give an output path. Then each card has a name from its content, such as card.3f9a1c2e.png, and the name changes when the card changes. Thus your host can cache cards for a long time, and an app that shares the link gets the new card.
With a fixed path, such as og/home.png, the URL stays the same when the card changes. Caches and apps can then show the old card.
Change the card for some pages
Give the partial values with args, such as a color for each section:
{{#var tones = { blog: '#ff5a36', docs: '#7cc4ff' } }}
{{#var tone = tones[$page.section] || '#ffd23f'}}
{{#var path = await $assets.render('og/card', undefined, { args: { tone } })}}
{{#var card = $utils.url.abs(path)}}The partial declares the value with {{#expect tone}}, and uses it in a style, such as background: {{ tone }}.
Draw cards for some pages only with {{#if}}, such as {{#if $page.section === 'blog'}}.
Use your own image
A page can name its own image in its frontmatter, in place of the drawn card:
---
title: Launch day
image: img/launch.jpg
---<bongo:asset> publishes that file from assets/. It writes a URL path, so put the origin of the site in front of it:
{{#var image = $page.params.image}}
{{#var origin = new URL($site.url).origin}}
{{#if image}}
<meta property="og:image" content="{{ origin }}<bongo:asset src='{{ image }}' />"/>
{{#else}}
{{#var path = await $assets.render('og/card')}}
<meta property="og:image" content="{{ $utils.url.abs(path) }}"/>
{{/if}}Give the width, height, and type of your own image too, or leave those tags out for it.