Files
novaconium/App/pages/blog/twig-syntax-guide/index.twig
T
code fbd4e92e9f Replace v1 with v2 codebase
Full rewrite: swap out the v1 framework (src/, controllers/, views/,
twig/, sass/, skeleton/) for the working v2 codebase from phpproject
(App/, novaconium/, public/).
2026-07-14 01:58:25 +00:00

114 lines
8.3 KiB
Twig

{% 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 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 %}
<h1>Twig Syntax Guide</h1>
<p>Every page on this site is a Twig template. This post is a quick tour of the syntax that shows up throughout <code>App/pages/</code> and <code>novaconium/pages/</code> — not a full Twig reference (see <a class="icon-link" href="https://twig.symfony.com/doc/3.x/templates.html">{{ icons.external_link() }}the official docs</a> for that), just the parts this framework actually leans on, plus a couple of gotchas learned the hard way<sup><a href="#fn-1">1</a></sup>.</p>
<h2>Output &amp; variables</h2>
<p>Twig prints an expression with <code>{{ '{{ ... }}' }}</code>. Sidecar data, route params, and a handful of framework-provided variables (<code>request_path</code>, <code>layout</code>, <code>site_name</code>) are all just variables in scope:</p>
<pre><code>{% verbatim %}{{ title }}
{{ params.slug }}
{{ post.title }}{% endverbatim %}</code></pre>
<p>Dot notation (<code>post.title</code>) works whether <code>post</code> is an array key or an object property — Twig tries both, so templates don't need to care which.</p>
<h2>Filters</h2>
<p>Filters transform a value with a pipe: <code>{{ '{{ value|filter }}' }}</code>, and chain left to right. A few used on this site:</p>
<table>
<thead>
<tr><th>Filter</th><th>Example</th><th>Does</th></tr>
</thead>
<tbody>
<tr><td><code>default</code></td><td><code>{{ '{{ request_path|default(\'/\') }}' }}</code></td><td>Falls back when a variable is undefined or empty.</td></tr>
<tr><td><code>date</code></td><td><code>{{ '{{ "now"|date("Y") }}' }}</code></td><td>Formats a date — used for the footer's copyright year.</td></tr>
<tr><td><code>e</code> (escape)</td><td><code>{{ '{{ matomo_url|e(\'js\') }}' }}</code></td><td>Escapes for a context other than HTML, here JavaScript string literals.</td></tr>
<tr><td><code>raw</code></td><td><code>{{ '{{ html_string|raw }}' }}</code></td><td>Opts out of autoescaping — see Other elements below.</td></tr>
</tbody>
</table>
<p><strong>One filter to avoid:</strong> <code>|slice</code> on a <em>string</em> (not an array) calls PHP's <code>mb_substr()</code> internally with no fallback — see the footnote<sup><a href="#fn-1">1</a></sup>.</p>
<h2>Control structures</h2>
<p>The two workhorses are <code>{% verbatim %}{% if %}{% endverbatim %}</code> and <code>{% verbatim %}{% for %}{% endverbatim %}</code>:</p>
<pre><code>{% verbatim %}{% if sent %}
&lt;p&gt;Thanks — your message has been sent.&lt;/p&gt;
{% endif %}{% endverbatim %}</code></pre>
<pre><code>{% verbatim %}{% for post in posts %}
&lt;li&gt;&lt;a href="/blog/{{ post.slug }}"&gt;{{ post.title }}&lt;/a&gt;&lt;/li&gt;
{% endfor %}{% endverbatim %}</code></pre>
<p>This exact loop is what renders the <a href="/blog">blog listing page</a> you probably followed a link from to get here.</p>
<h2>Comments</h2>
<p>Anything between <code>{% verbatim %}{# and #}{% endverbatim %}</code> is stripped entirely from the output — unlike an HTML comment, it never reaches the browser:</p>
<pre><code>{% verbatim %}{# Open Graph / Facebook #}{% endverbatim %}</code></pre>
<p>That one's real — it's the comment sitting above the Open Graph block in <code>novaconium/pages/_layout/layout.twig</code>.</p>
<h2>Template inheritance &amp; includes</h2>
<p><code>{% verbatim %}{% extends %}{% endverbatim %}</code> is how every page on this site gets its <code>&lt;html&gt;</code>/<code>&lt;head&gt;</code>/nav/footer for free — a child template only fills in named <code>{% verbatim %}{% block %}{% endverbatim %}</code> slots the parent declares:</p>
<pre><code>{% verbatim %}{% extends layout %}
{% block title %}Blog{% endblock %}
{% block blog_content %}
...
{% endblock %}{% endverbatim %}</code></pre>
<p><code>{% verbatim %}{% include %}{% endverbatim %}</code> pulls in a whole template inline (used for <code>_layout/nav.twig</code> and <code>_layout/matomo.twig</code>), while <code>{% verbatim %}{% import %}{% endverbatim %}</code> pulls in reusable <strong>macros</strong> — parameterized snippets like the icons used throughout this page:</p>
<pre><code>{% verbatim %}{% import '_layout/icons.twig' as icons %}
{{ icons.book() }}{% endverbatim %}</code></pre>
<p>See <a class="icon-link" href="/admin/docs/layouts">{{ icons.book() }}Layouts</a> for how <code>{% verbatim %}{% extends %}{% endverbatim %}</code> resolution walks the <code>App/</code>-over-<code>novaconium/</code> override chain.</p>
<h2>Lists, three ways</h2>
<p>Ordered:</p>
<ol>
<li>Parse the template source into an AST.</li>
<li>Compile the AST into a plain PHP class.</li>
<li>Cache and execute that compiled class (caching is disabled in this project's <code>Renderer</code>, since <code>novaconium/vendor/twig/</code> — the source — is already about as fast to re-parse as anything else here).</li>
</ol>
<p>Unordered:</p>
<ul>
<li><code>{{ '{{ }}' }}</code> — output</li>
<li><code>{% verbatim %}{% %}{% endverbatim %}</code> — tags/control structures</li>
<li><code>{# #}</code> — comments</li>
</ul>
<p>Nested — the three page kinds this framework recognizes:</p>
<ul>
<li>Sidecar-less pages
<ul>
<li>Rendered once, then cached as static HTML</li>
<li>e.g. this page's siblings <code>/</code>, <code>/about</code></li>
</ul>
</li>
<li>Pages with a sidecar
<ul>
<li>Return array (Twig context) or a <code>Response</code></li>
<li>Never cached, since output can vary per request</li>
</ul>
</li>
</ul>
<h2>Other elements</h2>
<p><strong>Whitespace control:</strong> a hyphen inside a tag delimiter — <code>{% verbatim %}{%- -%}{% endverbatim %}</code> or <code>{{ '{{- -}}' }}</code> — 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.</p>
<p><strong>Autoescaping:</strong> Twig HTML-escapes every <code>{{ '{{ }}' }}</code> output by default, so a sidecar can safely return user input (like the contact form's <code>old.name</code>) without it being able to inject markup. The <code>|raw</code> filter opts out where the content is trusted and intentionally HTML — used nowhere on the public site, but by the <code>|raw</code>-free docs pages under <code>/admin/docs</code>.</p>
<p><strong>Functions vs. filters:</strong> <code>block('title')</code> (used throughout this site's SEO blocks to reuse the <code>title</code> block's content for <code>og:title</code>) is a <em>function</em>, called with parentheses, not piped like a filter.</p>
<div class="footnotes">
<ol>
<li id="fn-1">Twig's <code>|slice</code> filter, applied to a string, calls PHP's <code>mb_substr()</code> with no fallback — a hard dependency on the <code>mbstring</code> extension that isn't guaranteed on every PHP install. This project hit that exact fatal error on <code>/blog/hello-world</code> once already, back when that page had a sidecar computing an excerpt this way. The fix: truncate in PHP instead, guarded with <code>function_exists('mb_substr')</code> falling back to plain <code>substr()</code> — see <code>AGENTS.md</code> for the standing rule.</li>
</ol>
</div>
{% endblock %}