fbd4e92e9f
Full rewrite: swap out the v1 framework (src/, controllers/, views/, twig/, sass/, skeleton/) for the working v2 codebase from phpproject (App/, novaconium/, public/).
85 lines
11 KiB
Twig
85 lines
11 KiB
Twig
{% extends 'admin/docs/_layout/layout.twig' %}
|
|
|
|
{% block title %}Design notes{% endblock %}
|
|
|
|
{% block description %}The original design rationale for this framework.{% endblock %}
|
|
|
|
{% block robots %}noindex, nofollow{% endblock %}
|
|
|
|
{% block docs_content %}
|
|
<h1>File-based PHP + Twig micro-router (Hugo-style, static-cached, Apache)</h1>
|
|
|
|
<h2>Context</h2>
|
|
<p>User likes Hugo's file-based routing/pretty-URLs but wants real PHP capability and Twig instead of Markdown. Pages are files on disk (page-bundle style): a directory tree of <code>.twig</code> templates defines the URL structure, and any page needing PHP logic gets an optional sidecar <code>index.php</code> that supplies data or a full custom response (redirect/JSON/XML). Pages <em>without</em> a sidecar are pure/deterministic, so their rendered output is cached to a static <code>.html</code> file and served directly by Apache — PHP is skipped entirely on repeat hits, similar to Hugo's static output but generated lazily on first request instead of an upfront build. Deployment target is Apache only, so <code>.htaccess</code> handles both the cache short-circuit and canonical-URL redirects.</p>
|
|
|
|
<h2>Pages/layouts are overridable, same mechanism as Lib\ overrides</h2>
|
|
<p>Routing and rendering key off an <em>ordered list</em> of roots (<code>App/pages/</code> then <code>novaconium/pages/</code>) and resolve every relative path (a page, a sidecar, a <code>_layout/layout.twig</code>) against that list via <code>Overlay::isFile</code>/<code>isDir</code>/<code>findFile</code>/<code>findWildcardDir</code>, first match wins. <code>novaconium/pages/</code> ships the framework defaults (<code>_layout/layout.twig</code>, <code>404/index.twig</code>); a project doesn't need to duplicate those to have a working site, but overrides either — or any page — just by placing a file at the same relative path under <code>App/pages/</code>. Twig's <code>FilesystemLoader</code> natively accepts an array of paths, so template rendering (including <code>{% verbatim %}{% extends %}{% endverbatim %}</code>) gets the same override-then-fallback resolution for free — no manual proxying required there.</p>
|
|
|
|
<h2>Router (novaconium/src/Router.php) — intentionally simple, no rendering</h2>
|
|
<ol>
|
|
<li>Take <code>$_SERVER['REQUEST_URI']</code>, strip query string, trim slashes, split into segments (root = <code>[]</code>).</li>
|
|
<li>Walk the page roots via <code>Overlay</code>, skipping any directory starting with <code>_</code> when matching (those are reserved, e.g. <code>_layout/</code>, never routable). At each level: exact-name subdirectory first (found in <em>any</em> root), else a <code>[param]</code>-named subdirectory (capturing into <code>params[name]</code>), else no match.</li>
|
|
<li>Returns a <code>Route</code> object: <code>{ dir: string|null (relative path, not absolute), params: array, found: bool }</code>. Never touches Twig, sidecars, or output — that's the Renderer's job. This keeps Router a pure "URL -> relative page path" lookup, easy to unit-test on its own.</li>
|
|
</ol>
|
|
|
|
<h2>Sidecar PHP contract — can return more than an array</h2>
|
|
<p><code>App/pages/**/index.php</code> (optional) returns one of:</p>
|
|
<ul>
|
|
<li><strong>array</strong> → treated as Twig context data (<code>$params</code> is in scope for slug etc.).</li>
|
|
<li><strong>Response object</strong> → short-circuits Twig entirely. <code>novaconium/src/Response.php</code> provides factories: <code>Response::redirect($url, $code = 301)</code>, <code>Response::json($data)</code>, <code>Response::xml($string)</code>, <code>Response::html($string)</code> (raw HTML, bypass Twig but still gets cached like a normal page if desired).</li>
|
|
</ul>
|
|
<p>Renderer checks the sidecar's return type: array → render matched <code>index.twig</code> with that data; <code>Response</code> → emit directly (correct headers/status, no Twig, no layout).</p>
|
|
|
|
<h2>Layout inheritance — walk upward like Hugo</h2>
|
|
<p>Reserved <code>_layout/layout.twig</code> directories are looked up by the Renderer, not baked into Router:</p>
|
|
<ol>
|
|
<li>Starting at the matched page's directory, check for <code>_layout/layout.twig</code> via <code>Overlay</code> (checking <code>App/pages/</code> before <code>novaconium/pages/</code> at every level). If absent, check parent directory, and so on up to the root <code>_layout/layout.twig</code> (<code>App/pages/_layout/layout.twig</code> if present, else <code>novaconium/pages/_layout/layout.twig</code>).</li>
|
|
<li>The resolved layout path is injected into the Twig context as <code>layout</code>, and each <code>index.twig</code> does <code>{% verbatim %}{% extends layout %}{% endverbatim %}</code> — keeps the mechanism transparent in the template rather than magic in the engine.</li>
|
|
</ol>
|
|
|
|
<h2>Static caching (sidecar-less pages only)</h2>
|
|
<ol>
|
|
<li>When Renderer serves a page whose directory has <strong>no</strong> <code>index.php</code>, after rendering the Twig output it also writes it to <code>public/cache/<segments>/index.html</code>.</li>
|
|
<li><code>.htaccess</code> checks <code>public/cache/<REQUEST_URI>/index.html</code> first (<code>RewriteCond ... -f</code>) and serves it directly if present — PHP/Twig never runs again for that route until the cache file is cleared.</li>
|
|
<li>Cache invalidation is manual for now: clearing <code>public/cache/</code> (or a specific subfolder) forces regeneration on next hit. (Simple, matches "no build step" philosophy; can add mtime-based invalidation later if needed.)</li>
|
|
<li>Pages <em>with</em> a sidecar are never cached this way since their output can vary per-request (DB data, params, POST handling, etc.).</li>
|
|
</ol>
|
|
|
|
<h2>Canonical URLs — no trailing slash (SEO)</h2>
|
|
<ul>
|
|
<li>Canonical form is <strong>without</strong> a trailing slash: <code>/about</code>, <code>/blog/hello-world</code>.</li>
|
|
<li><code>.htaccess</code> 301-redirects any URL ending in <code>/</code> (except the bare root <code>/</code>) to the same path without it, before any routing/cache logic runs.</li>
|
|
<li>Router/Renderer internally still key off directory-per-page (<code>pages/about/index.twig</code>), that's just the filesystem convention — it's independent of what the external canonical URL looks like.</li>
|
|
</ul>
|
|
|
|
<h2>Front controller (public/index.php) — kept intentionally light</h2>
|
|
<pre><code><?php
|
|
require __DIR__ . '/../novaconium/bootstrap.php';</code></pre>
|
|
<p>All real logic — autoload registration, config load, <code>Router::resolve()</code>, <code>Renderer::render()</code>, emitting headers/body — lives in <code>novaconium/bootstrap.php</code>, not in <code>public/index.php</code>.</p>
|
|
|
|
<h2>Sass (indented syntax, not SCSS)</h2>
|
|
<ul>
|
|
<li>Source lives in <code>novaconium/sass/</code> (structure in <code>main.sass</code>, default colors in <code>defaults/_colors.sass</code>) using indented syntax. <code>App/sass/_colors.sass</code> overrides the color palette.</li>
|
|
<li>Compiled output goes to <code>public/css/main.css</code>.</li>
|
|
<li>Compilation is a build step (not a PHP runtime concern) — use the <code>sass</code> CLI (Dart Sass, which supports indented syntax) e.g. <code>sass --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css</code>. No PHP Sass library is needed/assumed since PHP-based compilers (scssphp) target SCSS, not indented syntax.</li>
|
|
<li><code>main.sass</code> does <code>@use 'colors' as *</code> but its own directory has no <code>_colors.sass</code> — on purpose, so resolution falls through to the load paths above (<code>App/sass</code> checked first) instead of resolving to a same-directory sibling, which Dart Sass would otherwise prefer regardless of load-path order. Same override-by-presence mechanism used for pages/lib, extended to a non-PHP asset.</li>
|
|
</ul>
|
|
|
|
<h2>Autoloading (no Composer)</h2>
|
|
<p><code>novaconium/autoload.php</code>: <code>spl_autoload_register</code> mapping <code>Twig\</code> → <code>novaconium/vendor/twig/src/</code>, <code>App\</code> → <code>novaconium/src/</code>, and <code>Lib\</code> → checked against <code>App/lib/</code> first, falling back to <code>novaconium/lib/</code> — this is the override mechanism: a project drops a same-named class in <code>App/lib/</code> to replace a framework default without touching <code>novaconium/</code>. Twig vendored manually from a GitHub release zip into <code>novaconium/vendor/twig/</code>.</p>
|
|
|
|
<h2>Verification</h2>
|
|
<ul>
|
|
<li>Configure Apache (or equivalent local Apache setup) with docroot <code>public/</code> and the <code>.htaccess</code> rules active (<code>AllowOverride All</code> / mod_rewrite enabled).</li>
|
|
<li>Visit <code>/about/</code> → confirms 301 redirect to <code>/about</code> (canonical, no trailing slash).</li>
|
|
<li>Visit <code>/about</code> twice → first hit renders via PHP/Twig and writes <code>public/cache/about/index.html</code>; second hit confirms (e.g. via response headers/timing, or temporarily renaming <code>novaconium/bootstrap.php</code>) that Apache served the cached file directly.</li>
|
|
<li>Visit <code>/contact</code> (has a sidecar) → confirms it is NOT cached (no sidecar-less caching), and that a <code>Lib\</code> class (<code>Lib\Mailer</code>) can be called from it. Every post under <code>App/pages/blog/</code> is sidecar-less by contrast — visiting one twice should show the same static-cache behavior as <code>/about</code> above.</li>
|
|
<li>Add a sidecar that returns <code>Response::json([...])</code> → confirms JSON is emitted with correct <code>Content-Type</code>, bypassing Twig.</li>
|
|
<li>Add a sidecar that returns <code>Response::redirect('/somewhere')</code> → confirms a real HTTP redirect happens.</li>
|
|
<li>Confirm <code>App/pages/blog/_layout/layout.twig</code> overrides <code>App/pages/_layout/layout.twig</code> for pages under <code>blog/</code>, and that <code>_layout/</code> directories are never reachable as routes (404 if requested directly).</li>
|
|
<li>Drop a same-named class into <code>App/lib/</code> and confirm it's used instead of the <code>novaconium/lib/</code> default (override mechanism works).</li>
|
|
<li>Delete <code>App/pages/_layout/</code> and <code>App/pages/404/</code> (if present) and confirm the site still renders and 404s using <code>novaconium/pages/</code>'s defaults; then drop a <code>_layout/layout.twig</code> into <code>App/pages/</code> and confirm it takes precedence.</li>
|
|
<li>Run <code>sass --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css</code> and confirm compiled CSS loads on a page.</li>
|
|
</ul>
|
|
{% endblock %}
|