Frontmatter
.tibi file may open with a frontmatter block fenced by ---. A bare --- fence (or ---rs) defaults to Rust; tag the fence to switch language: ---js, ---ts, ---py. The compiler shells out to the matching interpreter, captures the script's result object, and substitutes its keys into the template as {token} values.Substitution directives
Bare {name} emits the value HTML-escaped — safe by default, suits text interpolation. For richer payloads use a directive form {@kind name}:
| Token | Behaviour |
|---|---|
{name} |
HTML-escaped text. <, >, &, quotes all encoded. |
{@html name} |
Raw HTML. Use for pre-rendered markup returned by frontmatter (CMS body fields, server-side templating output). You're trusting the source — don't pass user input. |
{@md name} |
Renders the value as CommonMark + GFM tables, strikethrough, task lists, footnotes, and smart punctuation. Output is HTML, no extra escape needed. |
{@json name} |
JSON-context substitution. If the value already looks like a JSON literal (object / array / number / bool / null / quoted string) it's emitted verbatim; otherwise wrapped as "<escaped>". Drop inside <script type="application/json"> tags or data-* attrs. |
Unknown directive names ({@bogus name}) are left untouched in the output, same as unknown identifiers — useful when downstream substitution (e.g. dynamic-route params) hasn't run yet.
This page's frontmatter
The script above ran in node v20.20.2. It walked the project layout the compiler handed in via ctx and returned the numbers below.
pages/
components/
Source: /app/docs/pages/docs/frontmatter.tibi
Component with Python frontmatter
The <BuildStamp/> component has a Python frontmatter that computes a wall-clock timestamp and a per-host build id. It runs in python3; the resulting values become attributes of the component's shadow template.
Component with JavaScript frontmatter
The <PageRoster/> component reads ctx.pages_dir at compile time and lists every route on the site.
/, /docs, /docs/architecture, /docs/auth, /docs/builder, /docs/cli, /docs/client-modules, /docs/components, /docs/deploy, /docs/file-format, /docs/frontmatter, /docs/getting-started, /docs/mcp, /docs/plugins, /docs/routing, /docs/server, /docs/state, /docs/tether, /docs/ui, /install, /plugins
Rust frontmatter
The ---rs evaluator shells out to rustc to compile the body into a one-shot binary, then runs it. The slim docs runtime image deliberately doesn't ship a Rust toolchain (rustc + std weighs ~250 MB), so Rust frontmatter components only render where rustc is on PATH — typically during tibi build SSG runs or in a dev container. Apps that want runtime Rust frontmatter eval should base their image on a Rust toolchain layer.
Turning frontmatter off
Frontmatter runs code on the machine doing the compile, so a host compiling sources it doesn't trust (a hosted builder, for example) can refuse it. Compile with FrontmatterPolicy::Deny through tibi_compiler::compile_page_with_policy_in, or render a whole route, layouts included, with tibi_router::render_route_with_policy_in. Any file whose fence has a non-blank body is rejected with CompileError::FrontmatterDisabled before an interpreter is started. A rejected page or layout fails the render; a rejected component is contained like any other component error, showing the inline error box in its place. Every other entry point keeps FrontmatterPolicy::Run, so nothing changes for normal projects.
Languages today
Tibi ships subprocess evaluators for:
- JavaScript (
---js/---javascript) — runs in Tibi's persistent node worker pool (see Server routes). Setresultorreturna plain object.awaitworks at the top level;requireresolves relative to the.tibifile, then the project root.ctx.redirect/setStatus/setHeader/setCookieshape the response. - TypeScript (
---ts/---typescript) — types are stripped with the project's esbuild when installed, otherwise with Node's built-in type stripping (Node 22.13+), then it runs like JS. - Reserved keys:
layout,inherit_layout,__params,__status/__redirect/__headers/__cookies, andcontentType/as.bodyis the response body only on content-typed routes (withasorcontentType); on an HTML page it's an ordinary{body}value. - Python (
---py/---python) — spawnspython3 -c. Set the module-levelresultdict. - Rust (
---rs/---rust, also the default for a bare---fence) — compiles the body viarustc --edition 2021 -Ointo a one-shot binary, runs it, and reads JSON back from stdout. The body sees actxstruct (file_path: &str,pages_dir: Option<&str>) and alet mut result: Vec<(&str, String)>it pushes pairs onto. The runner emits the JSON itself — noserdedep required.
The runner injects a single ctx variable carrying file_path, pages_dir, (optionally) rel_path, route while rendering a request, and user when an auth layer signed someone in. Everything else is up to your script.