Themes and Tau
This page covers directory-based themes: a folder with a theme.yaml, built with Tau templates,
loaded from a local path. For themes authored as a mod.ts module (including how Steno resolves a
theme specifier, and the StenoTheme shape either kind of theme produces), see the
Theme specification. For the template language itself (expressions,
filters, control flow), see Tau syntax.
A local theme is a directory with layouts, optional registered components, and optional assets:
theme/
├── theme.yaml
├── layouts/
│ └── layout.tau
├── components/
│ └── Header.tau
├── scripts/
│ └── site.ts
└── assets/
└── site.css
# theme.yaml
name: example-theme
version: 1.0.0
components:
header: components/Header.tau
defaultConfig:
brand: Steno
configSchema:
showSearch: { type: boolean, default: true, description: Show search }
density: { type: string, enum: [compact, comfortable], default: comfortable }
social:
type: object
properties:
github: { type: string, pattern: "^https://github\\.com/" }
Layout files use the .tau extension; their base filename is the layout name. A page without
layout uses layout, so it needs layouts/layout.tau. Components must be declared in
theme.yaml; their declared key is capitalized when loaded (header becomes <Header />). Assets
are copied to <output>/assets/.
Point a project at this theme with theme: ./theme (or wherever the folder lives, relative to the
config file) in content/.steno/config.yml. See Resolution for
every specifier form theme accepts, and Configuration for themeConfig.
Add extends: jsr:@steno/theme-minimal (or a local path to another theme.yaml directory) to
theme.yaml to override just a layout or two instead of building a theme from scratch - see
Extending a directory theme.
Scripts
scripts/*.ts/*.tsx are transpiled to JavaScript and merged into the theme's assets, so
scripts/site.ts is reachable at /assets/site.js - scripts/foo/bar.ts is flattened the same
way, to /assets/bar.js. Existing scripts/*.js/*.jsx are copied through unchanged. This only
applies to directory-based themes (theme.yaml); a theme authored as a mod.ts module already has
full control over how it builds its own assets map. Omit scripts/ entirely if a theme has no
need for it - there's no cost either way.
Layout context
Every layout receives content (compiled Markdown), site, theme, data, collections, env,
globals, assets, public environment variables, and all page frontmatter. theme contains its
name/version plus merged configuration. Writing a TypeScript helper that builds part of this context
yourself (outside .tau templates, which aren't typed)? Import PageRenderContext from
@steno/steno for the same shape, with autocomplete.
assets maps each theme asset's source-relative path (as written in assets/ or scripts/) to its
output filename. CSS and JS assets are written under a content-hashed filename by default
(site.css -> site.a1b2c3d4.css) so a redeploy with changed styles or scripts gets a new URL
automatically - no CDN cache purge needed. Set hashAssets: false in the site config to keep source
filenames as-is. CSS assets are also minified by default; set minify: { css: false } to keep
source formatting as-is. Reference assets through this map rather than hardcoding the source
filename either way:
<!doctype html>
<title>{title} · {site.title}</title>
<link rel="stylesheet" href="/assets/{assets['site.css']}" />
<header title="{site.title}" />
<article>{@html content}</article>
Component contexts include their props plus site, theme, globals, and the global values
themselves. They do not implicitly inherit arbitrary page frontmatter.
Tau syntax
Expressions are JavaScript expressions and are HTML-escaped:
<h1>{title | upper}</h1>
{#if date}
<time>{date | date}</time>
{:else}
<span>Undated</span>
{/if} {#each tags as tag, index}<span>{index}: {tag}</span>{/each}
Use {@html expression} only for trusted HTML, such as Steno's generated content. Built-in
filters include date, truncate(length), upper, lower, slugify, pluralize,
number_format, and markdown_inline; see
Built-in filters for their defaults and edge-case behavior. Invoke
a component with <Header />; props may be literals, expressions (title={title}), or shorthand
({title}).
{@include "name"} in a theme resolves a registered component name through the theme renderer. For
Markdown source-file includes, see Content.
Sharing boilerplate across layouts
Tau has no extends/layout-inheritance syntax, but {@include} already covers the common case that
would motivate one: a <head> block (charset, viewport, favicons, stylesheet links, and similar)
repeated identically across every layout in a theme.
The key difference from a <Component /> invocation is context: a component only receives its
explicit props plus site/theme/globals (see Layout context above), but
{@include "name"} inherits the full context of the template that includes it - the same
title, description, and other page frontmatter a layout itself sees. Register the shared block
as an ordinary component and pull it in with {@include} instead of <Head />, and every value it
needs is already in scope:
# theme.yaml
components:
head: components/head.tau
<!-- components/head.tau -->
<head>
<meta charset="utf-8" />
<title>{title} · {site.title}</title>
<meta name="description" content="{description}" />
<link rel="stylesheet" href="/assets/{assets['site.css']}" />
</head>
<!-- layouts/article.tau -->
<html>
{@include "Head"}
<body>
{@html content}
</body>
</html>
{@include} replaces the parts that are identical everywhere; it does not let a child layout
override part of what it includes. When an include contains the entire <head>, put additional
metadata inside that component's <head>, not after the include in the calling layout. Prefer the
Base composition pattern above when sharing the surrounding document structure as well.
Safety limits
Tau templates cannot access ambient runtime globals such as Deno, globalThis, process, or
generated renderer internals. Mutating, code-generating, prototype, and constructor expressions are
rejected.
Rendering also enforces shared limits across layouts, includes, and components: 64 nested renders,
100,000 loop iterations, 16 MiB of output, and 1 MiB per template by default. API consumers can
lower these limits through TauOptions.limits.
These controls harden rendering against malformed templates and accidental resource exhaustion. Tau templates remain trusted theme code and are not a security sandbox for arbitrary user-authored expressions.
See also
- Tau syntax for the full expression grammar, built-in filters, and escaping rules.
- Theme specification for module-based (
mod.ts) themes,configSchemavalidation rules, and howthemeis resolved. - Configuration for
theme,themeConfig,hashAssets,minify, and other site-level settings.