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.
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 |