Structured data · Core reference
Loops & conditions.
Nift has deliberately constrained control flow for rendering structured JSON data: @for(...){...} for arrays/objects and @if(...){...} with ordinary else if / else branches.
There are no assignments, mutable variables, arithmetic language, user functions or arbitrary expressions. The purpose is to select and repeat structured data at build time.
Loop over an array
{
"articles": [
{"title":"First","type":"article"},
{"title":"Notes","type":"note"},
{"title":"Third","type":"article"}
]
} @json('data/articles.json', data)
@for(item : data.articles){
<article>
<h2>$[item.title]</h2>
</article>
} The binding is scoped to the loop. It does not leak into the rest of the page.
Loop over an object
{
"social": {
"github": {"label":"GitHub","url":"https://github.com/"},
"docs": {"label":"Docs","url":"/docs/"}
}
} @for((key, val) : data.social){
<a data-kind="$[key]" href="$[val.url]">
$[val.label]
</a>
} Object iteration preserves the document's stored member order unless you explicitly request sorting. The parenthesised pair makes key/value destructuring explicit.
Loop metadata with $[loop.*]
Every @for iteration gets a small read-only loop binding describing its position in the rendered iteration order:
| Value | Meaning |
|---|---|
$[loop.index] | One-based position: 1, 2, 3, … |
$[loop.index0] | Zero-based position: 0, 1, 2, … |
$[loop.first] | true only for the first rendered iteration. |
$[loop.last] | true only for the last rendered iteration. |
$[loop.length] | Total number of items in the collection. |
@for(item : data.navigation) {
<a href="$[item.url]">$[item.title]</a>
@if(!loop.last) {
<span aria-hidden="true">·</span>
}
} The metadata is lexical. Nested loops get their own loop object while the inner loop runs, then Nift restores the outer loop metadata afterwards:
@for(section : data.sections) {
<h2>$[loop.index]. $[section.title]</h2>
@for(item : section.items) {
<p>Item $[loop.index] of $[loop.length]: $[item.name]</p>
}
<p>Finished section $[loop.index] of $[loop.length].</p>
} loop is reserved deliberately.You cannot create a JSON alias or loop variable named loop, just as loop bindings cannot replace built-in metadata such as title. That means $[loop.index] can never become ambiguous because a template happened to bind unrelated user data under the same name.
Sort while iterating with by ... asc|desc
When the source order is not the display order you want, add a sort key to the loop header:
@for(post : data.posts by post.date desc) {
<article>
<h2>$[post.title]</h2>
<time>$[post.date]</time>
</article>
} The syntax is:
@for(item : collection by item.field asc) { ... }
@for(item : collection by item.field desc) { ... } asc or desc is required when by is present. Without a by clause, Nift preserves the collection's stored order.
Numeric sorting
@for(product : data.products by product.price asc) {
<p>$[product.name] — $[product.price]</p>
} String sorting
@for(person : data.people by person.name asc) {
<li>$[person.name]</li>
} Sort scalar arrays too
@for(score : data.scores by score desc) {
<li>$[score]</li>
} Object iteration can sort by key or value data
@for((key, project) : data.projects by project.priority desc) {
<h2 data-key="$[key]">$[project.title]</h2>
} For object loops the sort expression must begin with either the key binding or value binding. This also makes alphabetical key ordering possible:
@for((key, value) : data.labels by key asc) {
...
} Every sort key must be a number or every sort key must be a string. Mixed types, objects, arrays, booleans and null are rejected rather than coerced. Equal keys keep their original relative order. Nift computes an iteration order; it does not rearrange or mutate the loaded JSON document.
Loop metadata follows the sorted order
After sorting, loop.first, loop.last and the index values describe the order that is actually rendered:
@for(post : data.posts by post.date desc) {
@if(loop.first) {
<strong>Newest</strong>
}
<a href="$[post.url]">$[post.title]</a>
} Conditions
@if(item.published){
<p>Published</p>
} Negation
@if(!item.draft){
<a href="$[item.url]">$[item.title]</a>
} Scalar comparisons
Conditions support equality with ==/!= and ordering with <, <=, > and >=.
@if(item.type == "article"){
<article>...</article>
}
@if(item.type != "note"){
...
}
@if(item.priority >= 3){
...
}
@if(item.price < 100){
...
}
@if(item.enabled == true){
...
}
@if(item.value == null){
...
} Both sides of a comparison may also be JSON paths:
@if(item.type == site.featured_type){
...
}
@if(item.priority >= site.minimum_priority){
...
} Numbers compare numerically. Strings compare lexicographically. Both operands of <, <=, > or >= must be numbers or both must be strings; Nift does not coerce between JSON types. Booleans, null, arrays and objects are not orderable.
else if and else
Branches use ordinary text syntax—there is no @else keyword:
@if(item.type == "article"){
<article>...</article>
}
else if(item.type == "video"){
<video>...</video>
}
else if(item.type == "gallery"){
<section class="gallery">...</section>
}
else{
<p>Unknown item type</p>
} You can have as many else if branches as the template needs. A plain else is optional; when present, it is the unconditional fallback and ends the chain.
Truthiness
| Value | Condition |
|---|---|
true | true |
false, null | false |
| number | false for zero, true otherwise |
| string | false when empty, true otherwise |
| array/object | false when empty, true otherwise |
Nesting
@for(section : data.sections){
<section>
<h2>$[section.title]</h2>
@for(item : section.items){
@if(item.visible){
<a href="$[item.url]">
$[item.title]
</a>
}
}
</section>
} Loop variables may be shadowed by a nested loop; Nift restores the outer binding when the inner loop finishes.
Skipped branches really are skipped
Nift only parses the selected branch. This is important for both behaviour and dependencies:
@if(site.use_legacy_navigation){
@input('partials/legacy-navigation.html')
}
else{
@input('partials/navigation.html')
} Only the selected input participates in that page render and dependency set. Code in an unselected branch is not executed.
Why : instead of in?
The loop syntax intentionally uses punctuation:
@for(item : items){ ... }
@for((key, val) : object){ ... } It stays visually compact, avoids pretending Nift has a larger keyword-based programming language, and behaves more predictably in generic syntax highlighters.
What conditions intentionally do not include
No assignment
No arithmetic expression language
No && / || expression trees
No mutable variables
No user-defined functions
No map/filter/reduce methods
No arbitrary code execution If data needs substantial transformation, do that in the tool that owns the data, write JSON, then let Nift render the result.
Why there is no general query pipeline
Nift deliberately stops short of turning @for into SQL, LINQ or a functional collection language. Sorting belongs naturally to iteration, and @if already handles presentation-time filtering. Joins, grouping, arbitrary projections, map/reduce, aggregation and transformation pipelines would add a second data-processing language to templates.
For substantial data transformation, use the tool that owns or prepares the data—TypeScript, Python, jq, a database query, an API/export step or another specialist tool—write the resulting JSON, then let Nift schema-check and render it. This keeps templates focused on data → output and keeps Nift's language small.
You may never need this page
Many excellent Nift projects can stay entirely on @content, @input and @pathto. Control flow exists for the point where structured data genuinely makes repetition or selection clearer than authored markup.