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, hreflang, 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 inspect http://localhost:4321 --open
pnpm exec santi-og audit --site dist --site-url https://example.com --sarif
pnpm exec santi-og audit --site dist --site-url https://example.com --standards
pnpm exec santi-og migrate --report --json
pnpm exec santi-og upgrade --to 1.0.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. Pinned remote images use verified content-addressed caching, while the typed decoration slot adds a trusted product visual without replacing the preset shell.
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, localized alternates, 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. Locale-matrix helpers reuse the same hreflang expectations in final-output audits.
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. |
One site contract, extensible schemas, and final-output auditing
defineSite binds metadata defaults once and derives pages, cards, HTML, Astro head output, and Next.js metadata. Route manifests connect generated cards to built routes, including the JSON-LD @type values each route requires.
import MetadataHead from '@santi020k/og/astro/head'
import { articleSchema, defineSchemaRecipe, serializeJsonLd } from '@santi020k/og/schema'
import { defineSite } from '@santi020k/og/site'
export const site = defineSite({
siteUrl: 'https://example.com',
siteName: 'Example',
publicImagePath: '/og',
defaults: { image: { width: 1200, height: 630 } },
})
export const page = site.page({
pathname: '/guides/useful',
title: 'A useful guide',
description: 'One definition for metadata, card, and audit.',
image: { output: 'guides/useful.webp', alt: 'Useful guide card' },
schemaTypes: ['TechArticle'],
})
const jsonLd = serializeJsonLd(articleSchema({
type: 'TechArticle',
name: page.title,
url: 'https://example.com/guides/useful',
}))
// Astro: <MetadataHead page={page} site={site.options} />
// Starlight: import Head from '@santi020k/og/astro/starlight'
// CI: santi-og audit --site dist --site-url https://example.com --standardsBuilt-in JSON-LD recipes cover websites, web pages, articles, software, events, FAQs, offers, images, breadcrumbs, collections, people, and organizations. Use extendSchema or defineSchemaRecipe for additional Schema.org vocabulary and domain-specific scenarios.
santi-og audit checks final HTML, canonicals, Open Graph and X metadata, h1 and language contracts, image bytes, dimensions and MIME declarations, duplicates, manifest coverage, and route-specific schemas. Add --standards for sitemap, robots, hreflang, and redirects; add --llms for AI-readable Markdown coverage. Reusable audit configs align local and CI policy, and human reports group repeated root causes.
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. |
resolveCover | false | Resolve content-relative local cover paths and track the selected asset. |
archives | [] | Declarative paginateArchive and groupArchive recipes. |
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. |
route | Content path | Map an entry to its published route when URLs differ from content folders. |
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. createImageResponse wraps the same renderer for prerendered or on-demand Fetch-compatible routes.
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-v2 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. |
audit | Validate final built HTML, social images, route manifests, and required schema types. |
inspect <url> | Analyze deployed or localhost metadata and social images; add --open for a local browser report. |
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, inspection, 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-v2",
"checked": false,
"generated": ["home.webp"],
"skipped": ["docs.webp"],
"cleaned": [],
"stale": [],
"total": 2,
"version": "1.0.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. |
inspect | Resolved URL, metadata, social-image response and dimensions, redirects, individual checks, 0–100 score, summary, and elapsed time. Passes earn full credit, warnings half credit, and errors no credit. |
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. |
routeManifest | false | Publish and check a deterministic route-to-image and schema contract. |
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 and validators with portable v0.9 workflows.
The multi-project migration keeps route data and project-specific contracts local while moving rendering, metadata, schema recipes, archives, and final-output auditing into the library.
Read the migration guide ↗