Add typed text pages; serve CSS as compiled-on-demand /css/main.css

Sidecar-less page bundles whose directory ends in a MediaType extension
(.txt/.css/.xml/.json/.js) now render raw (no HTML layout, autoescape off),
are served with that extension's Content-Type, and cache as a flat file
(public/cache/<path>) so Apache serves them directly via a new .htaccess
rule. They're excluded from the content index.

CSS becomes one of these: novaconium/pages/css/main.css/ holds index.twig
({{ sass('main.sass') }}), main.sass, and _colors.sass. App\SassExtension's
sass() Twig function resolves the entry file through the overlay and shells
out to Dart Sass on a cache miss; Lib\SassCompiler wraps the CLI. This
drops the committed App/css/main.css build artifact and the public/css
bind mount. Override styling by copying the whole bundle to App/pages/.

Also ship robots.txt and humans.txt as framework bundles, and drop the
php -S dev router (public/router.php) - Docker/Apache only now.

New: novaconium/src/MediaType.php, SassExtension.php, lib/SassCompiler.php.
Moved: novaconium/sass/ -> novaconium/pages/css/main.css/ (+ App/sass merged).
Docs: AGENTS.md, new /admin/docs/text-pages, and styling/caching/docker/
design-notes/getting-started/project-layout/config docs updated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Q9cAXC9xcXYnmP7qjsnw7
This commit is contained in:
code
2026-09-10 20:58:37 +00:00
co-authored by Claude Sonnet 5
parent 551efc0c36
commit f2724972af
33 changed files with 442 additions and 720 deletions
+52 -31
View File
@@ -50,18 +50,19 @@ vendored as plain source files.
## The two-root split
- **`App/`** — the project: `App/pages/`, `App/lib/` (`Lib\` classes),
`App/config.php`, `App/migrations/`, `App/sass/`. The only directory a
site author is expected to touch.
`App/config.php`, `App/migrations/`. The only directory a site author is
expected to touch.
- **`novaconium/`** — the framework: router/renderer core
(`novaconium/src/`), default pages/libs, vendored Twig, autoloader,
config, bootstrap.
(`novaconium/src/`), default pages/libs (including the `css/main.css/`,
`robots.txt/`, `humans.txt/` bundles), vendored Twig, autoloader, config,
bootstrap.
Routing/rendering resolve against **both roots, `App/` first** (via
`novaconium/src/Overlay.php` for pages, `novaconium/autoload.php` for
`Lib\` classes) — same override-by-presence mechanism used for
`config.php`, Twig's `FilesystemLoader`, and Sass (see below). A project
only lists the config keys it's changing in `App/config.php`; never edit
`novaconium/config.php` directly.
`config.php` and Twig's `FilesystemLoader`. A project only lists the config
keys it's changing in `App/config.php`; never edit `novaconium/config.php`
directly.
**`db_connections` is the one config key that isn't a plain shallow-merge.**
`Lib\Db::config()` (and the duplicate in `bin/migrate.php`) merges it one
@@ -139,19 +140,39 @@ syntax (`{% %}`/`{{ }}`) — highlight.js has no Twig grammar and a
restricted auto-detect still always guesses wrong without this class. Any
new Twig-syntax code sample needs it; PHP/Bash samples don't.
## Sass override quirk
## Typed text pages (CSS, robots.txt, humans.txt, …)
`novaconium/sass/main.sass` does `@use 'colors' as *` with **no**
`_colors.sass` sibling in `novaconium/sass/` — on purpose. Dart Sass
resolves a bare `@use` relative to the importing file's own directory
*before* `--load-path`, so a sibling file would always win and silently
defeat the `App/sass/_colors.sass` override. The framework default lives
at `novaconium/sass/defaults/_colors.sass` instead. Don't move it back.
A sidecar-less page bundle whose **directory segment ends in an extension**
in `App\MediaType::MAP` (`.txt` `.css` `.xml` `.json` `.js`) is a *typed
text page*: `Renderer` renders its `index.twig` with **no** HTML layout and
**autoescaping off**, serves it with that extension's Content-Type, and
`Cache` writes it as a flat file (`public/cache/css/main.css`, not
`.../index.html`). A rewrite in `public/.htaccess` (rule 2b) then serves
that file straight from Apache on every later request — so the extension
list is duplicated there; keep it in sync with `MediaType::MAP`. These
pages are excluded from the content index (`ContentIndexer::isCrawlable()`).
See `/admin/docs/text-pages`.
Every color rule in `main.sass` reads a CSS custom property (`var(--bg)`,
etc.), never a Sass variable directly — required for the runtime dark/light
toggle. Adding a color means adding both the plain and `-light` variable in
**both** `_colors.sass` files and wiring it into both `:root` blocks.
The framework ships three: `novaconium/pages/css/main.css/` (route
`/css/main.css`), `novaconium/pages/robots.txt/`, `novaconium/pages/humans.txt/`.
A project overrides one the normal App-over-novaconium way — put its own
bundle at the same relative path under `App/pages/`.
CSS is the interesting one: `novaconium/pages/css/main.css/` holds
`index.twig` (`{{- sass('main.sass') -}}`), `main.sass`, and `_colors.sass`.
The `sass()` Twig function (`App\SassExtension``Lib\SassCompiler`)
resolves the entry file through the overlay (App first) and shells out to
Dart Sass with that file's own directory as the sole `--load-path`, so
`@use 'colors'` resolves to the sibling `_colors.sass` in whichever copy of
the bundle won. To customise styling, copy the **whole** `css/main.css/`
bundle to `App/pages/css/main.css/` — there's no partial "just
`_colors.sass`" override.
Output is cached like any typed page; run
`php novaconium/bin/clear-cache.php` after editing Sass. Every color rule
reads a CSS custom property (`var(--bg)` …), never a Sass variable directly
— required for the runtime dark/light toggle; adding a color means adding
its plain and `-light` variable in `_colors.sass` and wiring both into the
two `:root` blocks in `main.sass`.
## Input handling
@@ -166,12 +187,14 @@ to be hashed) read `$_POST` directly — see login/users sidecars.
## Running it
Only via the Docker image built from `Dockerfile` (Apache reads
`public/.htaccess` directly — there is no `php -S` dev router). Typically:
```
php -S 127.0.0.1:8000 -t public public/router.php
docker compose up # image + volumes from docker-compose.yml
```
`public/router.php` is dev-only, mimics `public/.htaccess`. There is no
test suite — verification is manual route-by-route (see
There is no test suite — verification is manual route-by-route (see
`/admin/docs/design-notes`'s Verification section). After testing, clear
stray cache with `php novaconium/bin/clear-cache.php` and remove any
test-only debris from `App/lib/`/`App/pages/` — nothing there is gitignored
@@ -190,13 +213,11 @@ except `public/cache/*` and `novaconium/contact-log.txt`.
- `novaconium/bin/` holds standalone CLI entry points
(`php novaconium/bin/<script>.php`) — distinct from
`bootstrap.php`/`autoload.php`/`config.php`, which are only `require`'d.
- CSS compiles from `novaconium/sass/main.sass` (indented syntax) to
`App/css/main.css` (project-owned; docker-compose mounts `App/css` to
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. The Docker image ships Dart Sass standalone
(pinned `DART_SASS_VERSION` in the `Dockerfile`, symlinked to
`/usr/local/bin/sass`), so the same command runs inside the container;
bump that version deliberately, like the PHP base tag. See
`/admin/docs/styling` for the Docker fallback if `sass` isn't installed
locally.
- CSS is a **typed text page** (see that section above), not a build
artifact: the Sass lives in `novaconium/pages/css/main.css/` (override at
`App/pages/css/main.css/`) and is compiled on demand by the `sass()` Twig
function, cached to `public/cache/css/main.css`. There is nothing to
pre-build or commit. The Docker image ships Dart Sass standalone (pinned
`DART_SASS_VERSION` in the `Dockerfile`, symlinked to
`/usr/local/bin/sass`) as a runtime dependency. See `/admin/docs/styling`
and `/admin/docs/text-pages`.