Use a complete preset for conventional social cards, share page data with portable metadata, connect Markdown or MDX directly, or keep full visual control with a custom renderer.
Preset config
Brand, theme, route data, and optional images. No renderer file.
Shared metadata
One page definition for the image, canonical URL, Open Graph, X, HTML, and Next.js.
Custom renderer
Sharp, Satori, workers, or an existing encoded image function.
Quick start
Install the package with your preferred package manager, initialize a preset config, and generate the first image.
pnpm add -D @santi020k/ognpm install -D @santi020k/ogyarn add -D @santi020k/ogpnpm exec santi-og init
pnpm exec santi-og generate
pnpm exec santi-og check
pnpm exec santi-og compare --threshold 0.01
pnpm exec santi-og migrate --report --json
pnpm exec santi-og upgrade --to 0.4.0santi-og init now creates a preset-based configuration. It works immediately; writing SVG is optional.
Choose your starting point
import { createPathCards } from '@santi020k/og'
import { definePresetConfig } from '@santi020k/og/presets'
export default definePresetConfig({
outputDirectory: 'public/og/pages',
cards: createPathCards([
{
pathname: '/',
data: {
title: 'Ship the card. Delete the renderer.',
description: 'A complete social image from a small config.',
badge: 'Product',
variant: 'product',
},
},
]),
preset: {
brand: {
name: 'Example',
domain: 'example.com',
logo: 'public/logo.png',
},
theme: { accent: '#7c3aed' },
},
})import { collectContentCards } from '@santi020k/og/content'
import { definePresetConfig } from '@santi020k/og/presets'
export default definePresetConfig({
outputDirectory: 'public/og',
cards: () => collectContentCards({
directory: 'src/content/blog',
basePath: 'blog',
map: entry => ({
title: String(entry.frontmatter.title),
description: String(entry.frontmatter.description ?? ''),
...(typeof entry.frontmatter.cover === 'string'
? { image: entry.frontmatter.cover }
: {}),
variant: 'article',
}),
sources: entry => typeof entry.frontmatter.cover === 'string'
? [entry.filePath, entry.frontmatter.cover]
: [entry.filePath],
}),
preset: {
brand: { name: 'Example Journal' },
variant: 'article',
},
})import { defineConfig } from '@santi020k/og'
import { createSharpRenderer } from '@santi020k/og/sharp'
export default defineConfig({
cards: [{ output: 'index.webp', data: { title: 'Home' } }],
renderer: createSharpRenderer({
renderSvg: ({ title }, { height, width }) =>
`<svg viewBox="0 0 ${width} ${height}">...</svg>`,
webp: { quality: 86 },
}),
})Visual presets
Every preset shares the same accessible title, description, badge, brand, logo, domain, accent, and image data contract. Bundled Inter typography and glyph-aware wrapping make output portable across build machines. A config-level variant is the default; any card can override it.

Simple
A clean default for home pages, status pages, and general sharing.

Article
Editorial composition for posts, guides, changelogs, and announcements.

Docs
Structured visual language for references, component docs, and tutorials.

Product
Product-focused framing for apps, tools, launches, and landing pages.
These four images are rendered by this site’s og.config.mjs during every build.
Theme and card overrides
Set preset.brand, preset.theme, and preset.variant once. Override accent, brand, domain, image, or variant on individual cards.
Generated preset SVG uses portable fill and stroke opacity attributes instead of CSS rgba() colors, improving compatibility with SVG validators and consumer tooling.
Page metadata from the same source
definePageMetadata keeps the title, description, route, and social image together. Derive a card with an explicit renderer mapping, portable tag descriptors, escaped HTML, or a Next.js Metadata API object without adding a framework runtime.
import { createMetaTags, createPageCard, definePageMetadata } from '@santi020k/og/metadata'
import { renderMetaTags } from '@santi020k/og/metadata/html'
import { toNextMetadata } from '@santi020k/og/metadata/next'
export const page = definePageMetadata({
pathname: '/docs',
title: 'Documentation',
description: 'Learn how to generate deterministic social images.',
image: {
output: 'pages/docs.webp',
alt: 'Example documentation social card',
width: 1200,
height: 630,
},
})
export const card = createPageCard(page, {
data: ({ description, title }) => ({
title,
description,
badge: 'Guide',
variant: 'docs',
}),
})
const site = { siteUrl: 'https://example.com', siteName: 'Example' }
export const tags = createMetaTags(page, site)
export const html = renderMetaTags(tags)
export const metadata = toNextMetadata(page, site)The helpers resolve absolute canonical and image URLs, infer image MIME types, include dimensions and alternative text, remove canonical fragments, and validate empty text or invalid dimensions. Article pages can add dates, authors, section, and tags.
Metadata functions
| Function | Purpose |
|---|---|
definePageMetadata | Validate a portable page definition while preserving TypeScript inference. |
resolvePageMetadata | Resolve defaults, title templates, canonical and image URLs, MIME types, and robots settings. |
createPageCard | Derive an OgCard; use data to map SEO fields into renderer-specific data explicitly. |
createMetaTags | Create renderer-neutral title, link, Open Graph, article, robots, and X tag descriptors. |
renderMetaTags | Escape and render descriptors as HTML for static templates, Astro, Eleventy, or server output. |
toNextMetadata | Create the dependency-free structural object accepted by the Next.js App Router Metadata API. |
Page definition
| Property | Required | Description |
|---|---|---|
pathname | Yes | Page route used for its canonical URL and default image output. |
title | Yes | Page, Open Graph, X, and default card title. |
description | Yes | Page, Open Graph, X, and default card description. |
canonical | No | Canonical override as a relative or absolute HTTP(S) URL; fragments are removed. |
image | No | alt, public url, generated output, width (1200), height (630), and inferred or explicit MIME type. |
type | No | website, article, or profile; article data selects article automatically. |
article | No | Publication and modification dates, authors, section, and tags. |
authors / keywords | No | Standard author and keyword metadata; article authors are the fallback. |
locale / alternateLocales | No | Primary and alternate Open Graph locales. |
robots | No | Defaults to index/follow, large image previews, and unlimited snippet and video previews. |
twitter | No | X card, site, and creator overrides; defaults to large-image with an image and summary without one. |
Site defaults
| Property | Default | Description |
|---|---|---|
siteUrl | Required | Absolute HTTP(S) base used for canonical and social image URLs. |
siteName | Omitted | Open Graph site name. |
publicImagePath | /og | Public directory joined with generated image outputs. |
defaultImage | Omitted | Fallback image contract for pages without their own image. |
locale | en_US | Fallback Open Graph locale. |
titleTemplate | Omitted | Title composition containing %s, such as %s — Example. |
twitter | Omitted | Default X card, site, and creator settings; page settings override them. |
Framework-neutral Markdown and MDX
collectContentCards reads YAML frontmatter without starting Astro, Next.js, or another framework. It understands nested index.md routes, excludes drafts by default, and tracks each content file as a source.
import { collectContentCards } from '@santi020k/og/content'
import { definePresetConfig } from '@santi020k/og/presets'
export default definePresetConfig({
outputDirectory: 'public/og',
cards: () => collectContentCards({
directory: 'src/content/blog',
basePath: 'blog',
map: entry => ({
title: String(entry.frontmatter.title),
description: String(entry.frontmatter.description ?? ''),
...(typeof entry.frontmatter.cover === 'string'
? { image: entry.frontmatter.cover }
: {}),
variant: 'article',
}),
sources: entry => typeof entry.frontmatter.cover === 'string'
? [entry.filePath, entry.frontmatter.cover]
: [entry.filePath],
}),
preset: {
brand: { name: 'Example Journal' },
variant: 'article',
},
})Use readContent when you only need the normalized entries. Each entry exposes its body, absolute file path, parsed frontmatter, directory-relative ID, and nested-index-aware slug.
Existing @santi020k/og/astro imports remain compatibility aliases for the same implementation.
| Option | Default | Purpose |
|---|---|---|
directory | Required | Markdown/MDX content directory, relative to root. |
root | Working directory | Base for the content directory and relative paths. |
include | **/*.md, **/*.mdx | Glob patterns evaluated before parsing. |
exclude | [] | Glob patterns removed before parsing. |
filter | Include all | Async-capable predicate evaluated on parsed entries. |
draft | frontmatter.draft | Custom async-capable draft predicate. |
includeDrafts | false | Include entries identified as drafts. |
map | Preset frontmatter mapping | Map an entry to typed card data or return null to omit it. |
coverFields | image, cover, heroImage | Preferred frontmatter image fields for the default mapper. |
basePath | Empty | Route prefix used by deterministic output mapping. |
extension | webp | Default generated image extension. |
output | Pathname output | Override the output filename for each mapped entry. |
sources | Content file | Add covers or other files that invalidate the card cache. |
aggregate | Omitted | Append pagination, tag, locale, or collection cards after entry mapping. |
Typed catalogs and derived collections
createCards maps arrays, JSON, CMS results, products, tag archives, or pagination into typed cards without consumer-side expansion. Shared output rules, sources, dimensions, formats, and aliases stay in one place.
import { createCards } from '@santi020k/og'
const cards = createCards(products, product => ({
title: product.name,
description: product.summary,
image: product.image,
variant: 'product',
}), {
output: product => 'products/' + product.slug + '.webp',
formats: ['png', 'svg'],
formatAliases: product => ({
png: ['social/' + product.slug + '.png'],
}),
sources: product => product.image ? [product.image] : [],
})The mapper receives the item and index. Every option callback receives the original item, mapped card data, and index, so output names and cache sources can use either representation. formats applies to every logical card; formatAliases can derive encoding-specific destinations per item.
| Option | Purpose |
|---|---|
output | Required callback deriving each primary output path. |
aliases | Derive same-format string or named-directory aliases per item. |
formats | Publish the same logical card as additional SVG, PNG, WebP, JPEG, or AVIF encodings. |
formatAliases | Derive aliases for individual additional encodings. |
sources | Derive files or source callbacks that invalidate each mapped card. |
width / height | Apply shared dimensions to every card in the catalog. |
outputDirectory | Target a configured named output directory for the complete catalog. |
Route-oriented cards
pathnameOutput and createPathCards remove repeated slug and output mapping from route-heavy sites. URL segments are encoded into portable, deterministic filenames.
import { createPathCards, pathnameOutput } from '@santi020k/og'
pathnameOutput('/') // index.webp
pathnameOutput('/docs/api') // docs--api.webp
pathnameOutput('/guides/getting started') // guides--getting~20started.webp
const cards = createPathCards(pages, {
directory: 'routes',
extension: 'png',
})Custom renderers
Use a custom renderer for bespoke editorial art, data visualization, or strict legacy parity. The generation pipeline stays the same.
import { defineConfig } from '@santi020k/og'
import { createSharpRenderer } from '@santi020k/og/sharp'
export default defineConfig({
cards: [{ output: 'index.webp', data: { title: 'Home' } }],
renderer: createSharpRenderer({
renderSvg: ({ title }, { height, width }) =>
`<svg viewBox="0 0 ${width} ${height}">...</svg>`,
webp: { quality: 86 },
}),
})import { readFile } from 'node:fs/promises'
import { defineConfig } from '@santi020k/og'
import { createSatoriRenderer, html } from '@santi020k/og/satori'
const regular = await readFile('public/fonts/Inter-Regular.ttf')
export default defineConfig({
cards: [{ output: 'index.webp', data: { title: 'Hello' } }],
renderer: createSatoriRenderer({
satori: { fonts: [{ data: regular, name: 'Inter', weight: 400 }] },
template: ({ title }) => html`<div style="display:flex">${title}</div>`,
}),
})Existing functions that already return PNG, WebP, JPEG, or AVIF bytes can use createEncodedRenderer. Large collections can use defineWorkerRenderer or createSatoriWorkerRenderer with bounded automatic concurrency.
Use a preset config for social cards while independent video, diagram, or specialized media scripts retain their custom renderer and own only their outputs.
Outputs, aliases, and assets
Define one logical card and publish WebP, PNG, JPEG, AVIF, or SVG variants. Same-format aliases reuse bytes, format aliases add encoding-specific names, and named directories support multi-app repositories.
export default defineConfig({
outputDirectory: 'public/og',
outputDirectories: {
docs: 'apps/docs/public',
store: 'apps/store/public',
},
cards: [{
output: 'home.webp',
formats: ['png', 'svg'],
aliases: [
'og.webp',
{ directory: 'docs', output: 'social/home.webp' },
],
formatAliases: { png: ['share.png'] },
data: home,
}],
assets: [{
source: 'assets/app-icon.png',
directory: 'store',
output: 'app-icon.png',
}],
renderer,
})Each requested encoding renders once; every alias for that encoding reuses its bytes.
Deterministic preset typography
Presets bundle Inter Variable, embed it into generated SVG, measure real glyph advances, split long tokens at grapheme boundaries, and truncate safely. Text layout therefore stays portable across build machines, mixed-width scripts, code-heavy titles, and emoji.
export default definePresetConfig({
cards,
preset: {
brand: { name: 'Example' },
typography: {
file: 'public/fonts/Brand.woff2',
family: 'Brand',
},
},
})Set preset.typography.file to a local WOFF, WOFF2, TTF, or OTF font and give it a matching family. The file is automatically included in the preset cache fingerprint. Keep the bundled font by omitting typography.
Cache and cleanup guarantees
Fingerprints include card data, dimensions, destinations, config contents, declared sources, the generator version, and an optional semantic cache key. Presets also record their preset version.
Only cards affected by an input change are rebuilt.
Checks detect missing, edited, or corrupted generated bytes.
Only obsolete outputs recorded by the previous manifest are removed.
Semantic cache revisions
Set cache.key when renderer behavior changes without a directly tracked source change. The key participates in every fingerprint and appears in human-readable and JSON summaries. Preset configs supply their own preset-v1 key automatically.
export default defineConfig({
cards,
renderer,
cache: {
key: 'editorial-renderer-v2',
sources: ['public/fonts/*.woff2', 'public/logo.svg'],
},
})Output and manifest paths remain constrained to the project root. Commit the manifest with committed images; ignore both when images are CI-only artifacts. Use santi-og check for verification and santi-og compare for non-destructive visual reports.
CLI reference
| Command or option | Purpose |
|---|---|
init | Create a working preset configuration. |
generate | Render changed cards and clean tracked obsolete output when enabled. |
check | Report missing, changed, or stale cards without mutation. |
compare | Report dimensions, format, size, and pixel differences without replacing files. |
migrate --report | Inventory cards, outputs, local renderer modules, and remaining custom responsibilities. |
upgrade --to | Update package dependencies, pnpm catalogs, and release-age exclusions. |
--json | Print machine-readable generation, check, comparison, migration, or upgrade results. |
--threshold | Fail when the changed-pixel ratio exceeds the accepted value, or whenever an output is missing or changes dimensions. |
--config, -c | Use a specific config file or package directory. |
--concurrency | Set active renders to a positive number or auto. |
--force, -f | Regenerate every card regardless of its fingerprint. |
--clean | Remove obsolete outputs tracked by the manifest. |
Machine-readable results
Add --json for CI, migration tooling, or adoption reports. Generation and checks expose the exact changed sets, total logical cards, library version, semantic cache key, and elapsed time.
{
"command": "generate",
"config": "/project/og.config.mjs",
"cacheKey": "preset-v1",
"checked": false,
"generated": ["home.webp"],
"skipped": ["docs.webp"],
"cleaned": [],
"stale": [],
"total": 2,
"version": "0.4.0",
"elapsedMilliseconds": 184
}| Command | JSON fields |
|---|---|
generate / check | command, config, cacheKey, checked, generated, skipped, cleaned, stale, total, version, elapsedMilliseconds. |
compare | command, version, and comparisons with output, status, actual/expected image details, and changed-pixel counts and ratio. |
migrate --report | Config path and lines, generator version, cache key, custom-renderer status, local modules, logical and physical counts, and recommendations. |
upgrade | Target version, detected package manager, and each changed file with its previous and next values. |
Config reference
| Property | Default | Description |
|---|---|---|
cards | Required | Cards or an asynchronous card collector. |
preset | Neutral | Brand, theme, typography, variant, and Sharp options for definePresetConfig. |
renderer | Custom configs only | A function or worker descriptor used by defineConfig. |
assets | [] | Static files copied and tracked alongside cards. |
outputDirectory | public/og | Primary output directory relative to the project root. |
outputDirectories | {} | Additional named output directories. |
width / height | 1200 × 630 | Default dimensions; individual cards may override them. |
cache | Enabled | Boolean or cache options with semantic key, manifest path, and shared sources. |
clean | false | Remove only obsolete outputs previously tracked by the tool. |
concurrency | Renderer-based | Fixed, automatic, or bounded automatic active renders. |
root | Config directory | Project root used to resolve and constrain paths. |
Replace repeated renderers with portable v0.4 workflows.
The ten-project migration removed 2,273 lines of consumer Open Graph code while preserving route data and project-owned branding.
Read the migration guide ↗