{% extends layout %} {% import '_layout/icons.twig' as icons %} {% block title %}Twig Syntax Guide{% endblock %} {% block description %}A tour of the Twig syntax used throughout this site — output, filters, control structures, inheritance, and a few gotchas.{% endblock %} {% block robots %}index, follow{% endblock %} {% block tags %}twig, reference{% endblock %} {% block canonical %}{{ request_path|default('/') }}{% endblock %} {% block og_type %}article{% endblock %} {% block og_title %}{{ block('title') }}{% endblock %} {% block og_description %}{{ block('description') }}{% endblock %} {% block og_url %}{{ block('canonical') }}{% endblock %} {% block twitter_card %}summary{% endblock %} {% block twitter_title %}{{ block('title') }}{% endblock %} {% block twitter_description %}{{ block('description') }}{% endblock %} {% block blog_content %}
Every page on this site is a Twig template. This post is a quick tour of the syntax that shows up throughout App/pages/ and novaconium/pages/ — not a full Twig reference (see {{ icons.external_link() }}the official docs for that), just the parts this framework actually leans on, plus a couple of gotchas learned the hard way1.
Twig prints an expression with {{ '{{ ... }}' }}. Sidecar data, route params, and a handful of framework-provided variables (request_path, layout, site_name) are all just variables in scope:
{% verbatim %}{{ title }}
{{ params.slug }}
{{ post.title }}{% endverbatim %}
Dot notation (post.title) works whether post is an array key or an object property — Twig tries both, so templates don't need to care which.
Filters transform a value with a pipe: {{ '{{ value|filter }}' }}, and chain left to right. A few used on this site:
| Filter | Example | Does |
|---|---|---|
default | {{ '{{ request_path|default(\'/\') }}' }} | Falls back when a variable is undefined or empty. |
date | {{ '{{ "now"|date("Y") }}' }} | Formats a date — used for the footer's copyright year. |
e (escape) | {{ '{{ matomo_url|e(\'js\') }}' }} | Escapes for a context other than HTML, here JavaScript string literals. |
raw | {{ '{{ html_string|raw }}' }} | Opts out of autoescaping — see Other elements below. |
One filter to avoid: |slice on a string (not an array) calls PHP's mb_substr() internally with no fallback — see the footnote1.
The two workhorses are {% verbatim %}{% if %}{% endverbatim %} and {% verbatim %}{% for %}{% endverbatim %}:
{% verbatim %}{% if sent %}
<p>Thanks — your message has been sent.</p>
{% endif %}{% endverbatim %}
{% verbatim %}{% for post in posts %}
<li><a href="/blog/{{ post.slug }}">{{ post.title }}</a></li>
{% endfor %}{% endverbatim %}
This exact loop is what renders the blog listing page you probably followed a link from to get here.
Anything between {% verbatim %}{# and #}{% endverbatim %} is stripped entirely from the output — unlike an HTML comment, it never reaches the browser:
{% verbatim %}{# Open Graph / Facebook #}{% endverbatim %}
That one's real — it's the comment sitting above the Open Graph block in novaconium/pages/_layout/layout.twig.
{% verbatim %}{% extends %}{% endverbatim %} is how every page on this site gets its <html>/<head>/nav/footer for free — a child template only fills in named {% verbatim %}{% block %}{% endverbatim %} slots the parent declares:
{% verbatim %}{% extends layout %}
{% block title %}Blog{% endblock %}
{% block blog_content %}
...
{% endblock %}{% endverbatim %}
{% verbatim %}{% include %}{% endverbatim %} pulls in a whole template inline (used for _layout/nav.twig and _layout/matomo.twig), while {% verbatim %}{% import %}{% endverbatim %} pulls in reusable macros — parameterized snippets like the icons used throughout this page:
{% verbatim %}{% import '_layout/icons.twig' as icons %}
{{ icons.book() }}{% endverbatim %}
See {{ icons.book() }}Layouts for how {% verbatim %}{% extends %}{% endverbatim %} resolution walks the App/-over-novaconium/ override chain.
Ordered:
Renderer, since novaconium/vendor/twig/ — the source — is already about as fast to re-parse as anything else here).Unordered:
{{ '{{ }}' }} — output{% verbatim %}{% %}{% endverbatim %} — tags/control structures{# #} — commentsNested — the three page kinds this framework recognizes:
/, /aboutResponseWhitespace control: a hyphen inside a tag delimiter — {% verbatim %}{%- -%}{% endverbatim %} or {{ '{{- -}}' }} — trims adjacent whitespace/newlines from the output. Not heavily used in this project, since the HTML here isn't whitespace-sensitive, but useful when generating something like JSON or plain text from a template.
Autoescaping: Twig HTML-escapes every {{ '{{ }}' }} output by default, so a sidecar can safely return user input (like the contact form's old.name) without it being able to inject markup. The |raw filter opts out where the content is trusted and intentionally HTML — used nowhere on the public site, but by the |raw-free docs pages under /admin/docs.
Functions vs. filters: block('title') (used throughout this site's SEO blocks to reuse the title block's content for og:title) is a function, called with parentheses, not piped like a filter.
|slice filter, applied to a string, calls PHP's mb_substr() with no fallback — a hard dependency on the mbstring extension that isn't guaranteed on every PHP install. This project hit that exact fatal error on /blog/hello-world once already, back when that page had a sidecar computing an excerpt this way. The fix: truncate in PHP instead, guarded with function_exists('mb_substr') falling back to plain substr() — see AGENTS.md for the standing rule.