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');
});