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

@zakkster/lite-og

v1.0.0

Published

Server-side Open Graph image generation via Canvas2D (@napi-rs/canvas). A declarative scene-graph renders to a PNG/JPEG/WebP Buffer: text with word-wrap, gradients, fitted images, hi-DPI scaling. Framework-agnostic replacement for @vercel/og. Zero runtime

Readme

@zakkster/lite-og

npm version sponsor npm bundle size npm downloads npm total downloads types runtime deps

Server-side Open Graph image generation from a declarative scene graph. A plain JavaScript object describes the card; renderOG rasterizes it to a PNG (or JPEG/WebP) Buffer you can write to an HTTP response or to disk.

It is a framework-agnostic alternative to @vercel/og. Where Vercel pairs React with Satori's WASM text-layout engine (heavy, React-only), lite-og uses Canvas2D via @napi-rs/canvas (native, prebuilt, no system Cairo) and a small scene-graph API with explicit coordinates and greedy text wrapping. No JSX, no WASM, no layout engine, zero runtime dependencies of its own.

import { renderOG, loadFont } from "@zakkster/lite-og";

await loadFont("Inter Bold", "./fonts/Inter-Bold.ttf");

const png = await renderOG({
    width: 1200,
    height: 630,
    background: {
        type: "linear-gradient",
        stops: [{ offset: 0, color: "#5b34f8" }, { offset: 1, color: "#1abc9c" }],
    },
    children: [
        { type: "image", src: "./logo.png", x: 60, y: 60, width: 96, height: 96 },
        { type: "text", text: "Ship social cards without the WASM tax", x: 60, y: 220, font: '80px "Inter Bold"', color: "#ffffff", maxWidth: 1080, lineHeight: 96 },
        { type: "rect", x: 60, y: 540, width: 600, height: 6, fill: "#ffffff", opacity: 0.5, radius: 3 },
    ],
});
// png is a Buffer -> res.end(png) or fs.writeFile("og.png", png)

Install

@napi-rs/canvas is a peer dependency that lite-og imports lazily. This keeps lite-og's own dependency tree empty (so importing the types costs nothing), and a missing backend produces a clear OGError('missing_canvas') instead of an import crash. Install both:

npm install @zakkster/lite-og @napi-rs/canvas

Node 18+. v1 targets the Node runtime (see Scope for edge runtimes).

How it renders

flowchart LR
  S[Scene object] --> V{validate}
  V -- invalid --> E[reject OGError]
  V -- ok --> C["createCanvas(w*scale, h*scale)"]
  C --> B[paint background]
  B --> N[draw children in order]
  N --> EN[encode png / jpeg / webp]
  EN --> O[Buffer]

A single pass: validate the scene, create a canvas (scaled for hi-DPI), paint the background, draw each child in array order onto the 2D context, then encode. There is no layout step. Every node carries its own absolute coordinates.

Scene

| Field | Type | Notes | | --- | --- | --- | | width, height | number | Logical pixels (required, positive). | | background | string \| Background | Solid color, gradient, or fitted image. Omit for transparent. | | children | Node[] | Drawn over the background, in order. | | format | 'png' \| 'jpeg' \| 'webp' | Default 'png'. | | quality | number | 0..1 for jpeg/webp. | | scale | number | Hi-DPI multiplier. Output is width*scale by height*scale; coordinates stay logical. Default 1. |

Backgrounds

background: "#0b0e14"                                   // solid color
background: { type: "linear-gradient", angle: 135, stops: [...] }
background: { type: "radial-gradient", stops: [...] }   // brightest at center
background: { type: "image", src, fit: "cover" }        // fit: cover | contain | fill

Gradient angle is in degrees, clockwise, where 0 = left-to-right and 90 = top-to-bottom (default 0). Stops are { offset: 0..1, color }.

Nodes

flowchart TD
  Scene --> Background
  Scene --> Children
  Background --> bc[color string]
  Background --> lg[linear-gradient]
  Background --> rg[radial-gradient]
  Background --> bi[image]
  Children --> t[text]
  Children --> r[rect]
  Children --> i[image]
  Children --> l[line]
  Children --> g[group]
  g -. translate + opacity .-> Children

text -- { type, text, x, y } plus: font (CSS shorthand, e.g. '80px "Inter Bold"') or fontSize/fontFamily/fontWeight; color; maxWidth (enables wrapping); lineHeight (px, default round(fontSize * 1.25)); align (left/center/right); letterSpacing; opacity; shadow ({ color, blur, x, y }). y is the top of the first line (baseline is top).

rect -- { type, x, y, width, height } plus: fill, radius (rounded corners), stroke, strokeWidth, opacity.

image -- { type, src, x, y } plus: width/height (default natural size); fit (cover/contain/fill, default stretches); radius (rounded clip); opacity; cache (default true). src may be a file path, an http(s)/file:/data: URL string, a URL, or raw bytes (Buffer/Uint8Array/ArrayBuffer).

line -- { type, x1, y1, x2, y2 } plus: stroke, strokeWidth, cap (butt/round/square), opacity.

group -- { type, children } plus optional x/y (translate) and opacity. A group only translates its children and multiplies their opacity -- it is for moving a cluster of nodes together, not auto-layout.

Text wrapping

With maxWidth, text is wrapped greedily using ctx.measureText: words are packed onto a line until the next word would overflow. Explicit \n always breaks. This is fast and dependency-free for the handful of words in a typical OG title. Limitation: a single word longer than maxWidth is not character-broken in v1; it stays on one line and overflows. Pre-break very long unbroken strings yourself if needed.

Fonts

await loadFont("Inter Bold", "./fonts/Inter-Bold.ttf");   // file path
await loadFont("Inter Bold", new URL("./Inter-Bold.ttf", import.meta.url)); // file: URL
await loadFont("Inter Bold", fontBuffer);                  // Buffer / Uint8Array / ArrayBuffer
const families = await listFonts();                        // registered + system families

Font registration is process-global (the backend registers fonts globally). Register once at startup. A missing file or unreadable font rejects with OGError('font_load_failed'). Reference the family in a text node via the font shorthand or fontFamily.

Images and the cache

Decoded images are cached so a long-lived server doesn't re-decode (and, for remote sources, re-fetch) the same brand logo or template on every request. The cache is a bounded LRU:

  • Default capacity 100; at capacity it evicts the least-recently-used entry (a cache hit marks an entry most-recent). This is the key protection against OOM: a dynamic endpoint that renders per-request avatars can never grow the cache without bound.
  • Cacheable sources are keyed by src (path / URL / data URI). Raw byte sources are never cached -- the caller already owns those bytes.
  • Per-node cache: false opts a single image out.
setImageCacheLimit(256);   // tune capacity (0 disables caching entirely)
imageCacheSize();          // current entry count, for monitoring
clearImageCache();         // empty it

Output and hi-DPI

format selects png (default), jpeg, or webp; quality (0..1) applies to jpeg/webp. For retina, set scale: 2 and keep your coordinates at the logical size:

const buf = await renderOG({ width: 1200, height: 630, scale: 2, /* ... */ });
// encodes a 2400x1260 image; you still position elements in 1200x630 space

Errors

renderOG and loadFont reject with a typed OGError (it has a .code). Image and font failures fail loud by design: a corrupted OG image is worse than an error, because caching layers (Slack, X, etc.) may pin the broken result. Catch the rejection on your server and serve a static fallback or a 404.

| code | When | | --- | --- | | missing_canvas | The @napi-rs/canvas peer dependency is not installed. | | invalid_scene | Bad dimensions, options, background, or a node missing required fields. | | font_load_failed | loadFont could not read or parse the font. | | image_load_failed | An image source could not be loaded or decoded. | | unsupported_node | A child has an unknown type. |

Performance

bench.mjs measures end-to-end renderOG throughput for a 1200x630 card across formats and scale. Numbers are hardware-dependent (native rasterization dominates), so run it on your own machine:

npm run bench

Server example

import { createServer } from "node:http";
import { renderOG, OGError } from "@zakkster/lite-og";

createServer(async (req, res) => {
    try {
        const png = await renderOG({ width: 1200, height: 630, background: "#0b0e14", children: [/* ... */] });
        res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=86400" });
        res.end(png);
    } catch (e) {
        const code = e instanceof OGError ? e.code : "error";
        res.writeHead(500, { "content-type": "text/plain" });
        res.end(code);
    }
}).listen(3000);

Scope and limitations

v1 is deliberately small and explicit:

  • No flexbox / auto-layout. Coordinates are absolute; group only translates. This is the intentional simplification versus Satori -- OG cards are rigid, fixed-size designs.
  • Node runtime only. Cloudflare Workers / Vercel Edge do not provide @napi-rs/canvas; an OffscreenCanvas adapter is a candidate for a later version.
  • No arbitrary SVG paths, no per-span rich text within a single text node, and emoji depend on a system emoji font being available.
  • Overlong unbroken words are not character-wrapped (see Text wrapping).

License

MIT (c) Zahary Shinikchiev