Guide 02 · Developer docs

Template languages: Smarty and Twig

Core dna renders server-side sites with Smarty or with Twig, one engine per site, and treats the two as peers. This guide is the one place both are documented: what they share, each engine's language and the platform's additions and limits, the two side by side, and translation.

L1–L2
Unit 01 · The shared model

Template resolution order

Every template name is looked up in three folders, the site's own first, and the first match wins, in both engines. How one request resolves, how a site replaces a platform template by putting a file at the same path, why there is no frontend segment in the path, and what a missing template looks like.

Who this is for. The theme developer who includes partials, extends a base template or wants to change a screen the platform ships, such as the sign-in page. Afterwards you can predict which file renders for any template name, override a platform template safely, and tell a missing template from a broken one.

Three folders, first match wins

When the platform renders a template, or a template includes or extends another by name, the engine looks for the name in three folders, in this order, and uses the first file it finds:

  1. The site's templates/ folder: your theme repository.
  2. The platform's templates/ folder: the templates every site shares, such as the admin, the default sign-in page and the generated documentation.
  3. The site's virtual files folder (.virtual-files), where the platform keeps files stored through the Files module. It rarely supplies a template; it is here so the order is complete.

Both engines are given the same three folders in the same order, so the chain is identical on a Smarty site and a Twig site. A name is always written from the top of a templates/ folder, without a leading slash: components/card.html, never /templates/components/card.html.

One request, through all three

A visitor opens /login. The route sends the request to the Users module's sign-in controller, whose template is users/login.html. The engine then looks:

StepLooks forOn a site that has not overridden itOn a site that has
1<site>/templates/users/login.htmlNot thereFound: renders
2<platform>/templates/users/login.htmlFound: rendersNever reached
3<site>/.virtual-files/users/login.htmlNever reachedNever reached

A page whose layout names pages/pages/show.html goes through the same three steps, but the platform ships no file by that name, so a site without one reaches the end of the chain with nothing found (below). The layout, not the URL, decided the name; Which template renders a record covers that half.

Overriding a platform template

To replace a template the platform ships, create a file at the same relative path in your site's templates/ folder. Step 1 finds yours, and every other site keeps the platform's. Start from a copy of the platform's file so you keep what it does, then change it. A minimal sign-in page, at templates/users/login.html:

Template code templates/users/login.html
{extends 'includes/frontend/base-template.html'}

{block name='content'}
  <h1>Sign in</h1>
  {include 'includes/frontend/messages/all.html'}
  {if $localPasswordEnabled}
    <form method="POST">
      <label for="username">Username</label>
      <input id="username" type="text" name="username" required autocomplete="username"
             value="{$user->getUsername()|escape}">
      <label for="password">Password</label>
      <input id="password" type="password" name="password" required autocomplete="current-password">
      <button type="submit">Sign in</button>
    </form>
  {/if}
  {foreach $enabledOAuthAuthenticators as $provider => $providerName}
    <a href="{$_url->getFrontendPath('OAuth', 'loginWith', ['provider' => $provider])}">Sign in with {$providerName|escape}</a>
  {/foreach}
{/block}
{% extends 'includes/frontend/base-template.html' %}

{% block content %}
  <h1>Sign in</h1>
  {{ include('includes/frontend/messages/all.html') }}
  {% if localPasswordEnabled %}
    <form method="POST">
      <label for="username">Username</label>
      <input id="username" type="text" name="username" required autocomplete="username"
             value="{{ user.getUsername()|e }}">
      <label for="password">Password</label>
      <input id="password" type="password" name="password" required autocomplete="current-password">
      <button type="submit">Sign in</button>
    </form>
  {% endif %}
  {% for provider, providerName in enabledOAuthAuthenticators %}
    <a href="{{ _url.getFrontendPath('OAuth', 'loginWith', {'provider': provider}) }}">Sign in with {{ providerName|e }}</a>
  {% endfor %}
{% endblock %}

The base template and the messages partial are your site's own. The variables (user, localPasswordEnabled, enabledOAuthAuthenticators) are the ones the sign-in controller assigns, whichever template renders. Registration, sign-in and passwords covers the whole sign-in contract, including passkeys.

Three things come with an override:

  • You maintain it from now on. When the platform changes its version, adding a field or a passkey button, your copy does not change. Note in the file which platform template it replaces, so whoever upgrades the site knows to compare.
  • On a Twig site, overriding is not optional for some screens. The platform's users/login.html is written in Smarty and has no Twig version, so a Twig site that uses the platform's sign-in route provides its own. The screens that do exist in both engines carry a -twig suffix, and a Twig site overrides that file (Two engines, one platform).
  • Error pages are a special case. For errors/not-found.html, errors/fatal-error.html, errors/not-authorized.html and errors/bad-request.html, the platform checks only the site's folder. When the site has the file, it is rendered through the engine with the usual variables; when it does not, the platform sends its own file as plain HTML, without rendering it.

Full names and relative paths

In Smarty, a name that starts with ./ or ../ is resolved against the folder of the file doing the including, and the chain is not searched. That matters when you override one file of a set. The platform's includes/frontend/messages/all.html includes its siblings as ./success.html, ./error.html and ./warning.html, so a site that overrides only success.html sees no change: the platform's all.html still includes the platform's success.html. Override all.html as well, and its ./ includes then find your copies.

Twig has no relative names: ./card.html is looked up as a name in the three folders, and is not found. Write every Twig name in full from the top of templates/.

Neither engine loads a file outside the three folders. Smarty answers an absolute path elsewhere with Smarty Security: not trusted file path, and Twig answers a ../ that climbs out with Looks like you try to load a template outside configured directories.

No frontend segment

A module controller that is not given a template builds its name from the module, the entity and the action: <module>/<entities>/<action>.html, each part in lower case with hyphens. The Pages module's show controller renders pages/pages/show.html, the blog post controller blogs/posts/show.html, and the product description controller products/product-descriptions/show.html. There is no frontend/ in the path, although the controllers live in a Frontend folder of the platform: templates/pages/frontend/pages/show.html is never looked for.

The file you wrote is not the one requested, the request reaches the end of the chain, and what you see is the missing-template behaviour below, not an error that mentions the extra segment. If a template you can see on disk never renders, compare its path letter by letter with the name the layout or controller asks for.

The platform has its own exceptions: the OAuth sign-in, consent and passkey screens are named explicitly as o-auth/frontend/…, so those few overrides do include the segment. Copy the platform's path exactly and you cannot get this wrong.

What a missing template looks like

SmartyTwig
The template a record or controller asked forThe page renders, and its whole body is the text Template not found: pages/pages/show.htmlAn error: Unable to find template "pages/pages/show.html" (looked into: …), listing the three folders
A file named in an include or extendsAn error: Unable to load template 'file:components/card.html'The same Unable to find template error

An error shows the stack trace on a development environment and the site's error page on production (where errors show). The Twig message is the more useful of the two: it lists the folders searched, so a wrong path is visible at once.

Moving or renaming a template

A layout names its template as a plain path, with nothing that follows the file if it moves. Rename or move a file that a layout points at, and every record on that layout reaches the end of the chain. Check which layouts use a template before you move it, and change the layout's template in the same deploy.

Go deeper

Was this page helpful?

Your answer goes to the docs team.