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 theme — www/themes/frontend/<theme>/desktop/layouts/ |
layouts and their parts: header, footer, the wrapper around the content | @bazaar_layout_one.latte |
the module — app/UI/Front/<Module>/Templates/<Presenter>/ |
the content of one particular page | Advert/list.latte |
the component — app/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:
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.
Page sections (frontSystemPageSection)#
An editable template block (System › Pages › Sections) is addressed by a key, not by an ID or a slug — the coder writes the key into the template, the editor finds the section under it in the admin:
{control frontSystemPageSection 'homepage-how'} {* Default: <section class="page-section"> with a heading (h2) and the content *}
{control frontSystemPageSection:plain 'cta-providers'} {* content only, no heading and no wrapper — the template keeps its own wrapper *}
The content is HTML from the admin and is printed 1:1 — nothing is composed or trimmed. Only the admin (RBAC) may save it, never visitor input.
A nonexistent or inactive key renders nothing
No error, no placeholder text — the component returns nothing and the template
hides the section as elsewhere with :not(:has(...)). Only a developer spots a
missing seed, in the debug log (channel page-section); production stays silent.
Two sections on one page need multicontrol
Two calls with the same component name would overwrite each other's
$uniqueId. The second (and every further) call therefore goes through the
multiplier with the section key as the distinguisher:
{multicontrol frontSystemPageSection-homepageHow 'homepage-how'}
{multicontrol frontSystemPageSection-ctaProviders:plain 'cta-providers'}
The part after the dash (homepageHow) is only the unique key of the
multiplier instance — the section key is therefore written once more, after
a space, as the real argument.
Instance key without a dash, argument after a space
In Nette a dash also separates the path to a nested component.
frontSystemPageSection-homepage-how looks for the instance homepage and its child
how, and the page fails with "Component with name 'how' does not exist." Write the
instance key as one word (homepageHow). Arguments are separated from the name by a
space; a comma between the name and the first argument does not pass the Latte parser.
A comma belongs only between several arguments.
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 |