Referenz

Template-Syntax

Alles, was der Parser versteht, passt auf diese Seite. Es gibt fünf Konstrukte — Interpolation, Blöcke, Filter, Partials und Kommentare — und keine Möglichkeit, beliebiges JavaScript einzubetten.

Interpolation

Doppelte geschweifte Klammern fügen einen Wert ein. Das Ergebnis wird HTML-escaped, wobei die Escaping-Regeln danach gewählt werden, wo die Interpolation steht.

<p>{{user.name}}</p>                  // text context
<a href="{{link}}">…</a>            // URL context
<div style="width:{{w}}px">…</div>   // style context

Im URL-Kontext wird ein Wert, dessen Schema nicht http, https, mailto oder ein relativer Pfad ist, durch about:blank ersetzt. Das ist bewusst nicht konfigurierbar.

Escaping abschalten

Dreifache Klammern fügen den Wert unverändert ein:

<div class="body">{{{post.renderedHtml}}}</div>
Per Definition unsicher

Dreifache Klammern sind die einzige Stelle, an der Verso eine Injection erzeugen kann. Kompilieren Sie mit { allowRaw: false }, um sie im ganzen Projekt zum Kompilierfehler zu machen — nützlich, wenn Templates von Leuten geschrieben werden, die keine Sicherheitsreviews machen.

Bedingungen

Blöcke öffnen mit # und schließen mit /. Bedingungen akzeptieren einen Pfad, einen Vergleich oder einen Pfad durch Filter — sonst nichts.

{{#if user}}
  Welcome back.
{{#elseif invite.valid}}
  You were invited by {{invite.from}}.
{{#else}}
  <a href="/sign-in/">Sign in</a>
{{/if}}

Die Wahrheitswerte folgen JavaScript mit einer Änderung: Ein leeres Array ist falsy. In diese Entscheidung sind sechs Jahre Template-Bugs eingeflossen.

Vergleichsoperatoren sind ==, !=, <, <=, >, >= und in. Gleichheit ist strikt; eine lockere Variante gibt es nicht.

{{#if order.status == 'shipped'}}…{{/if}}
{{#if role in allowedRoles}}…{{/if}}
{{#unless flags.beta}}…{{/unless}}

Schleifen

#each iteriert über Arrays, Maps, Sets und einfache Objekte. Die optionale Klausel as benennt das Element und an zweiter Stelle den Index oder Schlüssel.

{{#each releases as release, i}}
  <li class="{{#if i == 0}}latest{{/if}}">
    {{release.version}} — {{release.date | date: 'MMM yyyy'}}
  </li>
{{#else}}
  <li>No releases yet.</li>
{{/each}}

Der Zweig #else einer Schleife läuft, wenn die Sammlung leer ist oder fehlt. Im Rumpf stehen vier Variablen zur Verfügung:

VariableBedeutung
@indexPosition, ab null gezählt
@firsttrue im ersten Durchlauf
@lasttrue im letzten Durchlauf
@lengthGesamtzahl der Elemente
Hinweis

Verschachtelte Schleifen verdecken diese Variablen. Um eine äußere zu erreichen, benennen Sie sie in der Klausel as — eine ../-Syntax für Elternpfade hat Verso absichtlich nicht.

Filter

Ein Pipe-Zeichen wendet einen Filter an. Filter werden von links nach rechts verkettet und nehmen Positionsargumente nach einem Doppelpunkt.

{{title | upper}}
{{post.body | strip | truncate: 280, '…'}}
{{price | number: 2 | prepend: '€'}}

Achtzehn Filter sind eingebaut:

FilterArgumenteWirkung
upper / lower—Groß-/Kleinschreibung, gebietsschemaabhängig
capitalize—Erster Buchstabe groß, Rest unverändert
trim / strip—Leerraum kürzen / alle HTML-Tags entfernen
truncateLänge, AuslassungKürzt an einer Wortgrenze
numberNachkommastellenFeste Nachkommastellen mit Tausendertrennung
dateMuster, ZeitzoneFormatiert ein Date oder einen ISO-String
joinTrennzeichenVerbindet ein Array
sortSchlüsselStabile Sortierung, optional nach Eigenschaft
reverse / unique—Array-Helfer
first / lastAnzahlAusschnitt von einem der Enden
defaultWertErsetzt leere Werte, null oder undefined
jsonEinrückungSerialisiert, sicher innerhalb von <script>
prepend / appendTextVerkettung

Eigene Filter

Registrieren Sie eigene beim Kompilieren. Ein Filter ist eine reine Funktion; er bekommt zuerst den Wert und danach die Argumente.

const page = await compileFile('views/page.vs', {
  filters: {
    money: (v, currency = 'EUR') =>
      new Intl.NumberFormat('en', { style: 'currency', currency }).format(v)
  }
});

Die Ausgabe von Filtern wird escaped wie jeder andere Wert. Um aus einem Filter Markup zurückzugeben, muss es in raw() verpackt werden, das genau dafür exportiert wird und sich im Review leicht per grep finden lässt.

Partials und Slots

> bindet ein anderes Template ein. Pfade werden relativ zur einbindenden Datei aufgelöst.

{{> './partials/card.vs'}}
{{> './partials/card.vs' with item}}
{{> './partials/card.vs' with { title: post.name, tone: 'muted' }}

Ohne with sieht das Partial das aktuelle Datenobjekt. Mit dieser Klausel sieht es nur, was Sie übergeben — dadurch lassen sich Partials isoliert testen.

Layouts

Layouts sind Partials mit benannten Slots:

layouts/base.vs
<main>{{#slot}}{{/slot}}</main>
<aside>{{#slot 'sidebar'}}Nothing here.{{/slot}}</aside>
views/article.vs
{{#extends '../layouts/base.vs'}}
  {{#fill}}<article>{{{post.html}}}</article>{{/fill}}
  {{#fill 'sidebar'}}{{> '../partials/toc.vs'}}{{/fill}}
{{/extends}}

Inhalt innerhalb von #slot ist der Ersatz, wenn kein #fill passt. Ein Template darf genau ein Layout erweitern, und Layouts dürfen Layouts erweitern.

Verzögerte Blöcke

Im Streaming-Modus markiert #defer einen Block, dessen Daten noch nicht bereit sind. Die umgebende Seite wird sofort ausgegeben, und der Block folgt, sobald sein Promise erfüllt ist.

<section>
  {{#defer recommendations as list}}
    {{#each list as item}}<li>{{item.title}}</li>{{/each}}
  {{#pending}}
    <p class="skeleton">Loading recommendations…</p>
  {{#failed as err}}
    <p>Could not load recommendations.</p>
  {{/defer}}
</section>
Rendern in einen String

Außerhalb von renderToStream() wartet #defer einfach an Ort und Stelle auf das Promise. Der Zweig #pending wird nie ausgegeben, und ein abgelehntes Promise rendert #failed, statt eine Ausnahme zu werfen.

Leerraum und Kommentare

Ein Bindestrich in den Klammern entfernt angrenzenden Leerraum, was bei allem außer HTML wichtig ist:

{{#each rows as r -}}
  {{r.a}},{{r.b}}
{{/each}}

Kommentare erreichen die Ausgabe nie:

{{!-- reviewed 2026-02-14, keep in sync with the API schema --}}