{if}, {each}, {/each}. The lexer recognises
14 names directly after a {. Eleven are usable tags, two are branch markers legal
only inside another tag, and one (with) is recognised by the lexer but has no
parser behind it, so every {with} is a hard error.
The complete table
Four more constructs are brace-delimited but are not block keywords. They are
listed here because they are written the same way:
{if}
Renders the first branch whose condition is truthy.
{else if}, at most one {else}. Neither takes a closing tag;
{/if} is required.
Truthiness: null, false, the number 0, the empty string, the empty array and
the empty object are falsy. Everything else is truthy, including the strings "0"
and "false". Note that an empty array is falsy here and truthy in JavaScript.
{each}
required
The binding for the current element. It comes first.
Optional, after a comma. A 0-based number.
required
The literal keyword
in. Omitting it is UnexpectedToken.required
Any expression. Filters after it are loop filters, a different table from
the value filters.
Loop variables
Four variables are bound automatically inside the body.
These four names are resolved by a flat-key fast path, so exactly
loop.index,
loop.count, loop.first and loop.last are shadowed inside a loop. Any other
property of a real loop object in the context still resolves normally.
Collection semantics
Each loop is capped at 1000 items. Over the cap the loop truncates, records an
IterationLimitExceeded warning, and the build still succeeds. A render is
additionally capped at 1000000 iterations in total, which is what bounds nested
loops.
{match}
{case} keyword. An arm is a bare pattern, then ->, then a body.
Writing {case "a"}A{/case} produces a cascade of UnexpectedToken errors starting
with Expected match pattern, found LBrace.
Patterns are a string literal, a number literal, a boolean literal, or _ for the
wildcard. Arms are self-delimiting: a body ends at the next pattern token, so no
per-arm closing tag exists.
Matching is type-strict. A "5" pattern never matches a numeric 5.
_, the block renders nothing.
Inside
{match} the lexer tokenises bare quotes, digits, -> and _ so it can
read arm patterns. A stray quote in an arm body can therefore misparse, even
though the same quote is harmless elsewhere in a template.{fetch}
name out of the render
context and picks a branch. The host is responsible for putting the data there.
The type hint
The syntax isname: Type or name: Type[]. The [] is only legal after a
colon and a type name. {fetch products[] from "/api"} is a parse error
(Expected 'from' in {fetch}).
The hint is recorded in the AST and has no effect on rendering. Nothing is
validated against it, and it does not change which branch is chosen.
Options
Options follow the URL expression. Only the parenthesised call form parses. An unrecognised option, and any colon form such ascache:300, is silently
dropped.
abortOnNavigate is accepted as an alternative spelling of the last one. All of
these are instructions for the optional browser runtime; the server ignores them.
{repeat}
null, an
array or an object all yield 0 iterations, so {repeat "5"} renders nothing. A
float is truncated toward zero, a negative is 0, and the count is capped at 1000.
{try}
{error} branch. This is
purely a client-side boundary and does nothing without the browser runtime.
{html}
Raw, unescaped output. This is the only way to bypass escaping.
sanitize runs an ammonia allowlist. Allowed tags:
<script>, <iframe>, <form> and <style>. Attributes work the same way. URL
schemes are limited to http, https and mailto.
Every sanitized
<a> gains rel="noopener noreferrer", not only one that
carries a target. That is stricter than the requirement, and a test pins it.srcset is not on the attribute allowlist: the scheme check does not parse its
comma-separated candidate grammar.{html} in attribute position
{html expr} also works as an attribute spread, including a conditional form:
on*, x-html, x-data, x-init,
x-effect) or a script-scheme value is dropped.
{$let}
Seeds CLIENT state. Server-side it emits only a JSON payload.
{$let cfg = {a: 1}} is a lex error. Pass the value through the render
context instead.
{slot} and {layout}
Only two slot names are ever filled: default and head. Any other name renders
as a literal <slot name="x"> element in the output.
layout frontmatter directive. A layout: line in frontmatter is
silently ignored.
Slot fallback
{slot}fallback{/slot} renders its fallback only when nothing filled the slot at
all.
A named slot always falls through, because only default and head are ever
filled, so <slot name="other">OTHER-FB</slot> renders as
<slot name="other">OTHER-FB</slot> in the output: the literal element, with the
fallback inside it.
{sections} and {blocks}
Host integration points. Neither takes a closing tag.
{sections} emits pre-rendered section HTML the host injected under the reserved
context key __slurp_sections_html. {sections "header"} reads
__slurp_section_group_header_html instead, which is how global chrome lives in a
layout. The page-level key is stripped when entering a component or layout scope,
so a stray {sections} in a component cannot duplicate the page. Group keys are
not stripped.
{blocks} renders the child blocks of the nearest enclosing section or block, each
from its own blocks/<type>.slurp file, preferring an enclosing block over an
enclosing section so nesting works. Each child is wrapped for the editor:
[A-Za-z0-9_-]+, because it arrives
from saved editor state and is used as a registry path; a block whose file hosts
its own type is skipped by a cycle guard; and every block is charged against the
shared iteration budget.
Neither tag produces anything under
slurp build, because the CLI supplies no
section state. A page with a section { } schema built from the CLI renders
${ section.settings.heading } as the empty string, not as the schema default.
Defaults are applied by the host’s render_section call. See
Schemas.{debug}, {redirect}, {next}
{debug expr} pretty-prints a value into a visible <pre data-slurp-debug> in
DEVELOPMENT mode and is stripped with no trace in production. slurp build always
uses production mode, so {debug} never reaches a build.
{redirect "path"} and {next} are middleware-only signals that render nothing.
Using either outside middleware is a RedirectOutsideMiddleware error. Middleware
is a compile option, not a directory convention, so the CLI treats every file as
non-middleware unless told otherwise with --middleware.
{with} does not exist
with is in the lexer’s keyword table but no parser supports it, so it is a hard
error rather than an unknown-tag no-op: