diff --git a/.gitignore b/.gitignore index 00b9a3f..8402f5b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /graphify-out/ /public/uploads/* !/public/uploads/.gitkeep +.env \ No newline at end of file diff --git a/App/pages/index.twig b/App/pages/index.twig index 6df0d33..9fa5829 100644 --- a/App/pages/index.twig +++ b/App/pages/index.twig @@ -56,8 +56,36 @@

Meta description, canonical links, Open Graph, and Twitter Card tags ship by default, all overridable per page.

-

Matomo & admin auth

-

Built-in analytics tracking and a multi-user session login for /admin/* — both off until you turn them on in App/config.php.

+

Admin authentication

+

A multi-user session login gates every /admin/* route, with email verification for new accounts. Off by default in App/config.php.

+
+
+

Access control & drafts

+

Gate a page to a user or group with one Lib\Access call in its sidecar, or preview an unfinished page as an admin-only draft.

+
+
+

Media manager & comments

+

An upload/browse/delete UI at /admin/media, and Lib\Comments for a moderated comment thread on any page.

+
+
+

Database, zero setup

+

Lib\Db wraps PDO for SQLite or MySQL, multiple named connections at once, migrating automatically on first use.

+
+
+

Search, sitemap & tags

+

One content index backs full-text /search, /sitemap.xml, and blog tag browsing — reindexed lazily, no extra steps.

+
+
+

Blog RSS feed

+

/blog/feed is built from the same hand-written post list App/pages/blog/index.php renders from — no database required.

+
+
+

Matomo & dark/light theme

+

Built-in analytics tracking, off by default, alongside a nav toggle that swaps every color via CSS custom properties.

+
+
+

Form security by default

+

Lib\Csrf, Lib\SpamGuard's honeypot check, and cleaning input accessors — wired into the contact form and every admin form.

Override anything

diff --git a/Dockerfile b/Dockerfile index 05c3eb1..94ab282 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,20 @@ # Arch Linux + Apache + PHP image for running novaconium in production. -# See /admin/docs/docker for volumes, overriding App/ without a rebuild, -# and optional MySQL wiring. +# See /admin/docs/docker for the bind-mounted paths, docker-entrypoint.sh's +# seeding/permissions behavior, and optional MySQL wiring. FROM archlinux:base -RUN pacman -Syu --noconfirm --needed apache php php-apache sqlite \ +# Arch's repos are rolling-release with no version-pinned packages, so +# `pacman -S apache=X.Y` isn't reliably available — point pacman at a dated +# Arch Linux Archive (https://archive.archlinux.org) snapshot instead of the +# live mirror so a rebuild months from now installs the same Apache/PHP/ +# SQLite versions instead of whatever happens to be current that day. Bump +# ARCH_SNAPSHOT deliberately (e.g. to pick up a PHP security release), not +# as a side effect of an unrelated rebuild. +ARG ARCH_SNAPSHOT=2026/07/01 +RUN echo "Server=https://archive.archlinux.org/repos/${ARCH_SNAPSHOT}/\$repo/os/\$arch" \ + > /etc/pacman.d/mirrorlist + +RUN pacman -Syyu --noconfirm --needed apache php php-apache sqlite \ && pacman -Scc --noconfirm # php-apache on Arch is built against mpm_prefork, not httpd's default @@ -15,7 +26,7 @@ RUN sed -i \ -e '/^#LoadModule rewrite_module/s/^#//' \ -e 's#DocumentRoot "/srv/http"#DocumentRoot "/var/www/html/public"#' \ -e 's###' \ - -e 's/^AllowOverride None/AllowOverride All/' \ + -e 's/^\( *\)AllowOverride None$/\1AllowOverride All/' \ /etc/httpd/conf/httpd.conf \ && printf '\nLoadModule php_module modules/libphp.so\nAddHandler php-script .php\nDirectoryIndex index.php index.html\nServerName localhost\n' \ >> /etc/httpd/conf/httpd.conf \ @@ -30,11 +41,24 @@ COPY novaconium/ ./novaconium/ COPY public/ ./public/ COPY App/ ./App/ -# Runtime-writable paths not covered by named volumes in docker-compose.yml. +# Pristine copy of the starter App/, kept outside /var/www/html so +# docker-entrypoint.sh can reseed a bind-mounted (but empty/missing) App/ on +# first start — see docker-entrypoint.sh and /admin/docs/docker. +RUN cp -a App/ /opt/novaconium-app-default/ + +# Runtime-writable paths — cache/uploads/App/data are bind-mounted from the +# host by docker-compose.yml, so docker-entrypoint.sh re-chowns them at +# every container start (a build-time chown only survives on the image +# layer, not on a host bind mount). This chown still covers a fresh +# container with no bind mounts configured at all. RUN mkdir -p public/cache public/uploads data \ && touch novaconium/contact-log.txt \ && chown -R http:http public/cache public/uploads data App novaconium/contact-log.txt +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + EXPOSE 80 +ENTRYPOINT ["docker-entrypoint.sh"] CMD ["httpd", "-D", "FOREGROUND"] diff --git a/README.md b/README.md index 63a7c6c..78fbf61 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,11 @@ For a full tour of what's included — routing, sidecars, caching, admin auth, a ## Getting started -**Requirements:** PHP 8.1+ (uses `readonly` constructor-promoted properties) and, for production, Apache with `mod_rewrite` and `AllowOverride All`. A few optional features (database, content index/search, admin authentication) need the `pdo_sqlite` extension — see `/admin/docs` for details once running. +### Requirements: + +PHP 8.1+ (uses `readonly` constructor-promoted properties) and, for production, Apache with `mod_rewrite` and `AllowOverride All`. A few optional features (database, content index/search, admin authentication) need the `pdo_sqlite` extension — see `/admin/docs` for details once running. + +### Developers Run it locally, no Apache needed: @@ -23,6 +27,12 @@ php -S 127.0.0.1:8000 -t public public/router.php Visit `http://127.0.0.1:8000/` — click around the example pages, then open `http://127.0.0.1:8000/admin/docs` for the complete documentation, rendered live from this same instance. +### Webmasters + +- Clone this repo. +- docker build: ``` docker build -t novaconium . ``` +- docker compose up -d + ## Documentation The full framework documentation lives inside the framework itself, at `/admin/docs` on any running instance — so it travels with the code, no internet connection needed. That's the canonical reference for everything: requirements, running locally, deploying on Apache or Docker, starting a new project, updating the framework, adding a page, routing, sidecars, libraries, database, session, content index, XML sitemap, RSS feeds, layouts, static caching, SEO, Matomo analytics, admin authentication, access control, draft pages, media manager, styling, and project layout. diff --git a/docker-compose.yml b/docker-compose.yml index 635a6c1..0236d17 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,13 @@ services: web: - build: . + image: novaconium-beta ports: - "8080:80" volumes: - - cache:/var/www/html/public/cache - - uploads:/var/www/html/public/uploads - - data:/var/www/html/data - # Uncomment to override the baked-in App/ with a host copy, no rebuild: - # - ./App:/var/www/html/App + - ${VOL_PATH:-/data}/novaconium/cache:/var/www/html/public/cache + - ${VOL_PATH:-/data}/novaconium/uploads:/var/www/html/public/uploads + - ${VOL_PATH:-/data}/novaconium/App:/var/www/html/App + - ${VOL_PATH:-/data}/novaconium/data:/var/www/html/data # Optional — only needed if App/config.php adds a db_connections entry # with driver: mysql. See /admin/docs/database. @@ -21,9 +20,3 @@ services: # MYSQL_ROOT_PASSWORD: change-me # volumes: # - mysql-data:/var/lib/mysql - -volumes: - cache: - uploads: - data: - # mysql-data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..c14e0e2 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Runs once per container start, before Apache — see /admin/docs/docker. +# +# docker-compose.yml bind-mounts App/, public/cache/, public/uploads/, and +# data/ from the host so a project's content/db survive a rebuild and can be +# edited without one. Two problems a plain COPY-at-build-time image can't +# solve on its own: +# +# 1. A bind mount to an empty (or not-yet-created) host directory shadows +# whatever COPY baked into that path in the image, replacing it with +# nothing — Docker does not seed bind mounts from image content the way +# it seeds a fresh named volume. App/ is only ever the docs/starter +# content wanted on host: seed it from the pristine copy stashed +# at build time (/opt/novaconium-app-default) if the mounted dir is +# empty, so `docker compose up` produces a working site on a first run +# with no manual copy step. +# 2. A bind-mounted host directory keeps the host's ownership, not the +# image's — the build-time `chown` in the Dockerfile never applies to +# it. Re-chown the mounted paths to the Apache worker user on every +# start so they're writable regardless of the host-side UID/GID. +set -e + +if [ -z "$(ls -A /var/www/html/App 2>/dev/null)" ]; then + cp -a /opt/novaconium-app-default/. /var/www/html/App/ +fi + +chown -R http:http /var/www/html/public/cache /var/www/html/public/uploads /var/www/html/App /var/www/html/data + +exec "$@" diff --git a/novaconium/pages/admin/docs/docker/index.twig b/novaconium/pages/admin/docs/docker/index.twig index 7cad083..ae5f49d 100644 --- a/novaconium/pages/admin/docs/docker/index.twig +++ b/novaconium/pages/admin/docs/docker/index.twig @@ -2,44 +2,52 @@ {% block title %}Docker{% endblock %} -{% block description %}Running novaconium in a container: the Apache/PHP image, its three volumes, and overriding App/ without a rebuild.{% endblock %} +{% block description %}Running novaconium in a container: the Apache/PHP image, its four bind-mounted paths, and how docker-entrypoint.sh seeds and permissions them.{% endblock %} {% block robots %}noindex, nofollow{% endblock %} {% block docs_content %}

Docker

-

The root Dockerfile builds an Apache + PHP image on an Arch Linux base, with mod_rewrite, AllowOverride All, and pdo_sqlite/pdo_mysql already enabled — nothing else in Getting started's "Deploy on Apache" section needs configuring by hand. docker-compose.yml wires it up with three named volumes so a project's own cache, uploads, and database never live inside a path that gets wiped by the "Updating the framework" workflow.

+

The root Dockerfile builds an Apache + PHP image on an Arch Linux base, with mod_rewrite, AllowOverride All, and pdo_sqlite/pdo_mysql already enabled — nothing else in Getting started's "Deploy on Apache" section needs configuring by hand.

docker compose up --build

Visit http://localhost:8080/.

-

The volumes

+

The bind mounts

+ +

docker-compose.yml bind-mounts four host paths under ${VOL_PATH:-/data}/novaconium/ (override VOL_PATH in the environment to relocate all four at once) so a project's content, uploads, cache, and database live on the host — editable without a rebuild, and untouched by the "Updating the framework" workflow:

    +
  • App/var/www/html/App — the project itself (pages, lib, config, migrations). Edit it on the host; changes show up after docker compose restart web, no rebuild.
  • cachepublic/cache/ — the static HTML page cache (see Static caching). Disposable; clear it from inside the container with docker compose exec web php novaconium/bin/clear-cache.php.
  • -
  • uploadspublic/uploads/ — files uploaded through Media manager (/admin/media). Needs its own volume specifically because public/ is otherwise baked into the image with COPY at build time — without this, an upload would only survive until the next docker compose up --build. Kept separate from the data volume below deliberately, since a project might use MySQL or no database at all and shouldn't have upload storage coupled to the SQLite volume.
  • -
  • datadata/ — holds data/novaconium.sqlite if Database-backed features (admin auth, content index) are enabled. Persists across docker compose down/up as long as you don't pass -v.
  • +
  • uploadspublic/uploads/ — files uploaded through Media manager (/admin/media).
  • +
  • datadata/ — holds data/novaconium.sqlite if Database-backed features (admin auth, content index) are enabled.
-

App/ itself is not a named volume — it's baked into the image with COPY at build time, so docker compose up --build alone produces a working site with no extra steps. A named volume seeded from COPY App/ would only populate once, on first container creation, and would silently go stale on every later rebuild. To edit content without rebuilding, uncomment the bind mount in docker-compose.yml:

+

A bind mount to a host directory shadows whatever the image's COPY put at that path — including with nothing at all, if the host directory doesn't exist yet or is empty. Docker doesn't seed a bind mount from image content the way it seeds a fresh named volume. Two consequences the plain image can't handle by itself, both worked around by docker-entrypoint.sh (the container's ENTRYPOINT, runs once per start before Apache):

-
volumes:
-  - cache:/var/www/html/public/cache
-  - uploads:/var/www/html/public/uploads
-  - data:/var/www/html/data
-  - ./App:/var/www/html/App
- -

A bind mount at the same path as a COPY'd directory shadows the image layer at container start, so a host-side edit under App/pages/ shows up after docker compose restart web — no rebuild, no custom entrypoint logic.

+
    +
  • Empty App/ on first run. The Dockerfile stashes a pristine copy of the baked-in App/ at /opt/novaconium-app-default at build time. If the bind-mounted /var/www/html/App is empty when the container starts, the entrypoint copies that pristine copy in — so a first docker compose up against a fresh, empty ${VOL_PATH}/novaconium/App on the host produces a working site instead of a blank one. It only seeds when the directory is empty, so it never clobbers content you've already put there.
  • +
  • Permissions. A bind mount keeps the host directory's ownership, not the image's — the build-time chown -R http:http in the Dockerfile only applies to the image layer, not to whatever gets mounted over it. The entrypoint re-runs chown -R http:http on all four mounted paths on every container start, so Apache's worker user (http on Arch) can always write to them regardless of the host-side UID/GID — no manual chmod on the host required.
  • +

MySQL

Lib\Db supports MySQL alongside or instead of SQLite (see Database). docker-compose.yml has a commented-out db service and mysql-data volume — uncomment both, add a db_connections entry with driver: mysql and matching credentials in App/config.php, and the app container can reach it at hostname db.

+

Pinned package versions

+ +

Arch's repos are rolling-release, so the Dockerfile doesn't install apache/php/sqlite from the live mirror — it points pacman at a dated Arch Linux Archive snapshot via the ARCH_SNAPSHOT build arg (a YYYY/MM/DD path), so a rebuild next month installs the exact same versions as today instead of whatever's newest that day. To move to a newer snapshot deliberately (e.g. to pick up a PHP security release):

+ +
docker build --build-arg ARCH_SNAPSHOT=2026/08/01 --no-cache -t novaconium-beta .
+ +

--no-cache is worth using any time you change the Dockerfile itself (not just App/ content) — Docker otherwise reuses a cached layer for an unchanged-looking RUN step, and docker-compose.yml here uses image: rather than build:, so docker compose up alone never rebuilds at all; you have to docker build (and re-tag, if needed) yourself first.

+

Arch-specific notes

-

Arch's php-apache package is built against mpm_prefork, not httpd's default mpm_event — the Dockerfile swaps MPMs as part of the build. The Apache/PHP worker user on Arch is http, not Debian's www-data; the Dockerfile chowns the cache/uploads/data/App directories and novaconium/contact-log.txt to http:http at build time so a freshly created named volume (which inherits the image mountpoint's ownership) is writable immediately. If you swap a named volume for a bind mount pointing at a host directory with different ownership, that automatic chown doesn't apply — you may need to adjust permissions on the host side.

+

Arch's php-apache package is built against mpm_prefork, not httpd's default mpm_event — the Dockerfile swaps MPMs as part of the build. The Apache/PHP worker user on Arch is http, not Debian's www-data; both the build-time chown and docker-entrypoint.sh's startup chown (see above) target http:http for this reason.

This is a separate Dockerfile from the one-off Dart Sass build tool described in Styling — that one lives at Dockerfile.sass, a Debian-based image whose only job is running the sass CLI, not serving the app.

{% endblock %} diff --git a/novaconium/sass/main.sass b/novaconium/sass/main.sass index 3c48089..8617fde 100644 --- a/novaconium/sass/main.sass +++ b/novaconium/sass/main.sass @@ -386,7 +386,7 @@ button opacity: 0 animation: fade-in-up 0.5s ease-out forwards - @for $i from 1 through 6 + @for $i from 1 through 12 &:nth-child(#{$i}) animation-delay: #{0.3 + $i * 0.06}s diff --git a/public/css/main.css b/public/css/main.css index bf7a57d..cca9366 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -424,6 +424,24 @@ button:hover { .feature-card:nth-child(6) { animation-delay: 0.66s; } +.feature-card:nth-child(7) { + animation-delay: 0.72s; +} +.feature-card:nth-child(8) { + animation-delay: 0.78s; +} +.feature-card:nth-child(9) { + animation-delay: 0.84s; +} +.feature-card:nth-child(10) { + animation-delay: 0.9s; +} +.feature-card:nth-child(11) { + animation-delay: 0.96s; +} +.feature-card:nth-child(12) { + animation-delay: 1.02s; +} .feature-card h2 { font-size: 1.1rem; margin: 0 0 0.5rem;