Latte Templates
Latte 3 syntax, filters, functions and template variables.
On this page
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 render template, checked in order: site/views/fields/{view}/{name}.latte, the field's render.latte beside its definition (site/fields/{name}/render.latte), and its type's render.latte (phlat/fieldtypes/{type}/render.latte, or site/fieldtypes/@phlat/{type}/render.latte to override a core type). 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" *}
{$timestamp|date} {* an int is read as a Unix timestamp *}
Functions
Available in every template without controller wiring:
| Function | Returns |
|---|---|
config($key, $default) |
A config value, dot notation |
p($uri) |
The page at $uri, or the current page with no argument |
find($query) |
Pages matching a registry query, from the root |
addon($id, $key, $default) |
An addon's setting from its addons.{id} config entry |
collection($field) |
The collection behind a field, for get($uuid) and queries |
source($name) |
The raw contents of a template file |
session($key) |
A value from the site session |
permission($key) |
True if the logged-in user has the permission |
highlight($code, $language, $theme) |
Highlighted code as a <pre> element, in any language the code field offers and any Phiki theme |
nonce() |
The request's CSP nonce |
isPartial() |
True for an HTMX request |
isFragmentRequest() |
True when the URL has a :blockname suffix |
isAdminPreview() |
True when the page renders inside the admin's preview for a signed-in editor |
placeholder($w, $h, $color) |
A static SVG data URI |
icon($name, $class, $inline) |
An icon from the icons addon, drawn from its cached sprite; $class goes on the <svg>, inline: true returns the SVG itself |
{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.
Includes
{include} pulls in a template. A bare name such as 'header.latte' is in your site's views/, and ./ or ../ is relative to the including file. It sees the including template's variables, and anything after the comma is added on top. Pass the names it needs explicitly anyway, so it doesn't depend on where it is included from. In particular $page is the page rendering the whole response, not the page the included template belongs to. See Views.
{include '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. $app is not available.
HTMX
HTMX attributes go straight in templates. Mutating requests need no token: a POST, PUT, PATCH or DELETE that a browser sends from another origin is refused with a 403.
<form hx-post="{$page->url()}" hx-swap="outerHTML">
...
</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');
});
When you need more
A block can be fetched on its own at a :blockname URL, and cached. See Routing.