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/).
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
{% extends '_layout/layout.twig' %}
|
||||
|
||||
{% import '_layout/icons.twig' as icons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="blog-layout">
|
||||
<aside>
|
||||
<p>This sidebar exists because <code>App/pages/blog/_layout/layout.twig</code> overrides the site-wide root layout for everything under <code>/blog</code> — the nearest <code>_layout/layout.twig</code> wins, so a subtree can look different without touching the pages themselves.</p>
|
||||
<p><a class="icon-link" href="/admin/docs/layouts">{{ icons.book() }}Layouts docs</a></p>
|
||||
</aside>
|
||||
<article>
|
||||
{% block blog_content %}{% endblock %}
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends layout %}
|
||||
|
||||
{% import '_layout/icons.twig' as icons %}
|
||||
|
||||
{% block title %}Hello, World!{% endblock %}
|
||||
{% block description %}The first post on this blog — a plain sidecar-less page, like every other post here now.{% 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>Hello, World!</h1>
|
||||
|
||||
<p>The first post on this blog — a plain page at <code>App/pages/blog/hello-world/index.twig</code>, sidecar-less like <a class="icon-link" href="/blog/twig-syntax-guide">{{ icons.book() }}the Twig Syntax Guide</a> and the <a class="icon-link" href="/blog/style-guide">{{ icons.book() }}Style Guide</a>. Since it has no sidecar, it's rendered once and served as static HTML from <code>public/cache/blog/hello-world/index.html</code> on every later request — see <a class="icon-link" href="/admin/docs/caching">{{ icons.book() }}Static caching</a>.</p>
|
||||
|
||||
<p>This URL used to be backed by a wildcard <code>App/pages/blog/[slug]/</code> route pulling from a <code>Lib\PostRepository</code> class — a live demo of route-param capture. That mechanism (any single URL segment captured into <code>$params</code>) is still fully supported by the framework; see <a class="icon-link" href="/admin/docs/routing">{{ icons.link() }}Routing</a>. It just isn't what renders this particular post anymore, now that this post is fixed content rather than a stand-in for arbitrary slugs.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
// Every post under App/pages/blog/ is now a plain, sidecar-less directory
|
||||
// with its own index.twig — none of them are driven by a repository or
|
||||
// database, so this listing is just a hand-maintained array pointing at
|
||||
// each one. Add a new entry here whenever a new post directory is added.
|
||||
return [
|
||||
'posts' => [
|
||||
[
|
||||
'slug' => 'hello-world',
|
||||
'title' => 'Hello, World!',
|
||||
'excerpt' => 'The first post on this blog — a plain sidecar-less page, like every other post here now.',
|
||||
],
|
||||
[
|
||||
'slug' => 'second-post',
|
||||
'title' => 'A Second Post',
|
||||
'excerpt' => 'A second post at its own URL, showing that adding a new page under App/pages/blog/ needs nothing but a new directory.',
|
||||
],
|
||||
[
|
||||
'slug' => 'twig-syntax-guide',
|
||||
'title' => 'Twig Syntax Guide',
|
||||
'excerpt' => 'A tour of the Twig syntax used throughout this site — output, filters, control structures, template inheritance, and a few gotchas worth knowing.',
|
||||
],
|
||||
[
|
||||
'slug' => 'style-guide',
|
||||
'title' => 'Style Guide',
|
||||
'excerpt' => "A showcase of this theme's default styling for headings, lists, tables, code, and other common HTML elements.",
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends layout %}
|
||||
|
||||
{% block title %}Blog{% endblock %}
|
||||
{% block description %}All posts on this blog — a working example of a sidecar-driven listing page.{% endblock %}
|
||||
|
||||
{% block robots %}index, follow{% endblock %}
|
||||
{% block canonical %}{{ request_path|default('/') }}{% endblock %}
|
||||
|
||||
{% block og_type %}website{% 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>Blog</h1>
|
||||
<p>Rendered by <code>App/pages/blog/index.php</code> and <code>index.twig</code> — a sidecar that returns a hand-maintained array pointing at each post directory under <code>App/pages/blog/</code>. Because this page has a sidecar, it's never served from the static cache; the list is rebuilt on every request, even though the array itself only changes when a post is added.</p>
|
||||
|
||||
<ul class="post-list">
|
||||
{% for post in posts %}
|
||||
<li>
|
||||
<h2><a href="/blog/{{ post.slug }}">{{ post.title }}</a></h2>
|
||||
<p>{{ post.excerpt }}…</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends layout %}
|
||||
|
||||
{% import '_layout/icons.twig' as icons %}
|
||||
|
||||
{% block title %}A Second Post{% endblock %}
|
||||
{% block description %}A second post at its own URL, showing that adding a new page under App/pages/blog/ needs nothing but a new directory.{% 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>A Second Post</h1>
|
||||
|
||||
<p>A second post at its own URL — <code>App/pages/blog/second-post/index.twig</code>. Just another plain, sidecar-less directory under <code>App/pages/blog/</code>, same as <a class="icon-link" href="/blog/hello-world">{{ icons.book() }}Hello, World!</a> next to it. Adding a new post is nothing more than a new directory with an <code>index.twig</code> — no route to register, no repository entry to add.</p>
|
||||
|
||||
<p>Both posts are listed on <a class="icon-link" href="/blog">{{ icons.book() }}the blog index</a>, built by <code>App/pages/blog/index.php</code> — see <a class="icon-link" href="/admin/docs/routing">{{ icons.link() }}Routing</a> for how a URL maps to a directory in the first place.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,128 @@
|
||||
{% extends layout %}
|
||||
|
||||
{% import '_layout/icons.twig' as icons %}
|
||||
|
||||
{% block title %}Style Guide{% endblock %}
|
||||
{% block description %}A showcase of this theme's default styling for headings, lists, tables, code, and other common HTML elements.{% 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>Style Guide</h1>
|
||||
<p>A plain page, like <a class="icon-link" href="/blog/twig-syntax-guide">{{ icons.book() }}the Twig Syntax Guide</a>, this time showing off the default styling every element on this site gets for free from <code>novaconium/sass/main.sass</code> — no per-page CSS involved. If you've changed <code>App/sass/_colors.sass</code> (see <a class="icon-link" href="/admin/docs/styling">{{ icons.book() }}Styling</a>), this page is the fastest way to see the new palette applied across everything at once.</p>
|
||||
|
||||
<h2>Headings</h2>
|
||||
<h1>Heading level 1</h1>
|
||||
<h2>Heading level 2</h2>
|
||||
<h3>Heading level 3</h3>
|
||||
<h4>Heading level 4</h4>
|
||||
<h5>Heading level 5</h5>
|
||||
<h6>Heading level 6</h6>
|
||||
|
||||
<h2>Paragraph & inline text</h2>
|
||||
<p>A normal paragraph, with <strong>bold</strong>, <em>italic</em>, <code>inline code</code>, and <a href="/">a link</a> mixed in. <small>Small print, like this, is used for captions and secondary detail throughout the site.</small></p>
|
||||
|
||||
<h2>Blockquote</h2>
|
||||
<blockquote>
|
||||
<p>Design is not just what it looks like and feels like. Design is how it works.</p>
|
||||
</blockquote>
|
||||
|
||||
<h2>Lists</h2>
|
||||
<h3>Unordered</h3>
|
||||
<ul>
|
||||
<li>File-based routing</li>
|
||||
<li>Optional sidecars</li>
|
||||
<li>Static caching</li>
|
||||
</ul>
|
||||
<h3>Ordered</h3>
|
||||
<ol>
|
||||
<li>Write a Twig template</li>
|
||||
<li>Optionally add a sidecar</li>
|
||||
<li>Visit the URL</li>
|
||||
</ol>
|
||||
<h3>Nested</h3>
|
||||
<ul>
|
||||
<li>App/
|
||||
<ul>
|
||||
<li>pages/</li>
|
||||
<li>lib/</li>
|
||||
<li>sass/</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>novaconium/
|
||||
<ul>
|
||||
<li>pages/</li>
|
||||
<li>src/</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Table</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Element</th><th>Styled by</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Headings</td><td><code>h1, h2, h3, h4, h5, h6</code></td></tr>
|
||||
<tr><td>Links</td><td><code>a</code>, <code>a:hover</code></td></tr>
|
||||
<tr><td>Code</td><td><code>code</code>, <code>pre</code></td></tr>
|
||||
<tr><td>Tables</td><td><code>table</code>, <code>th</code>, <code>td</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Code</h2>
|
||||
<p>Inline: <code>(new Mailer())->send($old['name'], $old['email'], $old['message']);</code></p>
|
||||
<pre><code>{% verbatim %}{% extends layout %}
|
||||
|
||||
{% block content %}
|
||||
...
|
||||
{% endblock %}{% endverbatim %}</code></pre>
|
||||
|
||||
<h2>Horizontal rule</h2>
|
||||
<p>Above this line:</p>
|
||||
<hr>
|
||||
<p>Below this line.</p>
|
||||
|
||||
<h2>Form elements</h2>
|
||||
<form>
|
||||
<p>
|
||||
<label for="style-guide-example">A label</label><br>
|
||||
<input type="text" id="style-guide-example" placeholder="A text input">
|
||||
</p>
|
||||
<p>
|
||||
<label for="style-guide-textarea">A textarea</label><br>
|
||||
<textarea id="style-guide-textarea" rows="3" placeholder="Some longer text"></textarea>
|
||||
</p>
|
||||
<p>
|
||||
<label for="style-guide-select">A dropdown</label><br>
|
||||
<select id="style-guide-select">
|
||||
<option>Option one</option>
|
||||
<option>Option two</option>
|
||||
<option>Option three</option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
Radio buttons<br>
|
||||
<label><input type="radio" name="style-guide-radio" checked> First choice</label><br>
|
||||
<label><input type="radio" name="style-guide-radio"> Second choice</label><br>
|
||||
<label><input type="radio" name="style-guide-radio"> Third choice</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" checked> A checkbox, too, while we're here</label>
|
||||
</p>
|
||||
<button type="button">A button</button>
|
||||
</form>
|
||||
|
||||
<h2>Icons</h2>
|
||||
<p>{{ icons.home() }} {{ icons.git() }} {{ icons.book() }} {{ icons.link() }} {{ icons.sitemap() }} {{ icons.email() }} {{ icons.search() }} {{ icons.rss() }} {{ icons.tag() }} {{ icons.lock() }} {{ icons.trash() }} {{ icons.external_link() }} {{ icons.menu() }} {{ icons.back_to_top() }} — every inline SVG icon in <code>novaconium/pages/_layout/icons.twig</code>, all inheriting the surrounding text color via <code>currentColor</code>.</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,113 @@
|
||||
{% 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 & 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 %}
|
||||
<p>Thanks — your message has been sent.</p>
|
||||
{% endif %}{% endverbatim %}</code></pre>
|
||||
<pre><code>{% verbatim %}{% for post in posts %}
|
||||
<li><a href="/blog/{{ post.slug }}">{{ post.title }}</a></li>
|
||||
{% 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 & includes</h2>
|
||||
<p><code>{% verbatim %}{% extends %}{% endverbatim %}</code> is how every page on this site gets its <code><html></code>/<code><head></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 %}
|
||||
Reference in New Issue
Block a user