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.