Plugins
A plugin is an optional piece of code that runs during your build and does something extra: compile Tailwind CSS, highlight code blocks, optimize images, and so on. Most people only ever add official plugins by name and never write one themselves, that's covered first below. Writing your own plugin, and the trust model behind running someone else's code, are covered further down for when you need them.
Using an official plugin
Steno publishes seven official plugins under the @steno/ scope:
jsr:@steno/plugin-tailwindcompiles Tailwind CSS as part of the build.jsr:@steno/plugin-shikiadds syntax highlighting to fenced code blocks in your Markdown.jsr:@steno/plugin-seogeneratessitemap.xml, an RSS feed (feed.xml), and an Atom feed (atom.xml) from your pages.jsr:@steno/plugin-imageoptimizes and resizes images referenced by your theme.jsr:@steno/plugin-docsmirrors an external directory of Markdown files intocontentDirbefore each build, so a docs folder living outside the project (a monorepo package's owndocs/, for example) gets built as regular pages.jsr:@steno/plugin-searchgenerates a JSON search index (title, route, excerpt, headings) from your rendered HTML, for client-side search.jsr:@steno/plugin-ogauto-generates an Open Graph preview image (plain SVG, no headless browser) per page and injects the matchingog:imagetags into<head>.
The scaffolder (deno create jsr:@steno/init) offers all seven as checkboxes in its interactive
plugin picker, and by name through --plugins:
deno create jsr:@steno/init --plugins tailwind,shiki,seo,docs,search,og,image
Any of them can also be added manually to an existing project's config, each with its own options:
plugins:
- jsr:@steno/plugin-tailwind
- jsr:@steno/plugin-shiki
- package: jsr:@steno/plugin-seo
options:
siteUrl: https://example.com
title: My Site
description: A concise description
authorName: Ada Lovelace
See each plugin's own JSR page for its full option list. The scaffolder pins an exact version and
runs every official plugin it offers in-process (mode: trusted); that's a deliberate choice, since
they're maintained alongside Steno itself and reviewed the same way. If you add any of them
manually, pin an exact version too, rather than a version range. See
Trust and permissions below if you would rather run one in the stricter
sandboxed mode instead; Isolated plugin entries lists
the exact permission fields to grant (each plugin's own docs list what filesystem and network access
it actually needs).
Writing your own plugin
A plugin is a small package whose module default-exports a factory function. That factory returns an object describing which parts of the build it wants to hook into:
import type { StenoPlugin } from "jsr:@steno/steno";
export default function createPlugin(options: Record<string, unknown>): StenoPlugin {
return {
name: "example",
transformHtml: (html) => html.replaceAll("TODO", String(options.label ?? "Done")),
};
}
Add it to your config the same way as an official plugin, with an optional options object that
gets passed straight to your factory:
plugins:
- jsr:@example/links
- package: npm:@example/minify
mode: isolated
options: { enabled: true }
The hooks available are beforeBuild(config), transformAst(tokens), transformHtml(html),
afterPage({ path, html }), and afterBuild(config). Plugins run in the order they're declared.
transformAst and transformHtml apply to page bodies and collection content alike. A theme's own
bundled plugins run before your site's configured ones, and can be turned off entirely with
allowThemePlugins: false.
One important detail: build lifecycle hooks see Steno's temporary staging output, not the final
dist/ folder, config.output in a hook points at that staging directory. afterPage gets both
the staging path (path) and the file's eventual real location (finalPath). A plugin should never
write directly to the final output path itself; see Transactional builds for
why.
Trust and permissions
A plugin runs arbitrary code during your build, so it's worth understanding what it can do before
you add one, especially one you didn't write yourself. Every plugin declares a mode:
isolated: the plugin runs in its own separate process, with every capability (filesystem, network, environment variables, subprocesses, FFI) denied by default. You explicitly grant only what it actually needs underpermissions. This is the recommended mode for any plugin you did not write yourself. See the plugin sandbox for the full permission model.trusted(the default, kept for backward compatibility): the plugin runs inside Steno's own process and inherits every permission Steno has: filesystem, network, environment, subprocess, FFI, all of it. A trusted plugin can read or change any project file, or the generated output, without restriction. Theme-bundled plugins run at this level too, unless you turn them off withallowThemePlugins: false.
In steno dev, trusted plugin factories run again when config.plugins, the source policy, or a
trusted local file:// entry point's contents change. Local entry point directories are watched,
including those outside content/. Editing content doesn't re-trigger factories. Changes to local
entry points invalidate rendered pages even when hook functions themselves haven't changed.
Imported dependencies remain cached by Deno; restart dev after editing a plugin's dependencies or
adding a plugin directory outside the paths already watched. Isolated plugins don't
get this: their worker is intentionally torn down at the end of every build (part of the sandbox's
threat model, see plugin sandbox), so an isolated entry always reloads on the
next rebuild regardless. steno build is unaffected either way - it loads plugins exactly once per
process.
pluginSourcePolicy controls which kinds of module specifiers are allowed at all (jsr: and npm:
by default), it is a source filter, not a sandbox, and it applies the same way to both modes. It
does not look inside a plugin's own dependencies, and can't stop an allowed plugin from importing
something else internally. See Configuration to change
it.
Before adding any plugin or theme, trusted or isolated:
- Review who publishes it and where its source lives.
- Pin an exact version instead of following a mutable tag or URL.
- Review what changed before accepting an update.
- Grant Steno itself, and any isolated plugin, only the permissions your project actually needs, nothing more.
Do not add a plugin you don't trust unless its entry explicitly sets mode: isolated.