code 4862526fa1 Add draft pages (admin-only preview); fix admin panel cache leak
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.
2026-07-14 16:35:31 +00:00

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.php next to any index.twig to supply Twig context data, or return a Response (redirect/JSON/XML/HTML) to short-circuit templating entirely.
  • Static caching, zero config — sidecar-less pages render once and are written to public/cache/; .htaccess serves the cached file directly on every later hit, skipping PHP and Twig entirely.
  • Override-by-pathApp/ (your project) is checked before novaconium/ (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.twig directories 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_url and matomo_site_id in App/config.php to enable tracking site-wide, including automatic 404 tracking. Off by default.
  • Admin authentication — gate every /admin/* route behind HTTP Basic Auth by setting admin_username/admin_password_hash in App/config.php; reusable for any admin page a project adds later, with a /admin/logout link to clear cached credentials and a built-in /admin/password-hash form so generating the hash doesn't require the CLI. Off by default.
  • Draft pages — list a route under draft_routes in App/config.php to 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-theme attribute (persisted to localStorage) that swaps every color via CSS custom properties; both palettes live in App/sass/_colors.sass, same override mechanism as everything else.
  • Self-hosted spam prevention & form validationLib\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 with Lib\FormValidator (accumulating required-field/email/length checks) and Lib\Validate (the underlying validation primitives — email, length, phone, postal/zip, spam-word checks). All three ship in novaconium/lib/, demonstrated on the contact form.
  • Form security by defaultLib\Input, a cleaning accessor for $_POST/$_GET (defense-in-depth against HTML/script injection, not a substitute for parameterized queries), and Lib\Csrf, standalone session-token CSRF protection called directly from a sidecar. Both ship in novaconium/lib/, wired into the contact form, /admin/clear-cache, and /admin/password-hash.
  • SQLite/MySQL database, zero setupLib\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 via php novaconium/bin/migrate.php. SQLite data lives in a project-owned top-level data/ directory, outside both public/ and novaconium/. See /admin/docs/database.
  • Sessions with flash dataLib\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 mechanism Lib\Csrf already uses. See /admin/docs/session.
  • No build step, no Composer — clone it, point Apache (or php -S) at public/, and it runs. Twig is vendored as source; see /admin/docs/upgrading-twig for 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:

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.

S
Description
4LT's PHP framework
Readme MIT 1.1 MiB
1.0.10 Latest
2026-01-27 05:56:17 +00:00
Languages
PHP 37.1%
Twig 34.7%
Sass 22.9%
JavaScript 5.1%
Dockerfile 0.2%