Use the Latte templating language on Statamic sites.
Features
- Use Statamic's built-in tags and modifiers
- Resolve the current layout from entry data
- Render Antlers inline where useful
- Use
<x-component>for Latte and Blade components
Why Latte?
Latte is simple, safe, and fast. Templates are compiled. Expressions are plain PHP. Output values are escaped automatically, in every context. Latte adds concise inline control structures and smart attributes for expressive templating.
- PHP syntax, no new language: Expressions and conditions are plain PHP, so there's little mental context-switching between template and application code.
- Context-aware escaping: Latte understands HTML and escapes differently inside text, attributes, JavaScript or URLs.
- Concise & expressive: Control flow lives directly on the html elements, clarifying intent and reducing nesting.
- Smart html attributes: Booleans,
null, arrays and data attributes render correctly with no manual string juggling, similar to modern frontend frameworks. - Fast: Templates compile to PHP once and run as native code on every request.
Antlers
{{ if entries | count }} <nav> {{ entries }} {{ if link }} <a href="{{ link }}">{{ title }}</a> {{ else }} {{ title }} {{ /if }} {{ /entries }} </nav>{{ /if }}
Latte
<nav n:ifcontent n:inner-foreach={$entries as $entry}> <a n:tag-if={$entry->link} href={$entry->link}> {$entry->title} </a></nav>
Installation
composer require daun/statamic-latte
Usage
Once installed, you can use Latte views in your frontend. Save or rename your views
using the extension .latte and reference them as usual. Antlers and Latte views can live
side-by-side as long as view names are unique.
Tags
Statamic Tags can be used via the native s: tag.
Found {s:collection:count in:pages /} pages:
Unlike Antlers,
Latte does not hoist loop item keys into scope. Inside a loop, the item itself is exposed
as $value. Access fields explicitly with $value->title over a bare {title}.
{s:collection:pages} {$value->title}{/s:collection:pages}
Assign tag output using parentheses:
{var $entries = (s:collection from: pages, sort: title)}{foreach $entries as $entry}{$entry->title}{/foreach}
Or capture output into a variable using the as param:
{s:collection from: pages, as: entries} {foreach $entries as $entry}{$entry->title}{/foreach}{/s:collection}
Use self-closing tags to output simple scalar return values from tags:
{s:link to: "snacks"/}
Arguments
Nested parameters are supported, with either => or : separators. They accept
variables, literals and expressions.
{var $entries = (s:collection from: pages, status:is => draft)}{var $entries = (s:collection from: pages, title:contains:Christmas)}{var $entries = (s:collection from: pages, title:contains:$request->title)}
Pagination
Paginated tags return a Laravel paginator. Loop it directly and fetch meta from its built-in methods.
{s:collection:pages as: entries, paginate: 10} {foreach $entries as $entry}{$entry->title}{/foreach} Page {$entries->currentPage()} of {$entries->lastPage()}{/s:collection:pages}
Subexpressions
Wrap a tag in parentheses to use it inline as a plain expression — in {var}, conditions,
filters, foreach, and Latte's n: attributes:
{var $entries = (s:collection from: pages, sort: title)}{if (s:collection:count in: pages) > 1}many{/if}{(s:link to: "snacks")|upper} <li n:foreach="(s:collection from: pages) as $entry">{$entry->title}</li><p n:if="(s:collection:count in: pages) > 1">many</p><a n:attr="href: (s:link to: 'snacks')">Snacks</a>
Tags consuming nested content as input
Some tags transform their tag-pair body instead of returning data (e.g. widont,
obfuscate ). Hand it to the tag via the content: argument.
{s:widont content: $entry->headline /}
Forms
Through the proxy, form:create returns the form's data rather than rendered
markup, so you build the <form> in Latte and loop the fields yourself. Capture
it with as::
{s:form:create as: form, in: contact} <form method="{$form->attrs->method}" action="{$form->attrs->action}"> {foreach $form->fields as $field} <label>{$field->display}</label> <input type="text" name="{$field->name}" value="{$field->value}"> {if $field->error}<span class="error">{$field->error}</span>{/if} {/foreach} <button type="submit">Send</button> </form>{/s:form:create}
Check submission state with the scalar form:success and the boolean
form:errors gate:
{s:form:success in: contact}<p>{$value}</p>{/s:form:success} {s:form:errors in: contact} <p>Please fix the errors below.</p>{/s:form:errors}
To list individual error messages, read them from the form:create capture
($form->errors, or $form->error->{handle} for a field's first error) — the
form:errors pair is a boolean gate here, not an iterator.
Modifiers
Statamic Modifiers can be used as filters in Latte:
<h1>{$title|upper|truncate:50}</h1>
HTML fields
Latte escapes every printed value, which is what you want for a title and wrong for a
markdown field. Fields whose fieldtype renders HTML during augmentation — bard,
markdown, redactor — are recognized and printed as markup, so |noescape is not
needed:
<h1>{$entry->title}</h1> {* text field → escaped, even if it holds <a> *}<div>{$entry->content}</div> {* markdown field → <p>…</p>, as markup *}
Resolving values
Most values are augmented and stringified automatically on print, so you rarely need to unwrap them yourself:
{$title}{$author->name}
As an escape hatch for the cases where you hold a raw Value/LabeledValue object (e.g. when
passing one into a function or comparison), the resolve and r helpers and filter return the
underlying value:
{resolve($author)} or {r($author)}{$author|resolve}
Mixing Latte and Antlers
If you ever need to combine Latte and Antlers code, you can use the antlers tag in your
Latte views to render Antlers code inline. This can be useful for complex built-in tags or quick
prototyping by copy-pasting examples from the docs.
Rendered in Latte: {$title} {antlers} Rendered in Antlers: {{ title }}{/antlers}
Layout
Just like in Antlers templates, the correct layout file will be used based on the data available in your entries and blueprints.
By default, it will look for /resources/views/layout.latte, but you can configure specific entries
and collections to use different layouts instead by setting layout: other_layout on the entry or
collection config file.
Sections & Yields
Use the section and yield tags to define content in one place and output it in
another. They map directly to Antlers' identical tags.
{* layout *} {yield breadcrumbs /} {* template *} {section breadcrumbs} <a href="{$entry->url}">{$entry->title}</a>{/section}
Use the self-closing form {yield 'name' /} when there's no fallback. To provide
default content for when no section was defined, use the paired form:
{yield breadcrumbs} Homepage{/yield}
Sections and yields share Statamic's underlying content store, so they interoperate freely across Latte, Antlers and Blade templates: a section defined in an Antlers partial can be yielded in a Latte layout, and vice versa.
Embeds & Slots
Latte composes templates with {embed}
and {block}: a partial defines
named, fillable regions with {block}, and the embedding template overrides them
inside {embed}.
For parity with the component/slot vocabulary used by Antlers and Blade, {slot} is
provided as an exact alias for {block}. It is a pure synonym — same parsing, same
rendering — so you can use slot terminology on both sides of an embed:
{* partials/figure.latte *} <figure> <img src="{$src}" alt="{$alt}"> <figcaption>{slot caption}Default caption{/slot}</figcaption></figure>
{* template *} {embed file 'partials.figure', src: $image->url, alt: $image->alt} {slot caption}A custom caption{/slot}{/embed}
Because {slot} is identical to {block}, the two are interchangeable everywhere
(including layouts and {extends}) and you can freely mix them. Omitting a slot in the
embed falls back to the default content defined in the partial.
The n:slot attribute is also available (mirroring n:block) and works on both sides:
{* partials/figure.latte *} <figcaption n:ifcontent n:slot="caption">Default caption</figcaption> {* template *} {embed file 'partials.figure'} <figcaption n:slot="caption">A custom caption</figcaption>{/embed}
Conditional content
Iftext
Latte's built-in n:ifcontent omits an element when
it renders no output at all. Empty markup still counts as output, though, so a wrapper around an empty
<p> or a Bard field that rendered nothing but <p> </p> survives.
The iftext tag tests for visible content instead. Tags are stripped from the rendered output before
the emptiness check — think innerText where n:ifcontent is innerHTML:
{* dropped: no text, nothing that renders *}<div n:iftext><p></p><span> </span><!-- note --></div> {* kept: text survives stripping *}<div n:iftext><h2>Hello</h2></div>
Elements that render on their own — images, embeds, form controls — count as content even though they hold no text:
{* both kept *}<figure n:iftext><img src="cat.jpg" alt=""></figure><div n:iftext><form><input type="email"></form></div>
It works as a paired tag too, optionally with {else}:
{iftext} {$page->body}{else} <p>Nothing to show yet.</p>{/iftext}
The elements that count as content on their own are img, picture, svg, video, audio,
iframe, embed, object, canvas, script, hr, table, form, input, button, select,
textarea, progress and meter. Adjust the list in a service provider if your markup needs it:
Caching
Cache
Use the cache tag to cache parts of a view.
{cache for: '10 minutes'} {foreach $stocks as $stock} {$stock->fetchPrice()} {/foreach}{/cache}
Nocache
The nocache tag can be used to exempt part of a view from static caching.
Both caching strategies are supported.
{include 'partials.nav', handle: main} {nocache} {if $logged_in} Welcome back, {$user->name} {else} Hello, Guest! {/if}{/nocache} {block content}{/block}
Nesting
The cache and nocache tags can be nested in either direction. A nocache region inside a
cache block stays dynamic even when the surrounding fragment is served from cache:
{cache} this will be cached {nocache} this will remain dynamic {/nocache} this will also be cached{/cache}
Components
Latte templates support the <x-component> syntax. A single tag resolves at compile time to either
a Latte or a Blade component. In case of a conflict, the Latte template wins.
<x-badge label="New"/><x-alert message={$error}/><x-forms.button type="submit">Go</x-forms.button>
A Latte component is a .latte template under the components/ view directory
(<x-forms.button> → components/forms/button.latte). Its tag is desugared to a native
{embed}, so the template receives the attributes as variables and renders slots as blocks.
Anything without a matching template falls back to a Blade component (class, anonymous or
vendor), rendered at runtime.
Attributes
Attributes can be static strings, dynamic PHP expressions, or bare booleans. For Latte
components they become variables in the template; for Blade components, any attributes not
declared as constructor params flow into the $attributes bag.
<x-button type="submit"/><x-button count={$n}/><x-button label={strtoupper($s)}/><x-button disabled/><x-greeting ...{$props}/>
Backing class (optional)
A Latte component may have a backing class extending Daun\StatamicLatte\Components\Component
for logic. Constructor parameters are filled from the tag's attributes, and data() is spread
into the template's variables. Without a class, a component is just its template (anonymous).
use Daun\StatamicLatte\Components\Component; class Alert extends Component{ public function __construct( public string $type = 'info', ) {} public function data(): array { return [...parent::data(), 'classes' => "alert alert-{$this->type}"]; }}
Slots
Latte components use named and default slots, which compile to {embed} blocks. Fill a named
slot with <x-slot:name> (or <x-slot name="name">); the remaining body fills the default slot.
A slot that is omitted falls back to the {slot …} content defined in the component template, and
slot content is evaluated in the caller's scope.
{* components/alert.latte *}<div class="alert"> <strong>{slot title}Notice{/slot}</strong> {slot default}{/slot}</div> {* usage *}<x-alert> <x-slot:title>Heads up</x-slot:title> Something happened.</x-alert>
Blade components accept a body as the default slot ({{ $slot }}), captured as a pre-rendered
string and echoed directly. Named slots work too: each <x-slot:name> becomes a Blade
ComponentSlot ($name, with isEmpty()/isNotEmpty() and its own $name->attributes).
<x-card> Hello <strong>World</strong></x-card> <x-framed> <x-slot:title class="font-bold">Heads up</x-slot:title> Body content</x-framed>
Control attributes
Latte's n: control attributes work on components:
<x-card n:if="$show">content</x-card> <x-greeting n:foreach="$names as $name" name={$name}/>