Latte Templates
Latte 3 syntax, filters, functions and template variables.
Assignment and output
{var} assigns without output. {$x = ...} assigns and outputs.
{var $children = $page->children()} no output
{$title = 'Hello'} outputs: Hello
Output is HTML-escaped by default. Use |noescape only for values you trust:
{$page->title} escaped
{$html|noescape} raw
Fields
$page->fieldName returns a FieldValue. Output it directly for the decoded string, or pipe it through |render:
{$page->body} decoded string, escaped
{$page->body|render} via the field type's render template
A FieldValue is always truthy, even when its content is empty. Check ->empty():
wrong: always true
{if $page->body} ... {/if}
right
{if !$page->body->empty()} ... {/if}
Page has typed properties for its structural keys (uuid, view, published, access, hidden, locked, created, updated) which are plain strings or bools. Everything else, title included, is a FieldValue. See Fields.
Filters
|render
Passes a FieldValue through its type's render template, checked in order: site/views/fields/{view}/{name}.latte, site/views/fields/{name}.latte, site/views/fields/{type}.latte. With none, it falls back to the decoded value. The template receives $field, $value (the decoded value), $page and $request.
|ago, |bytes, |date
{$page->created|ago} "3 days ago"
{$file->size|bytes} "1.2 MiB"
{$page->created|date} "4 Jan 2026"; default format is j M Y
{$page->created|date:'Y-m-d'} "2026-01-04"
Functions
Available in every template without controller wiring:
| Function | Returns |
|---|---|
config($key, $default) |
A config value, dot notation |
addon($id, $key, $default) |
An addon's setting from its addons.{id} config entry |
p($uri) |
The page at $uri, or the current page with no argument |
find($query) |
Pages matching a registry query, from the root |
session($key) |
A value from the site session |
permission($key) |
True if the logged-in user has the permission |
csrf() |
The CSRF token |
nonce() |
The request's CSP nonce |
isPartial() |
True for an HTMX request |
isFragmentRequest() |
True for a :blockname fragment request |
placeholder($w, $h, $color) |
A static SVG data URI |
icon($name, $class) |
An inline SVG from phlat/icons/; $class goes on the <svg> |
{foreach find('view=article, sort=-created, limit=4') as $article}
<a href="{$article->url()}">{$article->title}</a>
{/foreach}
{icon('arrow-right', 'w-4 h-4 text-primary')}
Layouts and blocks
A child template declares a {layout} and fills its blocks; {include parent} inside a block pulls in the parent block's default content. See Views.
Fragments
{partial 'target-id'} makes a block's content self-targeting: HTMX swaps it into #target-id on a :blockname request. See Routing.
Includes
{include} pulls in a partial, with paths resolved from the views root. Variables after the comma are passed as locals. A partial does not inherit the outer template's variables, so pass everything it needs.
{include 'site/header.latte'}
{include 'ui/badge.latte', label: 'New'}
Loops and conditionals
{foreach $items as $i => $item}
<li class="{if $iterator->first}first{/if}">{$i}: {$item->title}</li>
{/foreach}
{if $page->hidden}
<span>Hidden</span>
{elseif !$page->published}
<span>Unpublished</span>
{else}
<span>Published</span>
{/if}
Inside {foreach}, $iterator has first, last, counter (1-based), counter0 (0-based), and odd/even.
Template variables
Every view gets $page. Anything else comes from the global and view controllers, with later keys winning. $app is not available unless a controller passes it. See Controllers.
HTMX
HTMX attributes go straight in templates. Include the CSRF token in mutating requests:
<form hx-post="{$page->url()}" hx-swap="outerHTML">
<input type="hidden" name="_csrf" value="{csrf()}">
...
</form>
JavaScript
There is no client-side framework. Interactivity is plain JS in per-area bundles such as admin.js, loaded with <script defer>. Use data attributes and event delegation on document so handlers survive HTMX swaps:
<button type="button" data-toggle-target="#panel">Toggle</button>
<div id="panel" class="hidden">Content</div>
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-toggle-target]');
if (btn) document.querySelector(btn.dataset.toggleTarget).classList.toggle('hidden');
});