Stedefast

Plugins

OG Images

5 min read

/plugin-og-images generates Open Graph (social card) images for every content node at build time. Each page gets a 1200×630 PNG written to dist/assets/og/{slug}.png. A sidecar manifest at dist/data/og-images.json maps slugs to their public paths so your theme can inject <meta og:image> tags.

Images that already exist on disk are skipped on rebuild, so incremental builds are fast.

Installation

pnpm add /plugin-og-images

Basic setup

// stedefast.config.ts
import { defineConfig } from "@stedefast/core";
import { OgImagesPlugin } from "/plugin-og-images";

export default defineConfig({
  // ...
  plugins: [
    OgImagesPlugin({
      fonts: [
        { name: "Inter", path: "./theme/fonts/Inter-Bold.ttf", weight: 700 },
      ],
    }),
  ],
});

This uses the built-in DefaultOgTemplate — a dark slate card with the page title in white and the site name at the bottom.

Writing a custom template

Templates are React components that receive page data as props:

// theme/og-template.tsx
import type { OgTemplateProps } from "/plugin-og-images";

export default function OgTemplate({
  title,
  description,
  siteTitle,
  date,
  tags,
}: OgTemplateProps) {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        width: 1200,
        height: 630,
        background: "#0f172a",
        padding: 80,
      }}
    >
      <div style={{ fontSize: 64, fontWeight: 700, color: "white" }}>
        {title}
      </div>
      {description && (
        <div style={{ fontSize: 32, color: "#94a3b8", marginTop: 24 }}>
          {description}
        </div>
      )}
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "flex-end",
          marginTop: "auto",
        }}
      >
        <div style={{ fontSize: 24, color: "#64748b" }}>{siteTitle}</div>
        {date && (
          <div style={{ fontSize: 24, color: "#64748b" }}>{date}</div>
        )}
      </div>
    </div>
  );
}

Register it in your config:

import OgTemplate from "./theme/og-template.tsx";

OgImagesPlugin({
  template: OgTemplate,
  fonts: [
    { name: "Inter", path: "./theme/fonts/Inter-Regular.ttf", weight: 400 },
    { name: "Inter", path: "./theme/fonts/Inter-Bold.ttf", weight: 700 },
  ],
})

Satori CSS limitations

Satori renders React elements via its own layout engine. It supports a subset of CSS — specifically the same constraints as React Native's flexbox layout. Key limitations:

  • Inline styles only — no CSS classes, no className, no external stylesheets
  • No background-image — use a solid background colour or gradient
  • Limited positionrelative and absolute work; fixed and sticky do not
  • No overflow: scroll — use overflow: hidden
  • Flexbox only — no CSS Grid
  • No calc() — use numeric pixel values
  • No pseudo-elements:hover, ::before, etc. are ignored
  • display: flex is required on container elements — block layout is not supported

For the full list, see the Satori documentation.

Template props reference

Prop Type Description
title string Page title from front matter
description string | undefined Page description from front matter
date string | undefined Publication date as "YYYY-MM-DD"
tags string[] | undefined Tag array from front matter
siteTitle string The site title from SiteConfig.siteTitle
type string Content type, e.g. "post", "page"

Using the default template

The built-in DefaultOgTemplate renders a dark slate card (#0f172a) with:

  • The page title in large white text (64px, bold)
  • The siteTitle in muted slate text at the bottom (28px, #94a3b8)

It works without any custom fonts — Satori's built-in fallback font is used if none are provided. For best results, supply a web font.

// Minimal setup — no template, no fonts
OgImagesPlugin()

Using generated images in your layout

The plugin writes a manifest to dist/data/og-images.json:

{
  "hello-world": "/assets/og/hello-world.png",
  "about": "/assets/og/about.png"
}

Read this file in your layout or template to inject the correct og:image meta tag. In a React template:

// theme/layouts/default.tsx
import ogImages from "../../dist/data/og-images.json";

export default function DefaultLayout({ page, site }) {
  const ogImagePath = ogImages[page.slug];
  const ogImageUrl = ogImagePath
    ? `${site.baseUrl}${ogImagePath}`
    : undefined;

  return (
    <html>
      <head>
        <title>{page.frontMatter.title}</title>
        {ogImageUrl && (
          <>
            <meta property="og:image" content={ogImageUrl} />
            <meta property="og:image:width" content="1200" />
            <meta property="og:image:height" content="630" />
            <meta name="twitter:card" content="summary_large_image" />
            <meta name="twitter:image" content={ogImageUrl} />
          </>
        )}
      </head>
      <body>{/* ... */}</body>
    </html>
  );
}

Alternatively, use the static path pattern directly without reading the manifest:

const ogImageUrl = `${site.baseUrl}/assets/og/${page.slug}.png`;

This is simpler but will produce broken image links for pages where generation was skipped (e.g. if the plugin is not configured).

Font configuration

Satori requires font data to render text. Provide TTF or OTF files:

OgImagesPlugin({
  fonts: [
    // Regular weight for body text
    { name: "Inter", path: "./theme/fonts/Inter-Regular.ttf", weight: 400 },
    // Bold weight for titles
    { name: "Inter", path: "./theme/fonts/Inter-Bold.ttf", weight: 700 },
    // Italic variant
    {
      name: "Inter",
      path: "./theme/fonts/Inter-Italic.ttf",
      weight: 400,
      style: "italic",
    },
  ],
})
  • name — the font family name as used in fontFamily style values
  • path — path to the font file, relative to your stedefast.config.ts
  • weight — numeric weight (e.g. 400, 700)
  • style"normal" (default) or "italic"

You can download variable fonts from Google Fonts and use them as static font files.

Caching behaviour

The plugin skips generating an image if dist/assets/og/{slug}.png already exists on disk. This means:

  • First build — all images are generated
  • Subsequent builds — only new pages (or pages whose image was deleted) are generated
  • To force regeneration — delete dist/assets/og/ or run stedefast build --clean

The build log shows the counts:

  og-images  42 generated  8 cached  (3.2s)

Config reference

Option Type Default Description
template React.ComponentType<OgTemplateProps> DefaultOgTemplate React component to render as the OG image
width number 1200 Output image width in pixels
height number 630 Output image height in pixels
fonts OgFontConfig[] [] Font files to load for Satori

OgFontConfig

Field Type Default Description
name string Font family name
path string Path to font file, relative to config
weight number Font weight (e.g. 400, 700)
style "normal" | "italic" "normal" Font style

How it works

OgImagesPlugin uses the buildHook extension point in the StedefastPlugin interface. This hook runs after Stage 2 (content graph) in the build pipeline, before the asset pipeline and page rendering. It receives the full ContentGraph, the resolved SiteConfig, and the output directory path.

For each content node, the plugin:

  1. Renders your template component to SVG using Satori
  2. Converts the SVG to PNG using @resvg/resvg-js
  3. Writes the PNG to dist/assets/og/{slug}.png
  4. Records the path in the dist/data/og-images.json manifest

Up to 4 images are generated concurrently to keep build times reasonable on large sites.