Skip to content

Programmatic use

The CLI is a thin wrapper over two entry points. Use them directly when a project needs to drive generation itself.

The core takes props and config and returns rendered bytes. It touches no files and knows nothing about content trees:

import { renderMetaImages } from "@kensio/colophon";
import { writeFile } from "node:fs/promises";
const images = await renderMetaImages(
{
template: "banner",
title: "@kensio/colophon",
subtitle: "Generate social meta images from frontmatter",
version: "1.2.0",
},
{
colors: { brand: "#2563eb" },
footer: "example.com",
badge: { text: "npm" },
},
);
for (const image of images) {
// image.name is the output-size name ("og", "square", and so on).
await writeFile(`social-${image.name}.png`, image.bytes);
}

One image is returned per configured output size. Each carries its name, dimensions, the source svg and the encoded bytes, in whatever format the config asked for. extensionFor is what a build names its own files with, if you would rather not hardcode .png.

This is the right entry point for rendering an image from data that is not a markdown file at all, such as a database row or an API response.

generate ties walking, rendering and writing together. This is what the CLI calls:

import { generate } from "@kensio/colophon";
await generate({
contentDir: "content",
config: { colors: { brand: "#2563eb" } },
overwrite: false,
dryRun: false, // work out what would change and write nothing
concurrency: 4, // defaults to one per available CPU
onResult: (result) =>
// result.url is where it is served, when the placement knows.
console.log(`${result.skipped ? "skip" : "wrote"} ${result.outputPath}`),
});

onResult is called once per image, including the ones that were skipped because their stamp still matched. A result for an extra image has contentPath set to undefined, since there is no post behind it.

Under dryRun the results say what a real build would have done: skipped is true for an image whose stamp still matches, and false for one that would be rendered. Nothing is written, not even the manifest, and every check a build makes still runs. It is what the CLI’s --dry-run is.

Two generate options deliberately live outside ColophonConfig:

  • concurrency is a property of the machine doing the build rather than of the images. Putting it in config would drag it into the rebuild stamp, so changing it would re-render the tree. What a build reaches is capped by the libuv thread pool, which holds four threads unless UV_THREADPOOL_SIZE was set in the environment the process started in. generate warns once where the concurrency is above it. The command line has the measurements.
  • outputPath is a callback that decides where each image is written. It takes precedence over placement, and when it is used there is no URL, because the placement no longer describes where the file went.

generate’s walk option is the programmatic equivalent of config.content and wins where both are given.

walkContent finds content files and reads their frontmatter, without rendering anything:

import { walkContent } from "@kensio/colophon/content";
const files = await walkContent({ dir: "content" });

Import it from the @kensio/colophon/content subpath when frontmatter discovery is all you want. The root entry point pulls in the rasteriser and the syntax highlighter, and this subpath does not.

readContentFile is the same work for one file, which is what colophon preview uses. It takes the file, the content root its path and slug are relative to, and the same options, and returns undefined where the file asks for no image:

import { readContentFile } from "@kensio/colophon/content";
const file = await readContentFile("content/posts/hello.md", "content");

The pieces the above are built from are exported too, for anything that needs to work at a lower level: buildSvg and renderSvgToImage for the two halves of rendering, resolveConfig and resolveConfigForSize for config, the layout toolkit and createMeasurer for template authors, and createStamper, readImageStamp and stampImage for the rebuild stamps.

metaTags has its own @kensio/colophon/meta subpath, which loads neither the rasteriser nor the highlighter. So does the layout toolkit, as @kensio/colophon/layout, which loads nothing from Node at all.