Lets a page under App/pages/ be previewed by an admin before the public can see it: list its route in draft_routes (App/config.php), checked in bootstrap.php alongside the existing /admin/* gate. Not authenticated -> same plain 404 an unmatched route gets, not a login prompt, so a draft's existence isn't revealed. Authenticated -> renders normally. No separate login flow needed - Basic Auth credentials are scoped to the whole origin/realm, so authenticating once at /admin covers draft URLs too. AdminAuth::isAuthenticated() is extracted out of requireLogin() so the draft gate can reuse the same credential check with a different failure response (404 vs. a 401 challenge). Renderer::render() gains an $excludeFromCache param so a draft without its own sidecar can't get written to the static HTML cache - .htaccess serves a cached file before PHP, and therefore any auth check, ever runs again, so an uncached exception is required, not just the auth gate. While testing this, found the same bug already existed for /admin itself: novaconium/pages/admin/index.twig has no sidecar, so it was already being cached - meaning any admin visiting /admin once caused the panel to be served to everyone, unauthenticated, straight from public/cache/admin/. Fixed in this change by excluding every /admin/* route from the cache the same way, and documented as a standing rule in AGENTS.md: any future mechanism that conditionally hides page content from the public has to make the same check, not just gate the initial request. Closes the "Draft pages (admin-only preview)" backlog item in novaconium/ISSUES.md.
novaconium
A tiny, Hugo-flavored PHP micro-framework. Routes are directories on disk, pages render with Twig, and any page that needs real logic gets an optional PHP "sidecar" file. Pages without a sidecar are pre-rendered once and served as static HTML straight from Apache afterwards. No Composer — Twig is vendored directly into the repo as plain source files.
Features
- File-based routing — a directory under
App/pages/is a route (Hugo-style page bundles). No route table to maintain. [param]segments — a directory literally named[param](e.g.App/pages/products/[id]/) captures any single URL segment into$params['param']for clean URLs, no query strings.- Optional PHP "sidecars" — drop an
index.phpnext to anyindex.twigto supply Twig context data, or return aResponse(redirect/JSON/XML/HTML) to short-circuit templating entirely. - Static caching, zero config — sidecar-less pages render once and are written to
public/cache/;.htaccessserves the cached file directly on every later hit, skipping PHP and Twig entirely. - Override-by-path —
App/(your project) is checked beforenovaconium/(the framework defaults) for every page, layout,Lib\class, and even the Sass color palette (App/sass/_colors.sass). Drop a file at the same relative path to override it; nothing needs duplicating to get a working site. - Layout inheritance —
_layout/layout.twigdirectories are resolved by walking upward from the matched page, so you can override the layout for a whole subtree. - SEO boilerplate out of the box — the default layout ships meta description, canonical link, robots, Open Graph, and Twitter Card tags, all overridable per-page via Twig blocks.
- Built-in Matomo analytics — set
matomo_urlandmatomo_site_idinApp/config.phpto enable tracking site-wide, including automatic 404 tracking. Off by default. - Admin authentication — gate every
/admin/*route behind HTTP Basic Auth by settingadmin_username/admin_password_hashinApp/config.php; reusable for any admin page a project adds later, with a/admin/logoutlink to clear cached credentials and a built-in/admin/password-hashform so generating the hash doesn't require the CLI. Off by default. - Draft pages — list a route under
draft_routesinApp/config.phpto make it visible only to an authenticated admin; anyone else gets a plain 404, not a login prompt. Reuses the admin auth check directly, and is excluded from static caching so a cached copy can't leak the draft to the public. See/admin/docs/drafts. - Dark/light theme toggle — a nav button flips a
data-themeattribute (persisted tolocalStorage) that swaps every color via CSS custom properties; both palettes live inApp/sass/_colors.sass, same override mechanism as everything else. - Self-hosted spam prevention & form validation —
Lib\SpamGuard, a reusable class for any form: a CSS-hidden honeypot field plus a submission-timing check, no external CAPTCHA service, site key, or outbound API call. Pairs withLib\FormValidator(accumulating required-field/email/length checks) andLib\Validate(the underlying validation primitives — email, length, phone, postal/zip, spam-word checks). All three ship innovaconium/lib/, demonstrated on the contact form. - Form security by default —
Lib\Input, a cleaning accessor for$_POST/$_GET(defense-in-depth against HTML/script injection, not a substitute for parameterized queries), andLib\Csrf, standalone session-token CSRF protection called directly from a sidecar. Both ship innovaconium/lib/, wired into the contact form,/admin/clear-cache, and/admin/password-hash. - SQLite/MySQL database, zero setup —
Lib\Db, a thin PDO wrapper (no ORM) supporting multiple named connections open at once — e.g. this site's own SQLite data plus a MySQL connection to a legacy database, usable in the same sidecar — each with its own plain-SQL migration convention, applied automatically on first use or viaphp novaconium/bin/migrate.php. SQLite data lives in a project-owned top-leveldata/directory, outside bothpublic/andnovaconium/. See/admin/docs/database. - Sessions with flash data —
Lib\Session, a thin wrapper around native PHP sessions with CodeIgniter-style flash values (set now, readable on exactly the next request) for post/redirect/GET flows without a query-string flag. Lazy-start, same mechanismLib\Csrfalready uses. See/admin/docs/session. - No build step, no Composer — clone it, point Apache (or
php -S) atpublic/, and it runs. Twig is vendored as source; see/admin/docs/upgrading-twigfor upgrading it.
Getting started
Requirements: PHP 8.1+ (uses readonly constructor-promoted properties) with the pdo_sqlite extension (bundled with PHP, just needs to be enabled — no separate install; add pdo_mysql too if using a MySQL connection), and, for production, Apache with mod_rewrite and AllowOverride All.
Run it locally (no Apache needed)
php -S 127.0.0.1:8000 -t public public/router.php
public/router.php is a dev-only script that mimics the .htaccess rules (canonical redirects + static cache lookup) so you can develop without Apache. It is never used in production — Apache reads public/.htaccess directly.
Visit http://127.0.0.1:8000/ for the static home page, then click around — /about, /blog/hello-world, /contact, and /admin (cache clearing + these same docs, rendered live) are all included as working examples.
Deploy on Apache
Point the vhost's document root at public/, make sure mod_rewrite is enabled and AllowOverride All is set for that directory so public/.htaccess takes effect, and it just works — no build step required.
Starting a new project
Clone this repo and drop its Git history — no Composer scaffold or installer:
git clone --depth 1 <novaconium-repo-url> my-new-project
cd my-new-project
rm -rf .git && git init && git add -A && git commit -m "Initial commit from novaconium template"
Then replace the example content under App/pages/ with your own; leave novaconium/ and public/ alone.
Updating the framework
Since the framework core lives entirely under novaconium/, pick up a new release by overwriting just that directory against a tag and committing the diff:
git clone --depth 1 --branch <release-tag> <novaconium-repo-url> /tmp/nova-update
rm -rf novaconium && cp -r /tmp/nova-update/novaconium ./novaconium && rm -rf /tmp/nova-update
git add novaconium && git commit -m "Update novaconium framework to <release-tag>"
Safe by construction — App/ always overrides novaconium/, so an update can't clobber project customizations. See Getting started for the full write-up.
Add a page
Create a directory under App/pages/ with an index.twig — the directory path is the URL:
App/pages/pricing/index.twig -> /pricing
Add an index.php next to it if the page needs data or logic. See Sidecars in the docs for the full contract, or SEO for a ready-to-paste starter template with every overridable block — or skip the copy-paste and scaffold it:
php novaconium/bin/create-static-page.php blog/my-new-post
Documentation
The full framework documentation — routing, sidecars, libraries, database, session, layouts, static caching, SEO, Matomo analytics, admin authentication, draft pages, styling, project layout, and third-party notices — lives at /admin/docs on any running instance (so it travels with the code, no internet connection needed). Highlights:
- Getting started
- Routing
- Sidecars
- Libraries
- Database
- Session
- Layouts
- Static caching
- SEO
- Matomo
- Admin authentication
- Draft pages
- Styling
- Project layout
- Third-party
AGENTS.md is the short, agent-facing version for coding assistants working in this repo, and novaconium/ISSUES.md is the roadmap/backlog.
Project layout
App/ your project — pages/ (routes), lib/ (Lib\ classes), sass/ (color overrides) — the only directory you're expected to edit
public/ Apache document root — front controller, .htaccess, static cache, compiled CSS
novaconium/ the framework itself — router, renderer, vendored Twig, default pages/lib/sass — not edited per-project
See Project layout for the full tree with every file explained.
Third-party
Twig is vendored in source form under novaconium/vendor/twig/ (no Composer — see /admin/docs/upgrading-twig for how to upgrade it). It's BSD-3-Clause licensed; the full license text ships alongside it at novaconium/vendor/twig/LICENSE.