ogier
v1.8.1
Published
Easily generate Open Graph images from text, icons and fonts, with ready-made layouts and themes, and adapters for your site framework.
Maintainers
Readme
Ogier
Easily generate Open Graph images from text, icons and fonts, with ready-made layouts and themes, and adapters for your site framework.
Ogier turns a title, a description and an icon into the image a page shows when it is shared: a 1200 by 630 PNG in your fonts and colors. Icons come from any SVG file, your own or one from an icon package like Tabler or Lucide. The VitePress and Next adapters render a card for every page and write the tags that point at it.
Installation
npm install ogierQuick Start
import { writeFile } from 'node:fs/promises'
import { renderPng } from 'ogier'
const png = await renderPng({
header: { text: 'feedsmith', icon: { file: './public/favicon.svg' } },
eyebrow: 'Guides › Parsing',
title: 'Parsing namespaces',
footer: { text: 'macieklamberski/feedsmith', icon: { file: '@tabler/icons/outline/brand-github.svg' } },
})
await writeFile('og/guides-parsing.png', png)Examples
Each card below is rendered from a preset in examples. Click a card to see its source.
| | |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
API
renderPng(card, style?) and renderSvg(card, style?)
Render one card. Both take the same arguments and give back a PNG buffer or an SVG string.
The card is what the image says:
type Card = {
header?: { text?: string; icon?: IconRef; aside?: string }
icon?: IconRef
eyebrow?: string
title?: string
byline?: string
description?: string
footer?: { text?: string; icon?: IconRef; aside?: string }
image?: ImageRef & { position?: 'left' | 'right' }
align?: { horizontal?: 'left' | 'center' | 'right'; vertical?: 'top' | 'center' | 'bottom' }
extra?: Extra
}titleanddescription: at least one of them. A description alone takes the title's place, which is what a home page wants.headerandfooter: a line with a text, an icon, or both.asidesits at the far end of the line, for a date or a version. An icon alone makes a wordmark line.icon: an icon above the eyebrow and the title, for a logo or a favicon. It isiconSizetall, 88 by default, withiconRadiuscorners, 0 by default.eyebrow: the short line above the title, for a section name or a category.byline: the line under the title, for an author or a source.image: a picture over one half of the card, the right half unlesspositionis'left'. The headline takes the other half, and the header and footer lines keep to the card edges, drawn over the picture.align:horizontalmoves the eyebrow, title, byline and description left, center or right.verticalputs them at the top under the header, in the middle, or at the bottom above the footer.extra: any element satori can draw, such as JSX or a{ type, props }object, placed under the description, or under the title when there is none. It takes the alignment and width of the text above it, for a row of logos or a short list.
An icon or an image is { file } with a path, a file URL or a package path such as '@tabler/icons/outline/brand-github.svg', { svg } with the markup, or { data, type } with the bytes of a PNG or JPEG you already fetched and its media type, 'image/png' or 'image/jpeg'. Package paths resolve from your node_modules, so any icon package that ships SVG files works once installed. An image can also be a PNG or JPG. WebP shows up only in renderSvg: the PNG renderer cannot draw it, so renderPng throws. currentColor in an icon takes the color of the text beside it.
An icon can also be { element } with any element satori can draw, such as a logo component from your app: { element: <Logo /> }. It sits in a box as tall as the icon, and its currentColor takes the same color as markup does. An element that sets no height gets the icon's, a component's SVG included, and an SVG takes its width from its viewBox.
The style is how the card looks. Every field is optional:
type Style = {
theme?: Theme
sizes?: Partial<Sizes>
fonts?: Partial<Record<FontRole | FontElement, string | Array<FontFile>>>
background?: { pattern: Pattern; angle?: number; fade?: boolean } | { image: ImageRef }
}theme: the colors.darkandlightcome fromogier/themes. Spread one and change a value, or give a text its own color withtitle,description,asideand the other text names.sizes: the numbers. Card size and padding, the type scale, the accent bar along the bottom edge. Every key has a default, so set only what you change. Each text has its own weight, such aseyebrowWeightordescriptionWeight. The examples show the ones that matter in practice, and theSizestype lists them all.fonts: a fontsource family per role.titlefor the title,bodyfor the description,labelfor the rest. A text can take its own family under its name,header,aside,eyebrow,byline,descriptionorfooter, and the others keep their role's. Inter and JetBrains Mono ship with ogier. Any other family needs its@fontsource/<slug>package installed in your project. A weight the family does not ship is drawn in the nearest one it does. A role can take font files instead, as{ data, weight, style? }with the bytes you loaded yourself. Ogier uses them as they are, and a weight you leave out is drawn in the nearest one given.background:{ pattern }for a rail on the right that fades in from the middle of the card. The pattern isdots,stripes,grid,crosses,wavesorchecks, drawn in the theme'spatterncolor, with an optionalanglein degrees, 45 for stripes and 0 for the rest by default, andfade: falsefor a flat rail. Or{ image }for a picture behind the whole card.
import { renderPng } from 'ogier'
import { light } from 'ogier/themes'
await renderPng(card, {
theme: { ...light, accent: '#a2e57b' },
sizes: { titleText: 80 },
fonts: { title: 'roboto' },
background: { image: { file: './og-bg.png' } },
})createRenderer(style?)
Render many cards with one style. The fonts are read from disk once, on the first render, so a server or a build does not read them again for every card.
import { createRenderer } from 'ogier'
const renderer = createRenderer({ theme: light, background: { pattern: 'dots' } })
for (const page of pages) {
await writeFile(`og/${page.slug}.png`, await renderer.renderPng(page.card))
}The renderer has renderElement, renderSvg and renderPng, and they all share the fonts. Its size holds the card's width and height before any render, for the og:image tags of a page whose image is drawn later.
renderElement(card, style?)
The card as a satori element tree, with the fonts and the size it needs, for drawing it with another satori renderer. Next's ImageResponse takes all of it as it is:
import { ImageResponse } from 'next/og'
import { renderElement } from 'ogier'
const { element, fonts, width, height } = await renderElement(card, style)
return new ImageResponse(element, { width, height, fonts })renderElement from ogier needs the Node.js runtime, because it reads fonts and images with Node built-ins. A bundler that follows its imports also reaches resvg's native binary, which Turbopack refuses to bundle. Import from ogier/lite for those.
ogier/lite
The same renderElement and createRenderer, plus the metadata helpers and the types, with nothing that needs Node.js or a native module. It runs on Cloudflare Workers and the Next edge runtime, and bundles with Turbopack without any config.
import { ImageResponse } from 'next/og'
import { renderElement } from 'ogier/lite'
const { element, fonts, width, height } = await renderElement(card, {
fonts: {
title: [{ data: interBold, weight: 700 }],
body: [{ data: interRegular, weight: 400 }],
label: [{ data: monoMedium, weight: 500 }],
},
})
return new ImageResponse(element, { width, height, fonts })It reads nothing from disk, so every role takes font files, and icons and images come as svg or data, or icons as element. A fontsource family or a file ref throws an error that points back to ogier. renderSvg and renderPng are only in ogier.
composeMetadata(input) and composeMetaTags(metadata)
The tags that point at the image. composeMetadata builds the Open Graph values for one page, and composeMetaTags turns them into ['meta', attributes] pairs with the Twitter tags filled in from the same values.
import { getImageUrl, composeMetadata, composeMetaTags } from 'ogier'
const metadata = composeMetadata({
url: 'https://feedsmith.dev/guides/parsing',
title: 'Parsing namespaces',
description: 'How the parser reads namespaces.',
image: getImageUrl('https://feedsmith.dev', 'guides/parsing'),
})
const tags = composeMetaTags(metadata)getImageUrl(hostname, path, dir?) names the image for a page: the path with slashes turned into dashes, as a PNG under og/.
Adapters
VitePress
// docs/.vitepress/config.ts
import { dark } from 'ogier/themes'
import { vitepress } from 'ogier/adapters'
import { defineConfig } from 'vitepress'
const og = vitepress({
site: { hostname: 'https://feedsmith.dev', favicon: { file: 'docs/public/favicon.svg' } },
card: {
header: { text: 'feedsmith', icon: { file: 'docs/public/favicon.svg' } },
footer: { text: 'macieklamberski/feedsmith', icon: { file: '@tabler/icons/outline/brand-github.svg' } },
},
style: {
theme: { ...dark, accent: '#ff8c4d' },
background: { pattern: 'dots' },
},
})
export default defineConfig({
transformHead: og.transformHead,
buildEnd: og.buildEnd,
})Each page gets the tags from transformHead and a PNG under og/ from buildEnd. The eyebrow is the sidebar trail to the page, the title is the page title, and the home page shows the site description.
| Option | What it holds |
|---|---|
| site | hostname, plus imageDir for the folder under the output dir and in the image URL, default og, and imageUrl, a function from the page path to the image URL, default getImageUrl. favicon takes an SVG as { file } or { svg } and writes it as favicon.png at the output root, 192 px square unless size says otherwise, with an icon link on every page. Search engines take PNG favicons and not SVG ones. |
| card | The fields every card shares, merged over the page defaults. A function of the page data and site data gives per-page values. |
| style | The style passed to every render. |
Next
// app/blog/[slug]/opengraph-image.tsx
import { next } from 'ogier/adapters'
import { getPost } from '../../../lib/posts'
const image = next({
style: { background: { pattern: 'dots' } },
card: async ({ slug }) => {
const post = await getPost(String(slug))
return { header: { text: 'feedstand', aside: post.date }, title: post.title }
},
})
export default image.default
export const { size, contentType } = imageThe file's default export takes the route params, resolves the card and responds with the PNG. size and contentType feed the og:image width, height and type. A twitter-image.tsx beside it re-exports the same three names. Every card in the file renders through one renderer, so the fonts are read once.
headers sets response headers, such as a cache policy for the CDN. The Content-Type stays image/png whatever the headers say.
const image = next({ style, card, headers: { 'Cache-Control': 'public, max-age=86400' } })The older next(style, card) form still works and is deprecated.
With output: 'standalone', Next does not copy the @fontsource files into the build. Pass the fonts as files, or add ./node_modules/@fontsource/**/*.woff to outputFileTracingIncludes.
