npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

Readme

Ogier

codecov npm version license

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 ogier

Quick 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.

| | | |---|---| | Docs page on a dark theme with the dot rail | Book club card in Young Serif and IBM Plex Mono on red with a painting on the right | | Essay in DM Serif Display and Inter with a still life on the left and right-aligned text | Blog post in Literata and DM Sans with the headline at the bottom and an accent bar | | Wordmark card in JetBrains Mono | Research post in Epunda Slab with yellow text over a full-bleed aerial photo of a river | | Changelog card in Space Mono and Space Grotesk on blue with diagonal waves | Personal card with an icon and a name and role in Commit Mono | | Landing page in Bricolage Grotesque with centered text | Research post in Geist and Geist Mono on black with a paint swirl on the right | | Repository card in Geist on white with a stats row and an accent bar | Package card in JetBrains Mono on black with a stats grid and a download sparkline | | Release card in Bricolage Grotesque with a huge version number on a grid | Quote in Instrument Serif on cream with a quote icon and a crosses pattern | | Podcast episode in Fraunces on purple with overlapping host avatars | Conference talk in Space Grotesk on light blue with a date and venue row and the checks pattern | | Job post in Bricolage Grotesque on lime with tag pills | Incident on a status page in Geist on dark with a 90-day uptime bar | | Blog index in Literata with a table of posts | Marketing claim in Bricolage Grotesque on orange with flat stripes |

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
}
  • title and description: at least one of them. A description alone takes the title's place, which is what a home page wants.
  • header and footer: a line with a text, an icon, or both. aside sits 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 is iconSize tall, 88 by default, with iconRadius corners, 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 unless position is 'left'. The headline takes the other half, and the header and footer lines keep to the card edges, drawn over the picture.
  • align: horizontal moves the eyebrow, title, byline and description left, center or right. vertical puts 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. dark and light come from ogier/themes. Spread one and change a value, or give a text its own color with title, description, aside and 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 as eyebrowWeight or descriptionWeight. The examples show the ones that matter in practice, and the Sizes type lists them all.
  • fonts: a fontsource family per role. title for the title, body for the description, label for the rest. A text can take its own family under its name, header, aside, eyebrow, byline, description or footer, 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 is dots, stripes, grid, crosses, waves or checks, drawn in the theme's pattern color, with an optional angle in degrees, 45 for stripes and 0 for the rest by default, and fade: false for 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 } = image

The 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.