Ships Blog tags/categories, Internal search, and XML sitemap together as
one shared mechanism, plus a new meta keywords request, rather than three
separate ones - the "worth deciding together" call ISSUES.md made when
XML sitemap was first written.
Content stays in files. Per-page metadata is four Twig blocks in the
root layout, the same override mechanism already used for
title/description/og_* - keywords (rendered), tags/changefreq/priority
(not rendered, harvested only). App\ContentIndexer crawls every routable
page (Overlay::listPageDirs(), new) and pulls each block via
Renderer::renderForIndex() (new) calling Twig's own renderBlock() API,
not regex-parsing .twig source, so overrides and layout inheritance
resolve exactly like a real render. Rendered HTML is stripped and
indexed into a SQLite FTS5 table for search.
Off by default (content_index_enabled) - same posture as Matomo/admin
auth, since this is a real SQLite dependency plenty of sites won't want.
Verified true zero footprint when disabled: no data/novaconium.sqlite
gets created, all three consumer routes 404 like they don't exist.
content_index_auto (default true) reindexes lazily on first stale touch
of a consumer route, never on a normal page view; both that path and the
explicit `php novaconium/bin/index-content.php` share one reindex().
Two real bugs caught by testing, not review: a reentrancy bug where
/search's own sidecar calling ensureFresh() during the crawl triggered a
nested reindex() mid-transaction (fixed with a static re-entrancy guard),
and a wrong PDO constant in the search sidecar. Also fixed two unrelated
pre-existing bugs found while building this: an unescaped {{ }} in
admin/docs/sidecars that fataled Twig on any real render of that page,
and a stale "no database layer yet" claim there and in Lib\Input's
docblock, left over from before Lib\Db shipped.
Lib\Db's migrations_dir now accepts an ordered list of roots, not just
one path, so the content index's schema could ship as a framework
migration (novaconium/migrations/) without colliding with project
migrations in App/migrations/ - the two-root extension point flagged in
AGENTS.md when SQLite groundwork shipped. Migrations are tracked by path
relative to the repo root rather than bare filename so two roots with a
same-named file can't shadow each other.
New consumer routes: novaconium/pages/sitemap.xml/ and
novaconium/pages/search/ (framework defaults), App/pages/blog/tag/[tag]/
(project-owned, since blog/ is project content - the existing hand
-written post array in App/pages/blog/index.php is untouched). Added
tags to the 4 existing blog posts as a real demonstration.
Closes the Blog tags/categories, Internal search, and XML sitemap
backlog items in novaconium/ISSUES.md.
10 KiB
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. - Content index: sitemap, search, tags —
/sitemap.xml, full-text/search(SQLite FTS5), and blog tag browsing all share one crawler that renders every page and harvestskeywords/tags/changefreq/priorityTwig blocks via Twig's ownrenderBlock()— no front-matter, no separate metadata files. Off by default (depends on SQLite); reindexes lazily on demand or viaphp novaconium/bin/index-content.php. See/admin/docs/content-index. - 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. The content index's search (/admin/docs/content-index) additionally needs SQLite's FTS5 extension, bundled with pdo_sqlite on virtually every modern PHP build — only relevant if content_index_enabled is turned on.
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, content index, 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
- Content index
- 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.