Configuration reference
Steno reads content/.steno/config.yml by default; pass another path with --config if you'd
rather use a different one. The format is picked from the file extension: .yml/.yaml for YAML,
.toml for TOML - so --config content/.steno/config.toml reads the same fields, just written as
TOML instead.
Every field below is validated against its documented type as soon as the file loads - a wrong type
(a string devPort, a collections entry that isn't an object, ...) fails immediately with a
precise [config-invalid] message naming the exact field, in every command including dev. An
unrecognized top-level key is a warning, not an error - check for a typo, or nest project-specific
fields under custom.
title, description, and author can all be omitted. description/author default to "";
title is derived from content/index.md the same way a page's own title infers when its
frontmatter doesn't set one - an explicit title: in its frontmatter, falling back to its first #
heading, falling back to a humanized contentDir name if there's no index page at all. Set title
explicitly in config.yml to skip that lookup.
title: My site
description: A concise description
author: Ada Lovelace
contentDir: content # default: content
output: dist # default: dist
publicDir: public # default: public; set to false to disable
head:
- name: robots
content: index,follow
- property: og:type
content: website
- tag: link
rel: canonical
href: https://example.com/
- tag: script
src: /assets/app.js
defer: true
theme: ./theme
themeConfig:
accent: purple
globals:
repository: https://example.com/source
shortUrls: true
devPort: 5735
custom:
stylesheets:
- /assets/site.css
collections:
posts:
sortBy: date
order: desc
limit: 10
filter: { draft: false }
schema:
title: { type: string }
date: { type: string }
tags: { type: array, required: false }
redirects:
/old-url: /new-url
Redirects
Each redirects entry writes a static meta-refresh HTML page rather than a server-level redirect.
from must start with /, and to cannot be empty; an entry that violates either is reported as a
[redirect-invalid] diagnostic (see
How Steno reports problems - this fails
steno build, steno dev prints it but keeps running). shortUrls controls the emitted path: with
shortUrls: true, /old-url becomes <output>/old-url/index.html; otherwise it becomes
<output>/old-url.html. Redirects participate in the same output-collision detection as pages and
theme assets, so a redirect that would overwrite an existing page or asset fails the build with an
Output collision error.
title, description, and author are the site fields exposed as site in templates.
contentDir and output are relative to the working directory unless absolute. publicDir is
relative to contentDir; files under it are copied verbatim to the output root (see
Public assets). navigation optionally supplies a tree of
{ title, url, children } nodes for themes.
collections groups pages by their content subdirectory (content/posts/* into
collections.posts) with optional sorting, filtering, pagination, and frontmatter schema
validation; see Collections for every field sortBy, order, limit,
filter, and schema accept.
Managed head tags
head entries are injected into the rendered document independently of the active theme. A meta
entry may use name, property (including Open Graph), httpEquiv, or charset. Link entries
require tag: link, rel, and href. Script entries use tag: script with src or inline
content, and support type, async, defer, noModule, integrity, crossOrigin, and
referrerPolicy.
Pages can add or replace entries through steno.head frontmatter. A page entry replaces a site
entry when they share the same identity, and is otherwise appended in declaration order. Identity
is, in priority order: an explicit key if either entry sets one; for a meta tag, its name,
property, or httpEquiv value lowercased (a charset meta is always its own single identity);
for a link tag, rel: canonical is always one shared identity regardless of href, while any other
rel is identified by rel and href together; for a script tag, its src. Inline scripts
without src and link tags without a recognized rel have no identity and are always appended. A
meta entry must set exactly one of name, property, httpEquiv, or charset.
A custom.css at the root of the public dir gets an automatic link: stylesheet entry appended
here for free - see Public assets. Declaring your own head entry with
href: /custom.css replaces that automatic one by identity, so you can still move or configure it
explicitly.
Theme, globals, and other core settings
theme accepts a local directory, a local module, or an importable jsr:, npm:, or HTTPS module.
themeConfig is merged shallowly with theme defaults. globals are available both directly and as
globals in page layouts.
shortUrls defaults to false. devPort selects the initial development server port (default
5735). If it is unavailable, Steno scans forward one port at a time up to 65535 and binds the first
free one.
hashAssets defaults to true: theme CSS/JS get a content hash baked into their output filename
(style.css -> style.a1b2c3d4.css), so a redeploy with changed styles or scripts is served under
a new URL without a manual CDN cache purge. Set it to false to keep source filenames as-is.
minify defaults to true: theme CSS assets and rendered HTML pages both get comments and
extraneous whitespace stripped before being written to the output directory. <pre>, <script>,
<style>, and <textarea> contents are always left untouched in HTML, and CSS string literals
(quoted values, url(...)) are always left untouched, since minification is conservative by
design - a safety-first pass, not a byte-squeezing one.
Pass a plain boolean to turn everything on or off at once, or an object to control CSS and HTML independently:
minify: false # keep all source formatting as-is
minify:
css: false # keep theme CSS as-is, HTML still minified
html: false # keep rendered HTML as-is, CSS still minified
Each of css/html defaults to true when the other is set explicitly.
These fields, along with pluginSourcePolicy (below), used to live nested under a custom object.
That nesting is deprecated: set them at the top level of the config instead. steno doctor warns if
it finds any of them still under custom.
custom
custom is reserved for free-form, project-specific values that aren't part of Steno's own config
surface - for example a theme-facing stylesheets list that Steno exposes but never reads itself.
Anything Steno interprets directly (theme, themeConfig, shortUrls, devPort, globals,
pluginSourcePolicy) belongs at the top level, not under custom.
Plugin source policy
Top-level plugin specifiers from jsr: and npm: are allowed. Local file URLs, HTTP(S), and
node: specifiers require an explicit opt-in; data: and blob: are never allowed.
pluginSourcePolicy:
allowLocal: true
allowRemoteHttp: false
allowNodeBuiltins: false
allowThemePlugins: true # default
These settings are source filters rather than a runtime sandbox. They do not inspect transitive imports or reduce plugin permissions. All configured and theme-bundled plugins run in-process with the permissions granted to Steno.
The historical custom.pluginSourcePolicy and custom.pluginSecurity names remain accepted as
deprecated compatibility aliases. New projects should use top-level pluginSourcePolicy.
allowNodeBuiltins controls only a configured top-level node: specifier. It cannot prevent a JSR,
npm, file, or HTTP(S) plugin from importing a Node built-in internally.
See Plugins before enabling or installing code that executes during a build.
Isolated plugin entries
Object plugin entries can set mode: isolated. Isolated plugins accept permissions allowlists for
read, write, net, env, run, ffi, sys, and remote import hosts. They also accept
timeoutMs, maxOutputBytes, memoryMb, lockFile, and an optional integrity value. When
omitted, timeoutMs defaults to 5000, maxOutputBytes to 4194304 (4 MiB), and memoryMb to 128.
String entries and entries without a mode remain trusted and run in-process for compatibility. See
the plugin sandbox before granting capabilities.
Plugin integrity
Any object plugin entry, trusted or isolated, may set integrity: sha256-<base64>. Steno
verifies the digest of a file:// or https:// plugin's source before importing it and fails the
build on a mismatch. jsr: and npm: specifiers cannot be verified this way; pin their exact
version and use a frozen Deno lockfile (lockFile, isolated mode only) to protect their dependency
graph instead.
CLI
deno x jsr:@steno/steno [build|dev|preview|doctor|help] [--config path] [--port number]
build is the default. dev watches and serves the site, preview serves the already-built
production output without watching, and doctor reports common project/configuration problems; see
Doctor for the full check list. --port only applies to preview; it selects that
server's port, which defaults to 4173 and searches upward for the next available port the same way
dev does. dev's port comes from devPort in config instead (see above), since it has no
--port flag of its own. preview requires a prior build: it fails with an error naming the
missing output directory if dist/ does not exist yet. preview always binds to 127.0.0.1; dev
binds to 0.0.0.0.
See also
- Content for frontmatter,
_data, collections, and per-page overrides viasteno.*frontmatter. - Themes and Tau and Theme specification for what
theme/themeConfigfeed into. - Plugins for the
pluginslist itself, beyond source policy. - Doctor to catch config issues (like
custom.*nesting) before they reach a build. - Deploying for
outputin the context of an actual host.