コンテンツにスキップ
まだあたたかい

Automatic OG images: zero-effort social previews

How this theme generates beautiful Open Graph images at build time using Satori and Resvg  Eand how to customise or disable the feature.

Features 4 分で読めます

When you share a blog post link on Twitter, Discord, Slack, or LinkedIn, the platform fetches an Open Graph image to display as a visual preview. Without one, your link looks like a plain text snippet  Eeasy to scroll past.

This theme automatically generates a styled 1200ÁE30 PNG for every post that doesn’t already have a heroImage set. No design tool, no manual work  Ejust write your post and the OG image appears.

At build time, the theme:

  1. Loops through every post in every locale.
  2. For posts without a heroImage, generates a PNG at /og/<slug>.png (or /og/<locale>/<slug>.png for non-default locales).
  3. Wires the generated path into the <meta property="og:image"> and <meta name="twitter:image"> tags automatically.

Posts with a heroImage continue to use that image as their OG preview  Ethe generated image is a fallback, not an override.

  • Satori converts a React-like element tree into SVG. It supports flexbox layout, fonts, gradients, and border-radius  Eenough to build card-style designs.
  • @resvg/resvg-js renders the SVG into a high-quality PNG at exact pixel dimensions.
  • @fontsource/inter provides the Inter font locally (regular + bold), so no network requests happen during the build.

The entire pipeline runs server-side at build time. Zero JavaScript or external requests are added to your deployed site.

Each OG image features:

  • A deep indigo-blue gradient border (matching the theme’s primary colour).
  • A white card with rounded corners and a subtle shadow.
  • Category badge (top-left) in the theme’s indigo pill style.
  • Publication date (top-right).
  • Post title in bold, with adaptive font sizing for longer titles.
  • Description excerpt below the title (truncated at ~120 chars).
  • Site branding (bottom-left)  Ethe site name with a brand-coloured dot.
  • Tags (bottom-right)  Eup to 3 tags as pill badges, or the site hostname if no tags are set.

Here’s the OG image that was automatically generated for this very post:

Auto-generated OG image for this post

The feature is controlled by a single flag in src/config.ts:

src/config.ts
export const SITE: SiteConfig = {
// ...
autoOgImage: true, // Set to false to disable
};

When enabled (default):

  • A PNG is generated for every post at build time.
  • Posts without heroImage use the generated OG image.
  • Posts with heroImage continue to use their hero.

When disabled:

  • No OG images are generated (the /og/ route produces zero pages).
  • Posts without heroImage fall back to SITE.defaultOgImage (typically via SITE.defaultOgImage).

The OG image template lives in src/utils/og-image.ts. It exports a single function:

export async function generateOgImage(data: OgImageData): Promise<Buffer>;

The OgImageData interface:

interface OgImageData {
title: string;
description?: string;
date?: string;
category?: string;
tags?: string[];
}

The template uses hardcoded hex colours that match the Chirpy theme:

ElementCurrent colourWhere to change
Gradient background#1e3a5f ↁE#4a6cf7background property
Card background#ffffffbackgroundColor
Category badge#2a408eMultiple style objects
Title text#1f2937Title color
Description text#6b7280Description color
Brand accent dot#2a408eBrand dot style

If you change your site’s --color-primary in CSS, consider updating these hardcoded values to match.

The template uses Inter (400 + 700) loaded from @fontsource/inter. To use a different font:

  1. Install the font package: bun add @fontsource/your-font

  2. Update the file paths in src/utils/og-image.ts:

    const fontsDir = join(process.cwd(), 'node_modules/@fontsource/your-font/files');
    const fontRegular = readFileSync(join(fontsDir, 'your-font-latin-400-normal.woff'));
    const fontBold = readFileSync(join(fontsDir, 'your-font-latin-700-normal.woff'));
  3. Update the font name in the satori() options:

    fonts: [
    { name: 'YourFont', data: fontRegular, weight: 400, style: 'normal' },
    { name: 'YourFont', data: fontBold, weight: 700, style: 'normal' },
    ],

Satori uses a flexbox-only layout engine. Every element must have display: 'flex' if it has more than one child. The markup is a plain JavaScript object tree (no JSX needed):

{
type: 'div',
props: {
style: { display: 'flex', /* ... */ },
children: 'Hello world',
},
}

Satori supports: flexbox (flex-direction, gap, justify-content, align-items), border-radius, padding, margin, background (including linear-gradient), box-shadow, font-size, font-weight, color, line-height, border, and more.

It does not support: CSS Grid, position: absolute/relative, transform, animations, pseudo-elements, or media queries.

To add a site logo/avatar to the OG image, read it as a base64 data URL:

import { readFileSync } from 'node:fs';
import { join } from 'node:path';
const avatar = readFileSync(join(process.cwd(), 'src/assets/images/site/avatar.svg'));
const avatarDataUrl = `data:image/svg+xml;base64,${avatar.toString('base64')}`;

Then add an img element in the template:

{
type: 'img',
props: {
src: avatarDataUrl,
width: 48,
height: 48,
style: { borderRadius: '50%' },
},
}

If a post has a heroImage in frontmatter, that image is used for the OG meta tag  Ethe auto-generated image is not used:

---
title: My post with a custom OG
heroImage: ../../assets/images/posts/automatic-og-images/my-custom-og.png
---

This gives you full control on a per-post basis. Write posts without heroImage to get the automatic OG, or set one when you want a bespoke visual.

  • Build time: Each image takes ~50 E00ms to generate (depending on your machine). For a site with 100 posts ÁE2 locales, that’s ~10 E0 seconds added to your build.
  • File size: Generated PNGs are typically 100 E15 KB  Ewell within social platform limits (Twitter recommends < 5 MB, LinkedIn < 8 MB).
  • No runtime cost: Images are static files served from your CDN. No serverless functions or edge rendering needed.
  • Font loading: Fonts are read from node_modules on disk  Eno network requests during the build.

SymptomFix
OG image not showing on social platformsVerify the image URL is accessible. Use opengraph.xyz or Twitter’s Card Validator.
Build fails with Satori error about display: flexEnsure every div element with multiple children has display: 'flex' in its style.
Fonts look wrong or show squaresConfirm the .woff font files exist at the path in og-image.ts. Run bun install if needed.
Want to disable for performanceSet autoOgImage: false in src/config.ts.