Core reference · source audited
Template language essentials.
The language is deliberately small. This page is cross-checked against the current parser and documents the public primitives intended for new projects.
@content, @input and @pathto are enough for a surprisingly large class of sites. Learn that core first →, then add metadata, JSON or control flow only when the project actually needs them.
@content — place tracked content
@content accepts zero parameters. For a tracked item with a template, exactly one @content must execute across the rendered template/@input graph—even when the tracked content is empty. Zero or multiple executed insertions are build errors. If a tracked item does not need a content insertion point, omit its template and let the content file be the top-level Nift source.
Internally the parser processes tracked content through the same input mechanism, so the content becomes part of the dependency graph. An @content reached through a nested @input counts; one hidden in a Nift comment or skipped branch does not execute and therefore does not count.
<main>
@content
</main> @input(...) — process and insert one file
@input accepts exactly one path. Nift first checks the path as supplied; if it does not exist there, it also checks relative to the directory of the file currently being processed.
@input('templates/partials/header.html')
@content
@input('templates/partials/footer.html') The inserted file is parsed as Nift source, recorded as a dependency, and can itself contain additional @input calls. Input loops are detected and reported as build errors.
Relative partial composition
templates/
partials/
header.html
navigation.html <!-- templates/partials/header.html -->
<header>
@input('navigation.html')
</header> Because Nift can also resolve relative to the file currently being processed, nested partial structures do not have to repeat project-root paths everywhere.
Small presentation helpers
@join(array, separator)
@join renders a JSON array of scalar values with a textual separator. It is intentionally a presentation helper rather than a map/filter/reduce pipeline.
@join($[post.tags], ', ') Empty arrays render an empty string. Objects/arrays nested as items are rejected rather than implicitly flattened.
@substr(value, pos, length)
@substr selects a zero-based, length-based substring while respecting UTF-8 code-point boundaries:
@substr($[post.description], 0, 120) A position beyond the string renders an empty string, a length beyond the remaining text stops at the end, and negative positions/lengths are rejected.
@pathto(...) — checked paths from the current output
@pathto accepts exactly one value. If the value matches a tracked name, Nift resolves the target's output. Otherwise it treats the value as a concrete path and requires that path to exist.
Tracked-page links
<nav>
<a href="@pathto('/')">Home</a>
<a href="@pathto('about')">About</a>
<a href="@pathto('docs/getting-started')">Getting started</a>
</nav> Nift calculates the path relative to the output currently being generated. For tracked targets whose output filename is index.html, it resolves to the containing directory rather than forcing index.html into the URL.
Concrete local assets
<link
rel="stylesheet"
href="@pathto('public/assets/css/site.css')">
<script
type="module"
src="@pathto('public/assets/js/app.js')"></script>
<img
src="@pathto('public/assets/images/logo.svg')"
alt="Site logo">
<a href="@pathto('public/files/guide.pdf')">
Download guide
</a> If one of those concrete local paths does not exist, the parser reports that the target is neither a tracked name nor an existing file. That gives shared templates a useful build-time correctness check.
Nift also remembers resolved @pathto targets internally as requirements (reqs in page build metadata). Concrete project paths are existence-checked later: if one disappears, the referring page becomes stale, while ordinary modification alone does not rebuild it. Tracked-name targets are different: the tracked producer owns its own build state, so its output being temporarily absent (or its build failing) does not make otherwise-valid referring pages stale or transitively failed. A complete site build still fails if that producer itself fails.
Why prefer it to manual relative paths?
<!-- fragile: tied to one output depth -->
<img src="../../assets/images/logo.svg" alt="Site logo">
<!-- project-aware and checked -->
<img
src="@pathto('public/assets/images/logo.svg')"
alt="Site logo"> See the full paths guide with more real HTML examples →
@pathtofile(...) is still accepted as an alias of @pathto(...) so older projects can continue to build. New projects should use @pathto; the alias is intentionally not part of the recommended language surface.
$[...] — values and pure expressions
$[...] is Nift's value-expression syntax. A plain path such as $[title] or $[post.author.name] resolves one value; the same brackets can also evaluate pure numeric, comparison, logical and ternary expressions without introducing assignment or mutable template state.
$[loop.index + 1]
$[(price * quantity) / 100]
$[score >= 50]
$[post.featured ? ' featured'] Expressions support numeric +, -, *, / and integer-valued %, unary signs, parentheses, comparisons, !, short-circuit &&/||, and lazy ternary rendering. Arithmetic is numeric only; + is not string concatenation. In a ternary, a selected quoted string literal renders as its scalar value without the quote delimiters; otherwise the selected branch is parsed as ordinary Nift source, so it may contain directives such as @input(...). The unselected branch remains inert.
Expressions reference: operators, precedence, types, short-circuiting and ternaries →
Values can come from built-in page/build metadata, JSON/project-contract data, loop bindings and pagination metadata. The current parser recognises these 13 built-in page/build metadata names:
$[title]
$[name]
$[content-path]
$[output-path]
$[template-path]
$[build-timezone]
$[build-time]
$[build-UTC-time]
$[build-date]
$[build-UTC-date]
$[build-YYYY]
$[build-YY]
$[build-OS] <title>$[title]</title>
<!-- built $[build-date] --> Metadata reference and practical examples →
Structured JSON data
@json(path, name) loads immutable JSON data into the current page render. Access it through chained $[...] expressions such as $[site.navigation[2].title].
Use values inside textual parameters
Textual directive parameters can combine literal text with metadata, JSON scalars and lexically scoped loop values. This is useful when another build tool writes a manifest containing a hashed asset name, or when data selects a partial or dependency.
{
"entry": "public/assets/app-B7K2pQ.js",
"partial": "feature"
} @json('data/manifest.json', manifest)
<script type="module"
src="@pathto('$[manifest.entry]')"></script>
@input("templates/partials/$[manifest.partial].html") Interpolation is single-pass: Nift resolves the values once and passes the resulting text to the outer directive. If a value itself contains $[...], @input(...) or punctuation such as quotes, commas and parentheses, that text remains data—it is not reparsed as template syntax and cannot change the original parameter boundaries.
The eligible textual positions are @input, every @dep path, @pathto/@pathtofile, @getenv, @ent, and the @json source and optional schema paths. The @json binding name and control-flow grammar stay static. Strings, numbers, booleans and null become text; arrays and objects must be narrowed to a scalar member or element.
Use $ inside a quoted parameter when the dollar must remain literal rather than begin interpolation.
Loops and conditions
Structured data can repeat or select output with a small control-flow surface:
@for(item : array){...}iterates arrays.@for((key, val) : object){...}iterates object members.@if(...){...}selects output, with ordinaryelse if/elsebranches.
Conditions support !, &&, ||, parentheses and the comparison operators ==, !=, <, <=, > and >=.
@getenv(...) — read one environment variable
@getenv accepts exactly one environment-variable name and writes its value into the generated output. An unset variable resolves to an empty value.
@getenv('NIFT_ENV') <meta
name="deployment-environment"
content="@getenv('DEPLOY_ENV')"> Do not use @getenv to leak private tokens, passwords or credentials into client-visible HTML/JavaScript.
@ent(...) — emit a supported HTML entity
@ent accepts exactly one value. The mapping below is the complete mapping implemented by the current v4 parser.
| Input | Emits | Input | Emits |
|---|---|---|---|
` | ` | ~ | ˜ |
! | ! | @ | @ |
# | # | $ | $ |
% | % | ^ | ^ |
& | & | * | * |
? | ? | < | < |
> | > | ( | ( |
) | ) | [ | [ |
] | ] | { | { |
} | } | - | − |
_ | _ | = | = |
+ | + | | | | |
\ | \ | / | / |
; | ; | : | : |
' | ' | " | " |
, | , | . | . |
£ | £ | ¥ | ¥ |
€ | € | § or section | § |
+- | ± | -+ | ∓ |
!= | ≠ | <= | ≤ |
>= | ≥ | -> | → |
<- | ← | <-> | ↔ |
==> | ⇒ | <== | ⇐ |
<==> | ⇔ | <=!=> | ⇎ |
... | … |
@ent('@')
@ent('&')
@ent('...')
@ent('section') @dep(...) — advanced escape hatch
@dep is implemented by the parser and accepts one or more existing dependency paths, but it is intentionally documented separately because most projects should not need it. Normal template/content/@input relationships already establish dependencies automatically.
Read the advanced @dep guide →
Escaping literal Nift syntax
Most ordinary @, $ and # characters do not need escaping. Use a leading backslash only when you want to emit text that Nift would otherwise recognise as template syntax.
| Nift source | Generated output | Why |
|---|---|---|
\@input('example.html') | @input('example.html') | Emit an @input(...) expression literally instead of calling it. |
\$[title] | $[title] | Emit the metadata expression literally instead of substituting the page title. |
<\#-- example --#> | <#-- example --#> | Prevent the raw-comment opener from being interpreted as Nift syntax. |
CSS at-rules such as @media do not need escaping
Nift only treats recognised template syntax as a call. Ordinary CSS at-rules can be written normally:
@media (min-width: 60rem) {
.layout {
display: grid;
}
} That source passes through as the same @media rule. Older Nift versions required escaping in some situations, but current Nift deliberately allows unknown/plain @... text through so normal CSS does not need Nift-specific punctuation.
A useful rule
If the text would actually invoke Nift—such as @input(...) or $[title]—escape it when you want to show it literally. If it is ordinary source text such as @media, $HOME or #release-notes, write it normally.