Skip to content

Splitting a template into Latte files#

Where each template lives, how the system picks the right one, what flows into it from the presenter, and what you assemble it from.

The three places templates live#

Where What belongs there Example
the themewww/themes/frontend/<theme>/desktop/layouts/ layouts and their parts: header, footer, the wrapper around the content @bazaar_layout_one.latte
the moduleapp/UI/Front/<Module>/Templates/<Presenter>/ the content of one particular page Advert/list.latte
the componentapp/UI/Front/<Module>/Components/<Component>/Templates/ a reusable piece of a listing Default/Style_1.latte

The split follows what changes with the look

The theme holds what a new design rewrites (the page layout). The module holds what the function dictates (what the page prints). That is why the classifieds listing can be restyled without touching PHP, and a new theme need not copy the page contents.

How the system picks a page template#

It looks at two paths in this order and uses the first that exists:

1. theme:  layouts/modules/<module>/presenters/<presenter>/<action>.latte
2. module: app/UI/Front/<Module>/Templates/<Presenter>/<action>.latte

More precisely: the presenter name is split on colons, presenters is inserted before the last part and everything is lower-cased on the first letter. The presenter Front:Bazaar:Advert with action list therefore becomes layouts/modules/front/bazaar/presenters/advert/list.latte.

A theme can override any page

Just put a file on path 1 and the module is not used. Handy when one theme needs to print one particular listing differently — with no branching in PHP anywhere.

When neither exists, you will see both in the error

The 404 message prints both candidate paths in search order. It is not a list of "what is wrong" but of "where you may put that file".

Inside the theme the device variant (desktop) is tried first and desktop again as a fallback — so a mobile variant may have its own file, but need not.

How the system picks a layout#

The layout is NOT in the code — it is stored with the page in the database

Every page has a layout field in the administration and a second field for a logged-in user. The choice goes:

logged in     → loggedLayout ?? layout ?? '@layout.latte'
not logged in → layout ?? '@layout.latte'

It is looked up in the theme's layouts/. So when a page looks different from what you expect, do not start in the code — check which layout it has set.

@layout.latte is a bare skeleton, not a default design

It is Nette's emergency layout. A page that renders with no header and no footer almost certainly has an empty layout field.

Layouts are named after the module and the arrangement — @bazaar_layout_one.latte, @blog_layout_two.latte, @eshop_layout.latte, @homepage.latte. The _one/_two/_three variants differ in the number and side of the sidebars.

Anatomy of a layout#

<!doctype html>
<html lang="{$language->getAlias()}">
    {import "parts/shared/head.latte"}
    {include head}
    <body id="bazaar-page" n:class="$htmlClass ?? '', 'bazaar-page'">
    {snippet page}
        {var $containerClass='container-lg'}
        <div id="toastContainer" class="toast-container toast-container--top-right"></div>
        {include 'parts/bazaar/header/default.latte' containerClass => $containerClass}
        <main class="site-main">
            <section id="content">
                {snippet flashMessagesWrapperSnippet}
                    {include 'parts/shared/main/flash-messages.latte'}
                {/snippet}
                {include beforeContent}
                {include #content}          {* ← the page template lands here *}
            </section>
        </main>
        {include 'parts/shared/footer.latte' containerClass => $containerClass}
    {/snippet}
        {include 'parts/shared/bottom.latte'}
        {block scripts}{include 'parts/shared/scripts.latte'}{/block}
        {block extraScripts}{/block}
    </body>
</html>
Element Meaning
{include #content} where the content block of the page template is inserted
{snippet page} the wrapper used for AJAX redraws
{block scripts} / {block extraScripts} a page can add its own scripts here
parts/shared/ parts shared by every module (head, footer, scripts, messages)
parts/<module>/ parts of a single module (the shop header differs from classifieds)

{import} versus {include}

{import} merely makes the blocks of another file available (it prints nothing), {include} inserts them. That is why the head has both: first {import "parts/shared/head.latte"}, then {include head}.

Anatomy of a page template#

{block canonical}
<link rel="canonical" href="{$canonicalUrl}">
{/block}

{define metaTitle}{$categoryTranslation?->getMetaTitle($lang)}{/define}
{define metaDescription}{$categoryTranslation?->getMetaDescription($lang)}{/define}

{block content}
    {var $categoryTranslation = $category?->getTranslation($lang)}
    <h1>{$categoryTranslation?->getName()}</h1>
    {control advertList, $items}
{/block}
Construct What for
{block content} mandatory — the page content, inserted by the layout
{define metaTitle} and friends meta data; the layout picks them up when they exist
{block canonical} a custom canonical address
{control …} rendering a component

{define} does not print itself

Unlike {block}. Which is exactly what makes it right for meta data: the layout asks for it only where and when it needs it.

What flows into the template from the presenter#

These variables are available without you passing them anywhere:

Variable What it is
$page the page entity from the database
$lang the numeric ID of the language
$language the language entity ($language->getAlias() is cs)
$locale the locale used for translations
$title, $showTitle the page heading and whether it should be printed
$content the page's text content from the administration (HTML)
$metaTitle, $metaDescription, $metaKeywords meta data from the page
$metaData, $ogData supplemented metadata and Open Graph
$cssList CSS files attached to the page
$theme the active theme entity
$defaultUrlParams parameters used when building links
$flashes flash messages (see the trap below)

$lang is an ID, not an alias

$lang is what belongs in getTranslation() and similar methods. Passing 'cs' there raises a type error — and taking the alias off the presenter misses entirely. The alias is $language->getAlias().

Your own data is added by the presenter

Anything else ($items, $category, $searchHighlights…) is assigned by the presenter into $this->template. When a variable is missing in the template, the assignment is missing in the presenter — not in the template.

Components#

A component is rendered with {control name} or {control name, argument}. It finds its own template, in three steps — first hit wins:

1. Templates/<Theme>/<Variant>/Style_1.latte     (exactly this theme and device)
2. Templates/<Theme>/default/Style_1.latte       (this theme, all devices)
3. Templates/Default/Style_1.latte               (the shared default)

A new theme need not copy every component

It inherits Templates/Default/ and overrides only what it wants different. That is why most components in the repository only have the default variant.

Styles and the split into parts#

A component usually offers several shapes — Style_1, Style_2, Style_3 (row, tile, compact listing). It is chosen by an argument when rendering; renderStyle2() resolves to Style_2.latte automatically, so no method has to be written for it.

Repeating pieces inside the styles belong in an Elements/ subfolder:

Templates/Default/
├── Style_1.latte      ← row listing
├── Style_2.latte      ← tile
├── Style_3.latte      ← compact
└── Elements/
    ├── Actions.latte  ← buttons, shared by every style
    ├── Price.latte
    └── UserBadge.latte

Why separate rather than three copies

The price prints the same way in all three styles. Written out three times, a format change gets made in two places out of three — and that kind of bug takes a long time to find, because the page looks right until you switch the style.

A style can be picked by name#

Where a component offers several styles, a name can be used instead of a number — the template then reads {control name:box} rather than {control name, 4}. A number tells nobody what the listing looks like; a name does.

Homepage news (frontBlogIntroList)#

Prints active, already published intros (five at most). Every shape draws the core .list--* listing markup, so a template styles them with the same rules it uses for articles.

Style Call Draws When to use it
Editorial {control frontBlogIntroList:editorial} .list.list--editorial — a grid of cards with a thumbnail on the left, the date above the title and one line of text below A calm band under the header, where the news must not outshout the rest of the page. Small photos are fine.
Box {control frontBlogIntroList:box} .list.list--box — a grid of cards with the image on top, then date, title and text The most shop-like shape. It wants a photo on every item; without one a placeholder image stays.
Carousel {control frontBlogIntroList:carousel}, with a column count {control frontBlogIntroList:carousel, 4} .list.list--carousel — a horizontal carousel with arrows and dots (the template's carousel.js) When there is a lot of news and it should rotate through a single strip. The call parameter (3 by default) feeds --carousel-cols; a section's CSS can still override it.
Hero {control frontBlogIntroList:hero} .list.list--carousel.list--carousel-hero — one full-width card, the photo covers the whole area and the text sits in a gradient over it, switched horizontally The horizontal twin of the vertical carousel. Arrows sit vertically centred at both edges, dots at the bottom centre. Needs a large landscape photo.
Vertical carousel {control frontBlogIntroList} .list.list--carousel-vertical — one full-width card, switched vertically The default shape. One large photo with a caption instead of a listing.
Bootstrap carousel {control frontBlogIntroList:style2} A Bootstrap .carousel with the caption over the photo The older shape, kept for templates that already use it.

A section hides itself when it holds no .list-item

Templates commonly hide the news section with &:not(:has(.list-item)) { display: none; } so an empty listing leaves no hole on the page. The Editorial style, however, draws .list-item--small (that is what _editorial.scss expects), not .list-item — inside such a section it would disappear together with its content. The condition then has to be widened with .list-item--small.

Traps#

Never put |noescape in an attribute

A value prepared as safe HTML is safe for the body of an element. In title= or alt= that same escaping is worthless. Attributes take plain text.

{snippet} has its own variable scope

A variable set with {var} outside a snippet need not exist inside it after a redraw — an AJAX request renders only that fragment. Whatever a snippet needs should be set inside it, or come from the presenter.

A snippet name must be a literal

{snippet "row-$id"} does not compile. Dynamic parts are handled by {snippetArea} and n:snippet on the element of a loop.

AJAX replaces only the inside of a snippet

Attributes of the element the snippet sits on (classes, data-) keep their original values. If the wrapper has to change too, it must be inside.

n:if on a form field throws its value away

The field is not rendered, so it is not submitted either — and saving writes an empty value. Hide the wrapper, not the field itself.

Paths in {include} are relative to the file, not to the root

Moving a template into another folder breaks every nested include it has.

On the front end, flash messages turn into floating toasts

A printed message is moved by a script into #toastContainer and disappears from where it was printed. Looking for it in the DOM where you printed it will not find it.

A component template has no domain-aware translator

Translate in the presenter or in the component's PHP and pass the finished text into the template.

Where to look for what#

I want to change File
the header, footer, page arrangement www/themes/<theme>/desktop/layouts/
what a particular page prints app/UI/Front/<Module>/Templates/<Presenter>/<action>.latte
the look of a repeated listing element …/Components/<Component>/Templates/Default/Style_N.latte
colours, spacing, typography assets/scss/custom/theme styles
what an element should look like the component catalogue