debug config also drives Twig debug + bypasses static cache

When debug is true: enable Twig debug mode (dump(), auto_reload,
DebugExtension), and stop caching sidecar-less pages — Renderer skips
the cache write, the dev router skips serving cache files, and
bootstrap clears stale cache on the first request that reaches PHP.

Also update docs: new Debug mode section on the config page, cache-
opt-out notes (debug + empty index.php sidecar) on the caching page,
and correct the Sass build target to App/css/main.css in AGENTS.md
and the styling page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FZQHKxu24mvojrJDwhWVS
This commit is contained in:
code
2026-09-09 18:30:48 +00:00
co-authored by Claude Sonnet 5
parent 8959aa9040
commit 6ae05da6de
7 changed files with 66 additions and 9 deletions
+3 -2
View File
@@ -191,7 +191,8 @@ except `public/cache/*` and `novaconium/contact-log.txt`.
(`php novaconium/bin/<script>.php`) — distinct from (`php novaconium/bin/<script>.php`) — distinct from
`bootstrap.php`/`autoload.php`/`config.php`, which are only `require`'d. `bootstrap.php`/`autoload.php`/`config.php`, which are only `require`'d.
- CSS compiles from `novaconium/sass/main.sass` (indented syntax) to - CSS compiles from `novaconium/sass/main.sass` (indented syntax) to
`public/css/main.css`: `App/css/main.css` (project-owned; docker-compose mounts `App/css` to
`sass --load-path=App/sass --load-path=novaconium/sass/defaults --no-source-map novaconium/sass/main.sass public/css/main.css` the container's `public/css`, and it's served at `/css/main.css`):
`sass --load-path=App/sass --load-path=novaconium/sass/defaults --no-source-map novaconium/sass/main.sass App/css/main.css`
— commit the regenerated CSS. See `/admin/docs/styling` for a Docker — commit the regenerated CSS. See `/admin/docs/styling` for a Docker
fallback if `sass` isn't installed locally. fallback if `sass` isn't installed locally.
+11 -1
View File
@@ -52,7 +52,17 @@ $matomoUrl = $config['matomo_url'] !== '' ? rtrim($config['matomo_url'], '/') .
$adminAuthEnabled = (bool) $config['admin_auth_enabled']; $adminAuthEnabled = (bool) $config['admin_auth_enabled'];
$cache = new Cache($config['cache_dir']); $cache = new Cache($config['cache_dir']);
$renderer = new Renderer($config['pages_dirs'], $cache, $adminAuthEnabled, $matomoUrl, $config['matomo_site_id'], $config['site_name'], $config['content_index_enabled']);
// In debug mode nothing should be served from the static cache. The dev
// router already skips it; under Apache a pre-existing cache file would be
// served before PHP runs, so drop the whole cache on the first request
// that does reach PHP. Renderer also stops writing new cache files while
// debug is on, so this only ever runs against stale leftovers.
if ($config['debug']) {
$cache->clear();
}
$renderer = new Renderer($config['pages_dirs'], $cache, $adminAuthEnabled, $matomoUrl, $config['matomo_site_id'], $config['site_name'], $config['content_index_enabled'], $config['debug']);
// Every route under /admin/* — clear-cache, docs, users, and any admin // Every route under /admin/* — clear-cache, docs, users, and any admin
// page a project adds later — is gated here, once, rather than in each // page a project adds later — is gated here, once, rather than in each
@@ -15,6 +15,10 @@
<p>Two kinds of route are excluded from the cache unconditionally, regardless of whether they have a sidecar: every <a class="icon-link" href="/admin/docs/drafts">{{ icons.lock() }}draft page</a> and every <code>/admin/*</code> route. Both are gated by the <a class="icon-link" href="/admin/docs/admin-auth">{{ icons.lock() }}admin login</a>, and a cached copy would bypass that check entirely — <code>.htaccess</code> serves a cached file before PHP (and therefore any auth check) ever runs, so a cached admin or draft page would be served to anyone, unauthenticated, forever after the first authenticated view. See <a class="icon-link" href="/admin/docs/drafts">{{ icons.lock() }}Draft pages</a> for the full write-up.</p> <p>Two kinds of route are excluded from the cache unconditionally, regardless of whether they have a sidecar: every <a class="icon-link" href="/admin/docs/drafts">{{ icons.lock() }}draft page</a> and every <code>/admin/*</code> route. Both are gated by the <a class="icon-link" href="/admin/docs/admin-auth">{{ icons.lock() }}admin login</a>, and a cached copy would bypass that check entirely — <code>.htaccess</code> serves a cached file before PHP (and therefore any auth check) ever runs, so a cached admin or draft page would be served to anyone, unauthenticated, forever after the first authenticated view. See <a class="icon-link" href="/admin/docs/drafts">{{ icons.lock() }}Draft pages</a> for the full write-up.</p>
<p>To opt a single page out of caching permanently, give it a sidecar — presence is all that's checked, so an <strong>empty <code>index.php</code></strong> is enough (<code>&lt;?php return [];</code> if you'd rather it read as deliberate). The page then re-renders through PHP/Twig on every request. Reach for this when a sidecar-less page's output actually varies (a current-time stamp, a random pick) but you don't otherwise need sidecar logic.</p>
<p>The whole mechanism is disabled while <a href="/admin/docs/config">debug mode</a> is on: sidecar-less pages are re-rendered on every request and nothing is written to or served from <code>public/cache/</code>. Set <code>'debug' =&gt; false</code> in <code>App/config.php</code> for production so static caching takes effect.</p>
<p>To force a single page to re-render, delete its file under <code>public/cache/</code>. To clear everything at once, there are two equivalent options:</p> <p>To force a single page to re-render, delete its file under <code>public/cache/</code>. To clear everything at once, there are two equivalent options:</p>
<ul> <ul>
@@ -23,6 +23,24 @@ return [
<p>Deleting <code>App/config.php</code> entirely is just as valid as leaving it in place returning an empty array — either way, every setting falls back to <code>novaconium/config.php</code>'s defaults.</p> <p>Deleting <code>App/config.php</code> entirely is just as valid as leaving it in place returning an empty array — either way, every setting falls back to <code>novaconium/config.php</code>'s defaults.</p>
<h2>Debug mode</h2>
<p><code>debug</code> (default <code>true</code> in <code>novaconium/config.php</code>) is a single boolean that turns on everything you want while developing and nothing you want in production. Set <code>'debug' =&gt; false</code> in <code>App/config.php</code> before deploying:</p>
<pre><code>&lt;?php
// App/config.php
return [
'debug' => false,
];</code></pre>
<p>When <code>debug</code> is on:</p>
<ul>
<li><strong>PHP errors are shown.</strong> <code>novaconium/bootstrap.php</code> sets <code>error_reporting(E_ALL)</code> and <code>display_errors</code> — notices, warnings, and fatals render in the response instead of being swallowed. With it off, nothing is changed, so you get whatever <code>php.ini</code> / the server config decides (typically: logged, not displayed).</li>
<li><strong>Twig debug mode is on.</strong> <code>Renderer</code> builds the Twig <code>Environment</code> with <code>debug</code> and <code>auto_reload</code> enabled and registers <code>DebugExtension</code>, so <code>{{ dump(some_var) }}</code> works in templates and edited templates take effect immediately.</li>
<li><strong>The static HTML cache is bypassed.</strong> Sidecar-less pages normally get frozen into <code>public/cache/</code> on first request (see <a href="/admin/docs/caching">Static caching</a>); in debug mode <code>Renderer</code> stops writing those files, the dev router (<code>public/router.php</code>) stops serving them, and <code>bootstrap.php</code> calls <code>$cache-&gt;clear()</code> on the first request that reaches PHP to flush any stale leftovers. Every page renders fresh on every request. Turn <code>debug</code> off and normal caching resumes as pages are re-requested.</li>
</ul>
<h2>Site name</h2> <h2>Site name</h2>
<p><code>site_name</code> (default <code>'My Site'</code>) is used by the root layout as the default page <code>&lt;title&gt;</code> (when a page doesn't override the <code>title</code> block), <code>og:site_name</code>, and the footer copyright line:</p> <p><code>site_name</code> (default <code>'My Site'</code>) is used by the root layout as the default page <code>&lt;title&gt;</code> (when a page doesn't override the <code>title</code> block), <code>og:site_name</code>, and the footer copyright line:</p>
@@ -2,16 +2,16 @@
{% block title %}Styling{% endblock %} {% block title %}Styling{% endblock %}
{% block description %}Sass, indented syntax, compiled to public/css/main.css, with an overridable color palette.{% endblock %} {% block description %}Sass, indented syntax, compiled to App/css/main.css, with an overridable color palette.{% endblock %}
{% block robots %}noindex, nofollow{% endblock %} {% block robots %}noindex, nofollow{% endblock %}
{% block docs_content %} {% block docs_content %}
<h1>Styling (Sass, indented syntax)</h1> <h1>Styling (Sass, indented syntax)</h1>
<p>Source lives in <code>novaconium/sass/main.sass</code> (indented syntax, not SCSS). Compile it with <a href="https://sass-lang.com/dart-sass/">Dart Sass</a>, passing both Sass directories as load paths:</p> <p>Source lives in <code>novaconium/sass/main.sass</code> (indented syntax, not SCSS). Compile it with <a href="https://sass-lang.com/dart-sass/">Dart Sass</a>, passing both Sass directories as load paths. The output goes to <code>App/css/main.css</code> — a project-owned directory that docker-compose mounts onto the container's <code>public/css</code>, served at <code>/css/main.css</code>:</p>
<pre><code>sass --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css</code></pre> <pre><code>sass --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass App/css/main.css</code></pre>
<p>There's no PHP-based Sass compiler in this project (PHP options like <code>scssphp</code> only understand SCSS syntax) — this is a manual/CI build step, not something the app does at runtime.</p> <p>There's no PHP-based Sass compiler in this project (PHP options like <code>scssphp</code> only understand SCSS syntax) — this is a manual/CI build step, not something the app does at runtime.</p>
@@ -40,7 +40,7 @@ ENTRYPOINT ["sass"]</code></pre>
<pre><code>docker build -t novaconium-sass -f Dockerfile.sass . <pre><code>docker build -t novaconium-sass -f Dockerfile.sass .
docker run --rm -v "$(pwd):/usr/src/app" -w /usr/src/app novaconium-sass \ docker run --rm -v "$(pwd):/usr/src/app" -w /usr/src/app novaconium-sass \
--load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css</code></pre> --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass App/css/main.css</code></pre>
<p>See <a href="https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Sass.md">git.4lt.ca/4lt/novaconium/docs/Sass.md</a> for this project's own write-up of the Docker approach in general; the Dockerfile and paths/flags above are this project's own, adjusted to use Dart Sass directly and this repo's current layout.</p> <p>See <a href="https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Sass.md">git.4lt.ca/4lt/novaconium/docs/Sass.md</a> for this project's own write-up of the Docker approach in general; the Dockerfile and paths/flags above are this project's own, adjusted to use Dart Sass directly and this repo's current layout.</p>
+15 -1
View File
@@ -3,6 +3,7 @@
namespace App; namespace App;
use Twig\Environment; use Twig\Environment;
use Twig\Extension\DebugExtension;
use Twig\Loader\FilesystemLoader; use Twig\Loader\FilesystemLoader;
/** /**
@@ -31,11 +32,20 @@ final class Renderer
string $matomoSiteId = '', string $matomoSiteId = '',
string $siteName = 'My Site', string $siteName = 'My Site',
bool $contentIndexEnabled = false, bool $contentIndexEnabled = false,
private readonly bool $debug = false,
) { ) {
$loader = new FilesystemLoader($this->pagesDirs); $loader = new FilesystemLoader($this->pagesDirs);
// 'debug' also drives PHP error display (see bootstrap.php). When on,
// it enables Twig's debug mode too: {{ dump() }} in templates plus
// auto_reload so template edits take effect without clearing cache.
$this->twig = new Environment($loader, [ $this->twig = new Environment($loader, [
'cache' => false, 'cache' => false,
'debug' => $debug,
'auto_reload' => $debug,
]); ]);
if ($debug) {
$this->twig->addExtension(new DebugExtension());
}
$this->twig->addGlobal('admin_auth_enabled', $adminAuthEnabled); $this->twig->addGlobal('admin_auth_enabled', $adminAuthEnabled);
$this->twig->addGlobal('matomo_url', $matomoUrl); $this->twig->addGlobal('matomo_url', $matomoUrl);
$this->twig->addGlobal('matomo_site_id', $matomoSiteId); $this->twig->addGlobal('matomo_site_id', $matomoSiteId);
@@ -85,7 +95,11 @@ final class Renderer
header('Content-Type: text/html; charset=utf-8'); header('Content-Type: text/html; charset=utf-8');
echo $html; echo $html;
if (!$hasSidecar && !$excludeFromCache) { // In debug mode a sidecar-less page is re-rendered on every request
// rather than frozen into public/cache/ — so template/content edits
// show up immediately. bootstrap.php also skips serving any stale
// cached file while debug is on.
if (!$hasSidecar && !$excludeFromCache && !$this->debug) {
$this->cache->write($requestUri, $html); $this->cache->write($requestUri, $html);
} }
} }
+11 -1
View File
@@ -18,8 +18,18 @@ if ($uri !== '/' && (is_file($file) || is_dir($file))) {
return false; // let the built-in server handle real files/dirs (css, cache) return false; // let the built-in server handle real files/dirs (css, cache)
} }
// Mirror .htaccess's "serve the pre-rendered static file, bypassing PHP"
// rule — but skip it when debug is on, so sidecar-less pages render fresh
// on every request (Renderer also stops writing the cache in that mode).
$novaconiumConfig = require __DIR__ . '/../novaconium/config.php';
$appConfigFile = __DIR__ . '/../App/config.php';
$config = is_file($appConfigFile)
? array_merge($novaconiumConfig, require $appConfigFile)
: $novaconiumConfig;
$debug = (bool) $config['debug'];
$cacheFile = __DIR__ . '/cache' . $uri . '/index.html'; $cacheFile = __DIR__ . '/cache' . $uri . '/index.html';
if (is_file($cacheFile)) { if (!$debug && is_file($cacheFile)) {
readfile($cacheFile); readfile($cacheFile);
return true; return true;
} }