Tibi

Frontmatter

Every .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}:

TokenBehaviour
{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.

23 .tibi files in pages/
18 .tibi files in 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.

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). Set result or return a plain object. await works at the top level; require resolves relative to the .tibi file, then the project root. ctx.redirect / setStatus / setHeader / setCookie shape 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, and contentType / as. body is the response body only on content-typed routes (with as or contentType); on an HTML page it's an ordinary {body} value.
  • Python (---py / ---python) — spawns python3 -c. Set the module-level result dict.
  • Rust (---rs / ---rust, also the default for a bare --- fence) — compiles the body via rustc --edition 2021 -O into a one-shot binary, runs it, and reads JSON back from stdout. The body sees a ctx struct (file_path: &str, pages_dir: Option<&str>) and a let mut result: Vec<(&str, String)> it pushes pairs onto. The runner emits the JSON itself — no serde dep 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.