SoneSone
Output formats

Render config

Every option you can pass to sone(node, config) — size, background, margins, paginate, cache.

sone(node, config?) accepts an optional SoneRenderConfig as its second argument. This page is the comprehensive reference.

sone(root, {
  width: 1200,
  height: 630,
  background: "white",
  pageHeight: 1056,
  margin: { top: 48, right: 48, bottom: 48, left: 48 },
  header: ({ pageNumber, totalPages }) => Row(...),
  footer: ({ pageNumber }) => Row(...),
  lastPageHeight: "content",
  cache: imageCache,
}).pdf();

Sizing

OptionTypeDescription
widthnumberCanvas width in pixels. When set, margins inset content within it. Without width, Sone auto-sizes to content width.
heightnumberCanvas height in pixels. Without height, Sone auto-sizes to content height.
backgroundstringCanvas background fill. Accepts any color or gradient string.
// OG image — fixed canvas
await sone(doc, { width: 1200, height: 630, background: "white" }).png();

// Auto-sized — useful for hero cards where height varies
await sone(doc, { width: 800 }).png();

Pagination

OptionTypeDescription
pageHeightnumberEnables multi-page output. Each page is this many pixels tall.
headerSoneNode | (info) => SoneNodeRepeating header on every page.
footerSoneNode | (info) => SoneNodeRepeating footer on every page.
marginnumber | { top, right, bottom, left }Page margins.
lastPageHeight"uniform" | "content""content" trims the last page to its actual content. Default "uniform".

info passed to header/footer functions is { pageNumber: number, totalPages: number }.

await sone(content, {
  width: 816,
  pageHeight: 1056,                    // US Letter at 96 dpi
  margin: { top: 64, right: 48, bottom: 64, left: 48 },
  header: Row(Text("Annual Report").size(10)).padding(8, 16),
  footer: ({ pageNumber, totalPages }) =>
    Row(Text(`${pageNumber} / ${totalPages}`).size(10).align("right"))
      .padding(8, 16),
  lastPageHeight: "content",
}).pdf();

See Multi-page documents for the full guide.

Image caching

OptionTypeDescription
cacheMapImage decode cache shared across renders.

For high-throughput pipelines (per-request OG images, batch PDF generation), pass a long-lived Map so decoded images are reused:

const imageCache = new Map();

// First call decodes /logo.png
await sone(card1, { cache: imageCache }).png();

// Subsequent calls reuse the decoded image
await sone(card2, { cache: imageCache }).png();
await sone(card3, { cache: imageCache }).png();

The cache key is the image source (path / URL / Uint8Array reference).

Output density

SoneRenderConfig doesn't expose density directly, but you can drop down to the underlying skia-canvas Canvas to control it:

const canvas = await sone(doc, { width: 1200, height: 630 }).canvas();
canvas.density = 2;  // 2× pixel density
const buffer = await canvas.toBuffer("jpeg", { quality: 0.92, density: 2 });

The CSS dimensions stay 1200×630 but the output buffer is 2400×1260, so it looks crisp on retina displays.

Type signature

interface SoneRenderConfig {
  width?: number;
  height?: number;
  background?: string;
  pageHeight?: number;
  header?: SoneNode | ((info: SonePageInfo) => SoneNode);
  footer?: SoneNode | ((info: SonePageInfo) => SoneNode);
  margin?: number | { top: number; right: number; bottom: number; left: number };
  lastPageHeight?: "uniform" | "content";
  cache?: Map<unknown, unknown>;
}

interface SonePageInfo {
  pageNumber: number;
  totalPages: number;
}

Common page sizes

FormatWidth × Height @ 96 dpi
US Letter (portrait)816 × 1056
US Letter (landscape)1056 × 816
A4 (portrait)794 × 1123
A4 (landscape)1123 × 794
A5 (portrait)559 × 794
OG image1200 × 630
Twitter card1200 × 600