Replace v1 with v2 codebase
Full rewrite: swap out the v1 framework (src/, controllers/, views/, twig/, sass/, skeleton/) for the working v2 codebase from phpproject (App/, novaconium/, public/).
This commit is contained in:
@@ -1,59 +1,240 @@
|
||||
# NovaconiumPHP
|
||||
# AGENTS.md
|
||||
|
||||
A lightweight PHP 8.1+ MVC-ish web framework/CMS toolkit (router + controllers + Twig views + Sass styling), authored by Nick Yeoman (4lt.ca) and distributed via Composer as `4lt/novaconium`. The canonical repo is hosted on a self-hosted Gitea instance (`git.4lt.ca`), not GitHub — do not assume `gh` CLI or GitHub Actions apply here.
|
||||
Context for any coding agent working in this repo — Claude, DeepSeek, or
|
||||
otherwise; this file (and the maintenance rule below) applies regardless of
|
||||
which model or CLI is driving. Full narrative docs live at `/admin/docs`
|
||||
when the app is running (also the *only* place Twig upgrade instructions
|
||||
live now — see `/admin/docs/upgrading-twig`; there's no separate
|
||||
MAINTENANCE.md, keeping one copy in the docs page avoids drift). `README.md`
|
||||
is the GitHub-facing pitch, `novaconium/ISSUES.md` is the roadmap/backlog,
|
||||
and this file is the short, agent-facing version. The original design
|
||||
rationale used to live in a standalone `plan.md`; it's now folded into
|
||||
`/admin/docs/design-notes` (everything in it shipped) and the file was
|
||||
deleted. There used to also be a
|
||||
`GUIDE.md` mirroring `/admin/docs` for offline reading — it was removed to
|
||||
cut a doc copy that had to be kept in sync; `/admin/docs` is the only
|
||||
narrative reference now.
|
||||
|
||||
## Directory map
|
||||
## Documentation is duplicated on purpose — keep all copies in sync
|
||||
|
||||
- `src/` — core framework classes, namespace `Novaconium\` (PSR-4 → `src/`): `novaconium.php` (bootstrap), `Router.php`, `Database.php`, `Session.php`, `Redirect.php`, `Post.php`, `Logger.php`, `MessageHandler.php`, `functions.php`, `twig.php`, and `src/Services/` (`Auth.php`, `TagManager.php`).
|
||||
- `controllers/` — framework-level default controllers (dashboard, pages, messages, settings, auth/, sitemap, 404, init, create_admin, etc.).
|
||||
- `config/routes.php` — framework-level default route definitions, merged with app-level `App/routes.php` at runtime.
|
||||
- `views/` — Twig views mirroring controller names (`controllers/dashboard.php` ↔ `views/dashboard.html.twig`).
|
||||
- `twig/` — shared/partial Twig templates (layout pieces: `master.html.twig`, `nav.html.twig`, `head.html.twig`, `foot.html.twig`, `cp/` control-panel partials, `javascript/`).
|
||||
- `sass/` — framework's default Sass source (indented Sass, not SCSS), 7-1-ish structure: `abstracts/`, `base/`, `framework/`, `controlPanel/`, `coming-soon/`; has its own `Dockerfile` for compiling.
|
||||
- `skeleton/` — scaffold copied into a new consumer project on install: `.env`, `docker-compose.yml`, `novaconium/App/{config.php,routes.php,controllers,views,templates}`, `novaconium/public/{index.php,.htaccess,css,js}`, `novaconium/sass/project.sass`.
|
||||
- `docs/` — one short Markdown file per topic (see below).
|
||||
- `_assets/` — static assets (e.g. logo used in README).
|
||||
Every topic (routing, sidecars, libraries, layouts, caching, SEO, Matomo,
|
||||
admin authentication, styling, project layout, third-party) exists in
|
||||
**two** places: a page under `novaconium/pages/admin/docs/<topic>/index.twig`
|
||||
(the canonical reference), and (for anything a README-reading human needs
|
||||
up front) a mention in `README.md`. This is intentional — `/admin/docs` is
|
||||
for reading against a running instance with no internet needed, and
|
||||
`README.md` is the GitHub-facing pitch — but it means **any agent that
|
||||
changes framework behavior or adds a feature must update both copies in
|
||||
the same change**, not just the one that was open. Concretely, after
|
||||
touching routing/rendering/caching/SEO behavior or adding a new top-level
|
||||
docs topic:
|
||||
|
||||
## Build / run (Docker-only — no host tooling assumed)
|
||||
1. Update (or add) the matching page under
|
||||
`novaconium/pages/admin/docs/<topic>/index.twig`, and if it's a new
|
||||
topic, link it from both `admin/docs/index.twig` and the nav in
|
||||
`admin/docs/_layout/layout.twig`.
|
||||
2. Update `README.md` if the change affects the feature list, getting
|
||||
started steps, or the docs index there.
|
||||
3. Update this file if the change affects a convention an agent needs to
|
||||
know before editing code (not just narrative docs).
|
||||
|
||||
**Agents (Claude Code, opencode, etc.) run inside containers and have no access to Docker.** Do not attempt to run any `docker`/`docker compose` command below — they will fail. These commands are documented for a human maintainer to run on their own host. If you need to verify a change, do so by reading/reasoning about the code, not by invoking Docker.
|
||||
A doc change that only touches one of these copies is incomplete —
|
||||
verify the other copy before considering the task done.
|
||||
|
||||
## What this is
|
||||
|
||||
A dependency-light PHP + Twig micro-framework: directories under `pages/`
|
||||
map directly to URLs (Hugo-style page bundles), optional `index.php`
|
||||
sidecars supply data or short-circuit to a `Response`, and sidecar-less
|
||||
pages get pre-rendered to static HTML on first request and served straight
|
||||
from Apache after that. No Composer, no build step to install — Twig is
|
||||
vendored as plain source files.
|
||||
|
||||
## The two-root split — read this before editing anything under `pages/` or `lib/`
|
||||
|
||||
Everything lives in one of two places:
|
||||
|
||||
- **`App/`** — the actual project: `App/pages/` (routes/content) and
|
||||
`App/lib/` (project's own `Lib\` classes). This is the only directory a
|
||||
site author is expected to touch.
|
||||
- **`novaconium/`** — the framework itself: router/renderer core
|
||||
(`novaconium/src/`), default pages (`novaconium/pages/` — root layout,
|
||||
404, the `/admin` tools), default `Lib\` classes (`novaconium/lib/`),
|
||||
vendored Twig, autoloader, config, bootstrap.
|
||||
|
||||
Routing and rendering resolve against **both roots, in order** —
|
||||
`App/pages/` first, `novaconium/pages/` as fallback — via
|
||||
`novaconium/src/Overlay.php`. Same mechanism for `Lib\` classes:
|
||||
`App/lib/` is checked before `novaconium/lib/` in `novaconium/autoload.php`.
|
||||
Concretely: dropping a file at the same relative path in `App/` overrides
|
||||
the `novaconium/` default; nothing needs to be duplicated for the site to
|
||||
work, since `novaconium/pages/` already supplies a working layout and 404.
|
||||
|
||||
Twig's `FilesystemLoader` is constructed with both paths as an array, so
|
||||
`{% extends %}` / `{% include %}` get this override-then-fallback
|
||||
resolution for free — no custom logic needed there.
|
||||
|
||||
The same override-by-presence pattern applies to `novaconium/config.php`:
|
||||
if `App/config.php` exists, `novaconium/bootstrap.php` and
|
||||
`novaconium/bin/clear-cache.php` shallow-merge it over the framework defaults
|
||||
with `array_merge()`. A project only needs to list the keys it's changing
|
||||
— never edit `novaconium/config.php` directly.
|
||||
|
||||
`config['matomo_url']` / `config['matomo_site_id']` (both default `''`)
|
||||
gate the Matomo tracking script emitted by the root layout — set both via
|
||||
`App/config.php` to enable it, since either being empty disables tracking
|
||||
entirely. `bootstrap.php` normalizes a missing trailing slash on
|
||||
`matomo_url` before passing it to `Renderer`, which exposes `matomo_url`,
|
||||
`matomo_site_id`, and `is_404` as Twig globals (`is_404` is overridden to
|
||||
`true` in the 404 template's local render context by
|
||||
`Renderer::renderNotFound()`, per Twig's local-context-over-global
|
||||
precedence). Any new Twig global added to `Renderer`'s constructor should
|
||||
follow this same pattern: default value, `addGlobal()` call, documented
|
||||
here and in `/admin/docs`.
|
||||
|
||||
`config['site_name']` (default `'My Site'`) is the same pattern — passed to
|
||||
`Renderer` and exposed as the `site_name` Twig global, used by
|
||||
`novaconium/pages/_layout/layout.twig` for the default `title` block,
|
||||
`og:site_name`, and the footer copyright line. Any other hardcoded
|
||||
site-identity string that shows up in a shared template (as opposed to a
|
||||
per-page override) should become a `config.php` key the same way, not stay
|
||||
hardcoded in the template.
|
||||
|
||||
`config['admin_username']` / `config['admin_password_hash']` (username
|
||||
defaults to `'admin'`, password hash defaults to `''`) gate every
|
||||
`/admin/*` route behind HTTP Basic Auth — this replaced the old
|
||||
`docs_enabled` flag entirely (removed); a single gate over all of
|
||||
`/admin/*` (docs included) made a docs-only toggle redundant. Unlike the
|
||||
Twig-global pattern above, the gate itself is enforced in `bootstrap.php`,
|
||||
before rendering: `AdminAuth::requireLogin(...)`
|
||||
(`novaconium/src/AdminAuth.php`) is called once for any resolved route
|
||||
whose path is `admin` or starts with `admin/`. **Any new admin page
|
||||
dropped under `App/pages/admin/` or `novaconium/pages/admin/` is
|
||||
automatically protected — no per-page wiring needed.** `bootstrap.php`
|
||||
also special-cases the literal path `/admin/logout` *before* router
|
||||
resolution — no page exists there — to call `AdminAuth::logout()`, which
|
||||
always issues a fresh 401 so the browser drops its cached credentials
|
||||
(there's no server-side session to invalidate). `Renderer` separately
|
||||
exposes an `admin_auth_enabled` Twig global (true when a password hash is
|
||||
set) so `admin/index.twig` can conditionally show the "Logout" link — this
|
||||
is a derived display flag, not the enforcement mechanism itself, which
|
||||
never depends on Twig. `novaconium/pages/admin/password-hash/` is a
|
||||
built-in `password_hash()` form (no CLI needed) for generating
|
||||
`admin_password_hash` — a normal admin page, so it's covered by the same
|
||||
gate: reachable while no password is set yet (to generate the first
|
||||
one), then protected like everything else under `/admin/*` afterward. It
|
||||
computes and displays the hash per-request only; nothing is persisted or
|
||||
logged. This is a single-user HTTP Basic Auth stopgap, not
|
||||
the full multi-user system tracked in `novaconium/ISSUES.md` ("Admin login & user
|
||||
management"); don't extend this class toward multi-user/session-based
|
||||
auth — that's a separate, larger feature that
|
||||
will replace it.
|
||||
|
||||
## Running it
|
||||
|
||||
**Composer** (per `docs/Composer.md`):
|
||||
```
|
||||
docker run --rm --interactive --tty --volume $PWD:/app composer:latest require 4lt/novaconium
|
||||
docker run --rm --interactive --tty --volume $PWD:/app composer:latest update
|
||||
php -S 127.0.0.1:8000 -t public public/router.php
|
||||
```
|
||||
|
||||
**Sass build** (per `docs/Sass.md`), using `sass/Dockerfile`:
|
||||
```
|
||||
cd sass && docker build -t sass-container .
|
||||
docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app sass-container sass novaconium/sass/project.sass novaconium/public/css/novaconium.css
|
||||
```
|
||||
Compressed variant: add `--style=compressed`, e.g.
|
||||
```
|
||||
docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app sass-container --style=compressed sass/novaconium.sass skeleton/novaconium/public/css/novaconium.css
|
||||
```
|
||||
`public/router.php` is dev-only, mimics `public/.htaccess`. There is no
|
||||
test suite — verification is manual route-by-route (see
|
||||
`/admin/docs/design-notes`'s Verification section for the checklist used
|
||||
after any framework change).
|
||||
After testing, clear stray cache with `php novaconium/bin/clear-cache.php` or
|
||||
POST `/admin/clear-cache`, and remove anything written to `App/lib/` or
|
||||
`App/pages/` that was only for testing an override — nothing here is
|
||||
gitignored except `public/cache/*` and `novaconium/contact-log.txt`, so
|
||||
test debris left in `App/` will otherwise get committed or silently change
|
||||
site behavior for the next person.
|
||||
|
||||
**Dev stack**: `docker compose up -d` from `skeleton/docker-compose.yml` — services: `4lights/corxn:8.5.3` (Apache+PHP), `redis`, `mariadb`, `phpmyadmin`.
|
||||
## Conventions worth knowing
|
||||
|
||||
**Testing a cloned copy without reinstalling via Composer**: see `docs/Dev-Fake_autoload.md` for the fake-autoload dev trick.
|
||||
|
||||
## No test suite / lint / CI
|
||||
|
||||
There is no `phpunit.xml`, no lint config (no PHPCS, `.editorconfig`, ESLint), and no CI pipeline in this repo. Don't hunt for `npm test`/`phpunit`/lint commands — none exist.
|
||||
|
||||
## Conventions
|
||||
|
||||
- PSR-4 autoloading: `Novaconium\` → `src/`.
|
||||
- Controllers and views are name-paired by default (e.g. `controllers/dashboard.php` ↔ `views/dashboard.html.twig`), but this is only a convention — a controller can render any view, they are not required to match.
|
||||
- Sass partials are prefixed `_` (e.g. `_forms.sass`), aggregated per-folder via `index.sass`.
|
||||
- App-level config (`App/config.php`) is a multi-dimensional PHP array: database credentials, `base_url`, `secure_key`, `logfile`, `loglevel`.
|
||||
- Versioning strategy is semantic-versioning (declared in `composer.json` `extra.versioning`).
|
||||
|
||||
## Git workflow
|
||||
|
||||
Single `master` branch (no `main`), tracking a self-hosted Gitea remote. Solo-maintainer, trunk-based, direct commits to `master` with short informal messages — no conventional-commits enforcement, no PR-based tooling assumptions.
|
||||
|
||||
## Further reading
|
||||
|
||||
See `docs/*.md` for per-topic detail: `404.md`, `Composer.md`, `ConfigurationFile.md`, `Dev-Fake_autoload.md`, `Logs.md`, `Messages.md`, `Post.md`, `Redirect.md`, `Sass.md`, `Session.md`, `StyleSheets-sass.md`, `Twig-Views.md`, `docker.md`.
|
||||
- Reserved segments: any path segment starting with `_` (e.g. `_layout/`)
|
||||
or literally named `404` is never routable — `Router::resolve()` 404s on
|
||||
sight, don't try to serve content directly at those paths.
|
||||
- Sidecars (`index.php`) return either an array (Twig context) or a
|
||||
`Response` (redirect/json/xml/html — `novaconium/src/Response.php`).
|
||||
`$params` (route captures) and `$cache` (the `Cache` instance, e.g. for
|
||||
`$cache->clear()`) are both in scope automatically — see
|
||||
`novaconium/src/Renderer.php::runSidecar()`.
|
||||
- No Composer — `novaconium/autoload.php` is a hand-rolled PSR-4 loader.
|
||||
Adding a new framework-core class means adding it under `App\` in
|
||||
`novaconium/src/`; a new `Lib\` class goes in `App/lib/` or
|
||||
`novaconium/lib/` depending on whether it's project- or
|
||||
framework-specific.
|
||||
- `novaconium/bin/` holds standalone CLI entry points meant to be run
|
||||
directly (`php novaconium/bin/<script>.php`) — distinct from
|
||||
`bootstrap.php`/`autoload.php`/`config.php`, which are only ever
|
||||
`require`'d, never invoked directly. `clear-cache.php` and
|
||||
`create-static-page.php` (scaffolds a new page from the `/admin/docs/seo`
|
||||
starter template) both live there; a new CLI tool goes there too.
|
||||
- CSS is compiled from `novaconium/sass/main.sass` (indented syntax) to
|
||||
`public/css/main.css`. `dart-sass` is installed in this environment
|
||||
(Arch: `pacman -S dart-sass`) — after editing Sass source, run:
|
||||
`sass --load-path=App/sass --load-path=novaconium/sass/defaults --no-source-map novaconium/sass/main.sass public/css/main.css`
|
||||
and commit the regenerated `public/css/main.css` (`--no-source-map`
|
||||
avoids a stray `main.css.map` the project doesn't otherwise use). If
|
||||
`sass` isn't available in whatever environment you're in, either run it
|
||||
via Docker — `/admin/docs/styling` has a copy-pasteable
|
||||
Dockerfile that installs the same standalone Dart Sass release used in
|
||||
this environment (`1.101.0`) directly from GitHub, not via npm, plus
|
||||
the `docker build`/`docker run` commands adjusted to this repo's paths
|
||||
— or hand-edit both files in parallel and keep them in sync — that's
|
||||
how the dark/teal theme and the homepage hero/animation
|
||||
styling were originally written before `sass` was installed here.
|
||||
- The Sass color palette follows the same App-over-novaconium override
|
||||
pattern as pages/lib, but with a twist worth understanding before
|
||||
touching it: `novaconium/sass/main.sass` does `@use 'colors' as *`, and
|
||||
its own directory (`novaconium/sass/`) deliberately has **no**
|
||||
`_colors.sass` sibling. Dart Sass resolves a bare `@use` relative to the
|
||||
importing file's own directory *before* consulting `--load-path`
|
||||
entries, so if `novaconium/sass/_colors.sass` existed next to
|
||||
`main.sass`, it would always win regardless of load-path order —
|
||||
silently defeating the override. Keeping the framework default at
|
||||
`novaconium/sass/defaults/_colors.sass` (a different directory) forces
|
||||
resolution through the load path, where `App/sass` (checked first) can
|
||||
actually override it with `App/sass/_colors.sass`. Don't move
|
||||
`defaults/_colors.sass` back next to `main.sass` — it was moved out on
|
||||
purpose, and doing so reintroduces this bug.
|
||||
- Every color rule in `main.sass` reads a CSS custom property
|
||||
(`var(--bg)`, `var(--accent)`, etc.), never a Sass variable directly —
|
||||
that indirection is what makes the dark/light theme toggle possible,
|
||||
since Sass only runs at compile time and can't react to a runtime
|
||||
choice on its own. The two `_colors.sass` files seed `:root` (dark,
|
||||
the default) and `:root[data-theme="light"]` (via `-light`-suffixed
|
||||
variables — `$bg-light`, `$accent-light`, etc., same files, same
|
||||
override mechanism) once at compile time; the toggle button in
|
||||
`novaconium/pages/_layout/nav.twig` flips the `data-theme` attribute on
|
||||
`<html>` at runtime and persists it to `localStorage`.
|
||||
`novaconium/pages/_layout/theme-init.twig` re-applies a saved choice
|
||||
early in `<head>` (before the stylesheet link) to avoid a flash of the
|
||||
wrong theme on load. If you add a new color to the palette, add both
|
||||
the plain and `-light` variable in **both** `_colors.sass` files and
|
||||
wire it into both `:root` blocks in `main.sass` — a color that's only
|
||||
themed in one direction will look wrong after a toggle.
|
||||
- Sidecars should read request data via `Lib\Input::post()`/`::get()`
|
||||
(`novaconium/lib/Input.php`) rather than `$_POST`/`$_GET` directly — it
|
||||
trims, strips tags, and strips null bytes automatically. This is
|
||||
defense-in-depth against HTML/script injection, **not** SQL-injection
|
||||
protection (no string transform makes input safe to concatenate into a
|
||||
query — use PDO prepared statements once a database layer exists); don't
|
||||
add an `sqlSafe()`-style method to `Input`. One documented exception: a
|
||||
field needing an exact, unmodified value (e.g. a password about to be
|
||||
hashed) should read `$_POST` directly instead — see
|
||||
`novaconium/pages/admin/password-hash/index.php`. `Lib\Csrf`
|
||||
(`novaconium/lib/Csrf.php`) is standalone session-token CSRF protection, not
|
||||
wired into `FormValidator` — a sidecar calls `Csrf::verify()` directly.
|
||||
It's the first thing in the framework to start a native PHP session (only
|
||||
lazily, when a form actually calls it), which is otherwise unrelated to
|
||||
`AdminAuth`'s own session-free Basic Auth.
|
||||
- Don't use Twig's `|slice` filter on a **string** (as opposed to an
|
||||
array) — it unconditionally calls PHP's `mb_substr()` with no fallback
|
||||
(`novaconium/vendor/twig/src/Extension/CoreExtension.php`), which
|
||||
hard-requires the `mbstring` extension and will fatal
|
||||
(`Call to undefined function Twig\Extension\mb_substr()`) on a PHP
|
||||
install without it — a real regression this project hit once already,
|
||||
back when `/blog/hello-world` had a sidecar computing an excerpt this
|
||||
way (see the footnote on `App/pages/blog/twig-syntax-guide/index.twig`
|
||||
for the full story). Truncate strings in PHP instead, guarded with
|
||||
`function_exists('mb_substr')` falling back to `substr()`, and pass the
|
||||
already-truncated value into the template.
|
||||
|
||||
Reference in New Issue
Block a user