Steno

Configuration reference

Steno reads content/.steno/config.yml by default; pass another path with --config. 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 written as TOML instead.

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 /; entries that do not are skipped with a console warning. An empty to value is skipped the same way. 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.

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.

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 via steno.* frontmatter.
  • Themes and Tau and Theme specification for what theme/themeConfig feed into.
  • Plugins for the plugins list itself, beyond source policy.
  • Doctor to catch config issues (like custom.* nesting) before they reach a build.
  • Deploying for output in the context of an actual host.

Type to search guides, configuration, Tau, plugins, and API references.