summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-19 21:03:03 +0300
committerPaul Buetow <paul@buetow.org>2026-06-19 21:03:03 +0300
commit2cdec0ccd3ec1fdcafdc4f12fb30c7d58c7632b8 (patch)
treeb273182a25d4cdb760d6eb6f31ebad012173178f
parent0213d6345866351b08b34a1991c2cc14a31f9349 (diff)
docs: split README into quick-start + reference pages under docs/
Trim README.md to a quick-start guide (install, init, generate, sync, clean) with a main-flags table and feature-toggle summary, and move the detailed reference material into focused pages under docs/: installation, usage, configuration, generation, publishing, templates. Correctness fixes carried over during the split: - The 'Site generated ... with <URL>' source link lives in the page header bar (header.tmpl), not the footer (footer.tmpl only renders the tarball download). Fixed in docs and the SOURCE_URL code comment. - --sync is a config-backed action and accepts --config PATH / reads ./shuriken.conf; the --config action list now includes it. - --refresh-splash also re-copies the site favicon; documented. - State the Bash 5.1 requirement (enforced by the script) in the docs. - docs/stats-exif-audit.md: the EXIF cache moved to metadata-cache.source.sh and the native-field parser extension was implemented; add a status note and fix the stale module path. bin/shuriken regenerated from the config.source.sh comment change.
-rw-r--r--README.md308
-rwxr-xr-xbin/shuriken4
-rw-r--r--docs/configuration.md84
-rw-r--r--docs/generation.md115
-rw-r--r--docs/installation.md58
-rw-r--r--docs/publishing.md45
-rw-r--r--docs/stats-exif-audit.md14
-rw-r--r--docs/templates.md34
-rw-r--r--docs/usage.md105
-rw-r--r--src/lib/config.source.sh4
10 files changed, 517 insertions, 254 deletions
diff --git a/README.md b/README.md
index 5f2f41c..a24b331 100644
--- a/README.md
+++ b/README.md
@@ -2,261 +2,73 @@
<img src="assets/docs/shuriken-logo.svg" alt="Shuriken logo" width="160">
-shuriken is a Bash script for Unix like operating systems (such as Linux) to generate static web photo albums.
-The resulting static photo album is pure HTML+CSS (without any JavaScript!).
+shuriken is a Bash script for Unix-like operating systems (such as Linux) that
+generates static web photo albums. The resulting album is pure HTML+CSS — no
+JavaScript.
-## Installation
+## Quick start
-Build and install the command, templates, and default config from a source
-checkout with:
-
-```
-just build
-sudo just install
+```sh
+just build # build ./bin/shuriken from src/
+sudo just install # install to /usr/bin, /usr/share/shuriken, /etc/default
+shuriken --init # creates ./shuriken.conf in the current directory
```
-`bin/shuriken` is a committed generated artifact for compatibility with
-existing checkouts and packaging. Its source of truth is `src/shuriken.sh`
-rendered through the `VERSION` value in `Justfile`. Run `just build` after
-changing either file, and use `just check-generated` to verify that the tracked
-script has not drifted. `just test` and `just install` run that drift check
-before rebuilding so stale committed output is not hidden.
-
-`just install` installs `shuriken` to `/usr/bin`, templates and static assets
-to `/usr/share/shuriken`, and the default config to `/etc/default/shuriken`.
-Override paths with `DESTDIR`, `PREFIX`, `BINDIR`, `DATADIR`, or `SYSCONFDIR`
-when packaging or staging an install:
+Edit `shuriken.conf` and point `INCOMING_DIR` at a directory of photos, then:
+```sh
+shuriken --dry-run # preview the planned generation without writing anything
+shuriken --generate # build the album into DIST_DIR (./dist by default)
+shuriken --sync # rsync DIST_DIR/ to each configured SYNC_DESTINATIONS
+shuriken --clean # remove DIST_DIR and leftover staging dirs
```
-DESTDIR="$PWD/pkg" PREFIX=/usr just install
-DESTDIR="$PWD/pkg" PREFIX=/usr just deinstall
-```
-
-`just uninstall` is an alias for `just deinstall`.
-
-ImageMagick must also be installed. The script prefers the
-modern `magick` command and falls back to `convert` when needed.
-
-## Usage
-
-```
-shuriken --init
-shuriken --generate [--config PATH] [OPTIONS]
-shuriken --refresh-splash [--config PATH] [OPTIONS]
-shuriken --sync [--config PATH] [OPTIONS]
-shuriken --dry-run [--config PATH] [OPTIONS]
-shuriken --print-config [--config PATH] [OPTIONS]
-shuriken --clean [--config PATH] [OPTIONS]
-shuriken --version
-```
-
-* `--init` creates `./shuriken.conf` in the current working directory from the
- default config. It refuses to overwrite an existing file.
-* `--generate` builds the static album.
-* `--force` with `--generate` rebuilds from scratch instead of reusing cached
- scaled photos, thumbnails, blurs, or EXIF data from the existing output.
-* `--refresh-splash` rewrites only the generated root splash page.
-* `--sync` publishes the generated output directory to configured rsync
- destinations.
-* `--dry-run` loads the config and overrides, validates the planned generation,
- and prints the effective paths, image count, tarball plan, and generated file
- plan without writing output or running ImageMagick or tar.
-* `--print-config` loads the config and overrides, validates basic config values,
- and prints the effective configuration without writing output, running
- ImageMagick, running tar, cleaning, or initializing.
-* `--clean` removes the configured output directory and any leftover
- `.shuriken.*.staging`/`.backup` directories the generation pipeline created as
- siblings of `DIST_DIR` (e.g. from an interrupted run).
-* `--version` prints the program version.
-* `--config PATH` selects the config file for `--generate`,
- `--refresh-splash`, `--dry-run`, `--print-config`, or `--clean`.
-
-When `--config PATH` is not provided, `--generate`, `--dry-run`,
-`--print-config`, `--refresh-splash`, and `--clean` read `./shuriken.conf`.
-If the file is missing, run `shuriken --init` first.
-
-The config file is a Bash file with assignments such as `INCOMING_DIR`,
-`DIST_DIR`, `TEMPLATE_DIR`, `FAVICON`, `SOURCE_URL`, `TITLE`, `HEIGHT`,
-`THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`,
-`SHUFFLE`, `SPLASH_PAGE`, `STATS_PAGE`, `TARBALL_INCLUDE`, `TAR_TIMEOUT`,
-`SYNC_DELETE`, and `SYNC_DESTINATIONS`.
-
-Before generating, `shuriken` validates the loaded config and command-line
-overrides. It checks required values, positive integer settings, `yes`/`no`
-settings, readable input and template directories, a writable output location,
-and ImageMagick availability. Generation stops before writing album output when
-validation fails.
-
-Only regular files in `INCOMING_DIR` with supported image extensions are
-processed as album images. Supported extensions are `jpg`, `jpeg`, `png`, `webp`,
-and `gif`, matched case-insensitively. Other files, such as `.txt` or `.md`
-notes, are ignored with a warning so generation can continue.
-
-`--dry-run` reports the same `INCOMING_DIR`, `DIST_DIR`, and `TEMPLATE_DIR`
-values that generation would use after applying command-line overrides. Its
-tarball filename uses `<timestamp>` as a placeholder so the output is stable.
-
-`--print-config` writes stable shell-style assignments to stdout in this order:
-`CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `FAVICON`,
-`SOURCE_URL`, `TITLE`, `HEIGHT`,
-`THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`,
-`RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, `STATS_PAGE`, `TARBALL_INCLUDE`,
-`TARBALL_SUFFIX`, `TAR_TIMEOUT`, `TAR_OPTS`, `SYNC_DELETE`,
-`SYNC_DESTINATIONS`, and `ORIGINAL_BASEPATH`. Scalar values use Bash `%q`
-quoting and `TAR_OPTS` and `SYNC_DESTINATIONS` are normalized to Bash array
-assignments, so the output can be parsed by shell tooling. `--quiet` does not
-suppress this output, and `--verbose` does not add human-readable diagnostics to
-it.
-
-Successful generation writes `shuriken.json` into the output directory. This
-metadata records the generator version and timestamp, config source, template
-directory, supported source image and generated file counts, tarball status, and
-effective settings useful for debugging a published album.
-Generation also writes `favicon.ico` into the output directory and the default
-templates link to it. By default this is the bundled shuriken favicon; set
-`FAVICON` in the config or pass `--favicon PATH` to publish your own favicon
-file instead (it is copied in as `favicon.ico`).
-
-The page footer links to the project source ("Site generated ... with
-&lt;URL&gt;"). This defaults to the shuriken.sh repository; set `SOURCE_URL` in
-the config or pass `--source-url URL` to point it at your own album's repository
-instead. The displayed link text is the URL with its scheme removed.
-
-Normal generation preserves reusable generated artifacts from the previous
-`DIST_DIR` while still rerendering HTML, random splash/background choices,
-animation classes, timestamps, and shuffled preview order. Existing scaled
-photos, thumbnails, and blurs are reused from the previous output when the source
-image is unchanged.
-
-The per-photo EXIF `identify` output is cached in a separate `cache/` directory
-created next to `DIST_DIR` (i.e. parallel to `dist/` in the working directory).
-This cache is volatile and safe to delete, is **not** part of the published
-output (it is never written into `DIST_DIR`, so `--sync` does not deploy it), and
-persists across runs even if `DIST_DIR` is removed or rebuilt. Because reading
-EXIF from full-size originals is the slowest part of generation, keeping this
-cache makes regenerating an album dramatically faster: an unchanged photo skips
-`identify` entirely. `--clean` removes `DIST_DIR` (and any leftover staging
-directories) but leaves `cache/` in place; delete `cache/` by hand to force a
-full EXIF rebuild on the next run.
-Pass `--force` with `--generate` to rebuild all generated image artifacts and
-re-read every photo's EXIF from scratch (it clears `cache/` once up front, then
-repopulates it during the run).
+ImageMagick (`magick` or `convert`) and Bash 5.1 or newer are required.
-The following long options override config values:
+## Main flags
-| Option | Config value |
+| Flag | Purpose |
| --- | --- |
-| `--incoming PATH` | `INCOMING_DIR` |
-| `--dist PATH` | `DIST_DIR` |
-| `--template PATH` | `TEMPLATE_DIR` |
-| `--favicon PATH` | `FAVICON` |
-| `--source-url URL` | `SOURCE_URL` |
-| `--title TEXT` | `TITLE` |
-| `--height VALUE` | `HEIGHT` |
-| `--thumbheight VALUE` | `THUMBHEIGHT` |
-| `--maxpreviews N` | `MAXPREVIEWS` |
-| `--image-jobs N` | `IMAGE_JOBS` |
-| `--random-seed VALUE` | `RANDOM_SEED` |
-| `--shuffle` | `SHUFFLE=yes` |
-| `--no-shuffle` | `SHUFFLE=no` |
-| `--splash` | `SPLASH_PAGE=yes` |
-| `--no-splash` | `SPLASH_PAGE=no` |
-| `--stats` | `STATS_PAGE=yes` |
-| `--no-stats` | `STATS_PAGE=no` |
-| `--tarball` | `TARBALL_INCLUDE=yes` |
-| `--no-tarball` | `TARBALL_INCLUDE=no` |
-| `--sync-delete` | `SYNC_DELETE=yes` |
-| `--no-sync-delete` | `SYNC_DELETE=no` |
-
-Pass `--sync-destination DEST` one or more times with `--sync` to override the
-configured sync destinations for that run.
-
-By default, the generated root `index.html` is a no-JavaScript splash page using
-a randomly selected album photo. Set `SPLASH_PAGE=no` or pass `--no-splash` to
-restore the top-level redirect to `page-1.html`.
-
-`shuriken` can also generate a no-JavaScript stats site with EXIF-derived
-insights (camera leaderboard, shooting dates, exposure, dimension, format, and
-decoded-enum breakdowns), reachable from the `Stats` link in the page header bar.
-This is off by default; set `STATS_PAGE=yes` or pass `--stats` to enable it. Every
-row on the stats overview is clickable: each bucket (each camera, ISO, year,
-aperture, orientation, …) is its own filter "mini-album" — a gallery of just the
-matching photos with view pages whose previous/next cycle within that filter.
-
-To keep the album root uncluttered, all of this lives under a `stats/`
-subdirectory: the overview is `stats/index.html` and each mini-album is its own
-directory `stats/<filter>/` (gallery `index.html` plus numbered view pages). Only
-the main album sits in `DIST_DIR` itself. The mini-album pages reuse the album's
-shared `photos/`, `thumbs/`, and `blurs/` assets (only the HTML is per-filter)
-and are rendered in parallel honouring `IMAGE_JOBS`. Set `STATS_PAGE=no` or pass
-`--no-stats` (the default) to skip the whole `stats/` tree and hide the link.
-
-To quickly pick a new random splash photo for an already generated album, run
-`shuriken --refresh-splash`. This rewrites only `DIST_DIR/index.html` using
-the existing `photos` and `blurs` output, so it avoids reprocessing images and
-rerendering album pages. It requires `SPLASH_PAGE=yes`; pass
-`--random-seed VALUE` when you need a repeatable pick.
-
-By default, splash and background photos, animation classes, generated
-timestamps, and `--shuffle` preview order remain non-deterministic. Set
-`RANDOM_SEED` in the config, or pass `--random-seed VALUE`, to make those
-choices repeatable for stable tests or reproducible album builds. Use the same
-seed and inputs to produce the same HTML.
-
-To publish generated output, configure destinations and run `shuriken --sync`:
-
-```
-SYNC_DESTINATIONS=(
- admin@fishfinger.buetow.org:/var/www/htdocs/example.org/
- admin@blowfish.buetow.org:/var/www/htdocs/example.org/
-)
-```
-
-`SYNC_DESTINATIONS` must be a Bash array, even for a single destination (for
-example `SYNC_DESTINATIONS=( '/path/with spaces/' )`). A scalar string is
-rejected with an error, since word-splitting would break destinations that
-contain spaces.
-
-`--sync` runs `rsync -av --delete "$DIST_DIR/" "$destination"` for each
-destination by default. The trailing slash on `DIST_DIR/` means the generated
-contents are copied into the target directory. Set `SYNC_DELETE=no` or pass
-`--no-sync-delete` to omit `--delete`.
-
-`--dry-run`, `--print-config`, and `--refresh-splash` accept the same override
-options as `--generate`. `--clean` accepts the same override options, but only
-`--dist` changes what it removes.
-
-Output is human-readable by default and reports routine generation progress.
-Use `--quiet` to suppress routine progress while still writing errors to stderr.
-Use `--verbose` for extra diagnostics, including the selected config file,
-effective paths, skipped existing files, staging output directory, and tarball
-decisions. If `--quiet` and `--verbose` are repeated or combined, the last output
-flag wins.
-
-ImageMagick photo processing and per-photo HTML template rendering run in
-parallel. The default is `IMAGE_JOBS=3`. Set `IMAGE_JOBS` in the config, or pass
-`--image-jobs N`, to tune the number of concurrent image and template jobs.
-
-Each ImageMagick command is bounded by `IMAGEMAGICK_TIMEOUT=60` seconds, and
-tarball creation is bounded by `TAR_TIMEOUT=120` seconds. Set either config
-value to a positive integer to adjust the limit for large images or archives.
-
-## Example usage
-
-1. Run `shuriken --init`.
-2. Edit `shuriken.conf`. Set `INCOMING_DIR` to the directory containing the
- pictures and adjust `DIST_DIR`, `TITLE`, or template settings as needed.
-3. Run `shuriken --dry-run` to inspect the planned generation.
-4. Run `shuriken --generate` to generate the album.
-5. Run `shuriken --sync` to publish `./dist`, or distribute it manually.
-6. Run `shuriken --clean` to remove the generated output.
-
-## HTML templates
-
-Templates live under `share/templates/default` in the source tree and under the
-installed template directory after installation. The stock default resolves to
-the installed template directory when it exists, and otherwise falls back to the
-source tree's `share/templates/default` when running from a checkout. Copy and
-edit templates, then point `TEMPLATE_DIR` or `--template PATH` at the customized
-directory.
+| `--init` | Create `./shuriken.conf` from the default config (refuses to overwrite). |
+| `--generate` | Build the static album. |
+| `--force` | With `--generate`: rebuild all image artifacts and re-read every EXIF tag from scratch. |
+| `--dry-run` | Load config + overrides, validate, and print the plan without writing output or running ImageMagick/tar. |
+| `--print-config` | Print the effective configuration as shell assignments. |
+| `--refresh-splash` | Rewrite only the root splash page of an already generated album. |
+| `--sync` | Publish `DIST_DIR/` to configured rsync destinations. |
+| `--clean` | Remove `DIST_DIR` and leftover `.shuriken.*.staging`/`.backup` dirs. |
+| `--version` | Print the program version. |
+| `--config PATH` | Select the config file for any config-backed action (default: `./shuriken.conf`). |
+
+Common per-run overrides (see the full reference table in [docs/usage.md](docs/usage.md)):
+
+`--incoming`, `--dist`, `--template`, `--title`, `--height`, `--thumbheight`,
+`--maxpreviews`, `--image-jobs`, `--random-seed`, `--shuffle`/`--no-shuffle`,
+`--splash`/`--no-splash`, `--stats`/`--no-stats`, `--tarball`/`--no-tarball`,
+`--favicon`, `--source-url`, `--sync-destination`, `--sync-delete`/`--no-sync-delete`,
+`--quiet`, `--verbose`.
+
+Feature toggles at a glance:
+
+* **Splash page** (`SPLASH_PAGE=yes`, the default): the root `index.html` is a
+ no-JavaScript splash page using a random album photo. `--no-splash` restores a
+ top-level redirect to `page-1.html`.
+* **Stats site** (`STATS_PAGE=no`, the default): set `--stats` to generate a
+ no-JavaScript EXIF stats site under `stats/` (camera leaderboard, shooting
+ dates, exposure/dimension/format breakdowns), with each bucket as its own
+ clickable filter mini-album.
+* **Reproducible builds**: set `RANDOM_SEED` (or `--random-seed VALUE`) to make
+ splash/background picks, animation classes, timestamps, and shuffle order
+ repeatable.
+
+## Documentation
+
+The quick start above is all you need for a first album. Detailed reference:
+
+* [docs/installation.md](docs/installation.md) — build, install, paths, packaging overrides, requirements.
+* [docs/usage.md](docs/usage.md) — full CLI reference: every action, `--config`, the override-option table, output flags.
+* [docs/configuration.md](docs/configuration.md) — the config file format, every variable, defaults, and validation rules.
+* [docs/generation.md](docs/generation.md) — how generation works: artifact reuse, the EXIF cache, `--force`, splash/stats pages, `--refresh-splash`, reproducibility, parallelism/timeouts, `shuriken.json`, favicon, source URL.
+* [docs/publishing.md](docs/publishing.md) — publishing with `--sync`, `SYNC_DESTINATIONS`, `SYNC_DELETE`, and the rsync command.
+* [docs/templates.md](docs/templates.md) — HTML template layout and customization.
+* [docs/stats-exif-audit.md](docs/stats-exif-audit.md) — EXIF field coverage audit behind the stats site (historical design record). \ No newline at end of file
diff --git a/bin/shuriken b/bin/shuriken
index 67779b0..144b4b9 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -4709,10 +4709,10 @@ apply_config_defaults() {
ORIGINAL_BASEPATH="${ORIGINAL_BASEPATH:-}"
RANDOM_SEED="${RANDOM_SEED:-}"
SHUFFLE="${SHUFFLE:-no}"
- # SOURCE_URL is the project/source link shown in the page footer ("Site
+ # SOURCE_URL is the project/source link shown in the page header bar ("Site
# generated ... with <SOURCE_URL>"). Defaults to the shuriken.sh repo;
# override it per site (e.g. to the album's own repo) via config or
- # --source-url. The footer derives the displayed text from the URL itself.
+ # --source-url. The header bar derives the displayed text from the URL itself.
SOURCE_URL="${SOURCE_URL:-https://codeberg.org/snonux/shuriken.sh}"
SPLASH_PAGE="${SPLASH_PAGE:-yes}"
STATS_PAGE="${STATS_PAGE:-no}"
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000..07bc1e0
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,84 @@
+# Configuration
+
+The config file is a Bash file sourced by shuriken. `shuriken --init` creates
+`./shuriken.conf` from the default config; edit it and override any of the
+variables below. Command-line options (see [usage.md](usage.md)) override config
+values for the current run.
+
+## Variables
+
+| Variable | Default | Description |
+| --- | --- | --- |
+| `TITLE` | `A simple Shuriken` | Album title. |
+| `HEIGHT` | `1200` | Scaled photo height in pixels. Leave unset to keep original size. Optional positive integer. |
+| `THUMBHEIGHT` | `300` | Thumbnail height in pixels. Positive integer. |
+| `MAXPREVIEWS` | `40` | Maximum previews per page. Positive integer. |
+| `IMAGE_JOBS` | `3` | Parallel jobs for image processing and HTML template rendering. Positive integer. |
+| `IMAGEMAGICK_TIMEOUT` | `60` | Per-ImageMagick-command timeout in seconds. Positive integer. |
+| `TAR_TIMEOUT` | `120` | Tarball creation timeout in seconds. Positive integer. |
+| `SHUFFLE` | `no` | Randomly shuffle all previews. `yes`/`no`. |
+| `SPLASH_PAGE` | `yes` | Generate a splash landing page at `index.html`. `yes`/`no`. |
+| `STATS_PAGE` | `no` | Generate the EXIF stats site under `stats/`. `yes`/`no`. |
+| `RANDOM_SEED` | _(unset)_ | Any non-empty value makes splash/background picks, animation classes, timestamps, and shuffle order repeatable. |
+| `INCOMING_DIR` | `$(pwd)/incoming` | Directory containing source photos (full path). |
+| `DIST_DIR` | `$(pwd)/dist` | Output directory (full path). |
+| `TEMPLATE_DIR` | `/usr/share/shuriken/templates/default` | Template directory. Falls back to the source tree's `share/templates/default` when running from a checkout. |
+| `FAVICON` | _(unset = bundled default)_ | Custom favicon, published as `favicon.ico`. Must be a readable file when set. |
+| `SOURCE_URL` | `https://codeberg.org/snonux/shuriken.sh` | Project/source link shown in the page header bar. |
+| `TARBALL_INCLUDE` | `yes` (in `--init` config) | Include a `.tar` of the incoming dir in the dist. `yes`/`no`. |
+| `TARBALL_SUFFIX` | `.tar` | Suffix for the generated tarball. |
+| `TAR_OPTS` | `(-c)` | Tar options as a Bash array (or whitespace-separated scalar). |
+| `SYNC_DELETE` | `yes` | Pass `--delete` to rsync on `--sync`. `yes`/`no`. |
+| `SYNC_DESTINATIONS` | `()` | Bash array of rsync destinations. See [publishing.md](publishing.md). |
+| `ORIGINAL_BASEPATH` | _(unset)_ | Recorded in `shuriken.json` for debugging a published album. |
+
+> Note on `TARBALL_INCLUDE`: the bundled `shuriken.default.conf` sets it to
+> `yes`, so a freshly `--init`'d config enables the tarball. The runtime default
+> applied when a config file leaves it unset is `no`.
+
+## Supported source images
+
+Only regular files in `INCOMING_DIR` (top level, `maxdepth 1`) with supported
+image extensions are processed as album images. Supported extensions are `jpg`,
+`jpeg`, `png`, `webp`, and `gif`, matched case-insensitively. Other files, such
+as `.txt` or `.md` notes, are ignored with a warning so generation can continue.
+
+## Validation
+
+`shuriken` validates the loaded config and command-line overrides before acting.
+The checks (details in `src/lib/config.validate.source.sh`):
+
+* **Required values** are set: `TITLE`, `THUMBHEIGHT`, `MAXPREVIEWS`,
+ `IMAGE_JOBS`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`.
+* **Positive integers**: `THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`,
+ `IMAGEMAGICK_TIMEOUT`, `TAR_TIMEOUT`; `HEIGHT` is an optional positive integer.
+* **`yes`/`no` settings**: `SHUFFLE`, `SPLASH_PAGE`, `STATS_PAGE`,
+ `TARBALL_INCLUDE`, `SYNC_DELETE` (where applicable).
+* **Readable input**: `INCOMING_DIR` must be a readable directory; `TEMPLATE_DIR`
+ must be a readable directory containing the required templates (plus `splash`
+ when `SPLASH_PAGE=yes`).
+* **Writable output**: `DIST_DIR` (or its nearest existing parent) must be
+ writable.
+* **ImageMagick** availability (`magick` or `convert`).
+* **`FAVICON`**, when set, must be a readable file.
+* **`--clean`** additionally refuses to delete dangerous paths (filesystem root,
+ `HOME`, the current directory, well-known system trees) after resolving
+ `DIST_DIR` canonically.
+* **`--sync`** requires at least one destination and rsync.
+
+Generation stops before writing album output when validation fails.
+
+## `--print-config` output format
+
+`--print-config` writes stable shell-style assignments to stdout in this order:
+
+`CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `FAVICON`,
+`SOURCE_URL`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`, `IMAGE_JOBS`,
+`IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, `STATS_PAGE`,
+`TARBALL_INCLUDE`, `TARBALL_SUFFIX`, `TAR_TIMEOUT`, `TAR_OPTS`, `SYNC_DELETE`,
+`SYNC_DESTINATIONS`, `ORIGINAL_BASEPATH`.
+
+Scalar values use Bash `%q` quoting; `TAR_OPTS` and `SYNC_DESTINATIONS` are
+normalized to Bash array assignments, so the output can be parsed by shell
+tooling. `--quiet` does not suppress this output, and `--verbose` does not add
+human-readable diagnostics to it. \ No newline at end of file
diff --git a/docs/generation.md b/docs/generation.md
new file mode 100644
index 0000000..b477b30
--- /dev/null
+++ b/docs/generation.md
@@ -0,0 +1,115 @@
+# Generation
+
+This page describes how `shuriken --generate` works and the features that
+affect its output.
+
+## Artifact reuse and incremental generation
+
+Normal generation preserves reusable generated artifacts from the previous
+`DIST_DIR` while still rerendering HTML, random splash/background choices,
+animation classes, timestamps, and shuffled preview order. Existing scaled
+photos, thumbnails, and blurs are reused from the previous output when the
+source image is unchanged.
+
+## The EXIF cache
+
+The per-photo EXIF `identify` output is cached in a separate `cache/exif/`
+directory created next to `DIST_DIR` (i.e. parallel to `dist/` in the working
+directory — the path is `$(dirname "$DIST_DIR")/cache/exif`). Each cache entry
+records a `<photo>:<size>:<mtime>` signature so it is reused only while the
+source file is unchanged.
+
+This cache is volatile and safe to delete, is **not** part of the published
+output (it is never written into `DIST_DIR`, so `--sync` does not deploy it),
+and persists across runs even if `DIST_DIR` is removed or rebuilt. Because
+reading EXIF from full-size originals is the slowest part of generation, keeping
+this cache makes regenerating an album dramatically faster: an unchanged photo
+skips `identify` entirely.
+
+* `--clean` removes `DIST_DIR` (and any leftover staging directories) but leaves
+ `cache/` in place; delete `cache/` by hand to force a full EXIF rebuild on the
+ next run.
+* `--force` (with `--generate`) clears `cache/` once up front, then repopulates
+ it during the run, so there is still exactly one `identify` per photo even
+ under force.
+
+## Splash page
+
+By default the generated root `index.html` is a no-JavaScript splash page using
+a randomly selected album photo. Set `SPLASH_PAGE=no` or pass `--no-splash` to
+restore the top-level redirect to `page-1.html`.
+
+To quickly pick a new random splash photo for an already generated album, run
+`shuriken --refresh-splash`. This rewrites `DIST_DIR/index.html` (and re-copies
+the site favicon) using the existing `photos` and `blurs` output, so it avoids
+reprocessing images and rerendering album pages. It requires `SPLASH_PAGE=yes`;
+pass `--random-seed VALUE` when you need a repeatable pick.
+
+## Stats site
+
+`shuriken` can also generate a no-JavaScript stats site with EXIF-derived
+insights (camera leaderboard, shooting dates, exposure, dimension, format, and
+decoded-enum breakdowns), reachable from the `Stats` link in the page header
+bar. This is off by default; set `STATS_PAGE=yes` or pass `--stats` to enable it.
+
+Every row on the stats overview is clickable: each bucket (each camera, ISO,
+year, aperture, orientation, …) is its own filter "mini-album" — a gallery of
+just the matching photos with view pages whose previous/next cycle within that
+filter.
+
+To keep the album root uncluttered, all of this lives under a `stats/`
+subdirectory: the overview is `stats/index.html` and each mini-album is its own
+directory `stats/<filter>/` (gallery `index.html` plus numbered view pages). Only
+the main album sits in `DIST_DIR` itself. The mini-album pages reuse the album's
+shared `photos/`, `thumbs/`, and `blurs/` assets (only the HTML is per-filter)
+and are rendered in parallel honouring `IMAGE_JOBS`. Set `STATS_PAGE=no` or pass
+`--no-stats` (the default) to skip the whole `stats/` tree and hide the link.
+
+See [stats-exif-audit.md](stats-exif-audit.md) for the EXIF field coverage
+analysis behind these categories.
+
+## Reproducibility
+
+By default, splash and background photos, animation classes, generated
+timestamps, and `--shuffle` preview order remain non-deterministic. Set
+`RANDOM_SEED` in the config, or pass `--random-seed VALUE`, to make those
+choices repeatable for stable tests or reproducible album builds. Use the same
+seed and inputs to produce the same HTML.
+
+## Parallelism and timeouts
+
+ImageMagick photo processing and per-photo HTML template rendering run in
+parallel. The default is `IMAGE_JOBS=3`. Set `IMAGE_JOBS` in the config, or pass
+`--image-jobs N`, to tune the number of concurrent image and template jobs.
+
+Each ImageMagick command is bounded by `IMAGEMAGICK_TIMEOUT=60` seconds, and
+tarball creation is bounded by `TAR_TIMEOUT=120` seconds. Set either config
+value to a positive integer to adjust the limit for large images or archives.
+
+## Generation metadata (`shuriken.json`)
+
+Successful generation writes `shuriken.json` into the output directory. This
+metadata records:
+
+* the generator name and version, and a generation timestamp;
+* the config source and template name/directory;
+* the source `INCOMING_DIR` and source image count;
+* generated photo, thumbnail, and HTML file counts;
+* tarball status (included + file);
+* effective settings (title, height, thumbheight, maxpreviews, image jobs,
+ random seed, shuffle, splash page, stats page, original basepath) useful for
+ debugging a published album.
+
+## Favicon
+
+Generation writes `favicon.ico` into the output directory and the default
+templates link to it. By default this is the bundled shuriken favicon; set
+`FAVICON` in the config or pass `--favicon PATH` to publish your own favicon
+file instead (it is copied in as `favicon.ico`).
+
+## Source URL
+
+The page header bar links to the project source ("Site generated … with
+&lt;URL&gt;"). This defaults to the shuriken.sh repository; set `SOURCE_URL` in
+the config or pass `--source-url URL` to point it at your own album's repository
+instead. The displayed link text is the URL with its scheme removed. \ No newline at end of file
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..60c8859
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,58 @@
+# Installation
+
+## Requirements
+
+* **Bash 5.1 or newer.** shuriken uses features that require it and exits with
+ an error on older Bash.
+* **ImageMagick.** The script prefers the modern `magick` command and falls back
+ to `convert` / `identify` when only the legacy tools are present.
+* **rsync** (only needed for `shuriken --sync`).
+
+## Build and install from a source checkout
+
+```sh
+just build
+sudo just install
+```
+
+`just install` installs:
+
+* `shuriken` to `/usr/bin`,
+* templates to `/usr/share/shuriken/templates/default` and static assets to
+ `/usr/share/shuriken/assets`,
+* the default config to `/etc/default/shuriken`.
+
+## Packaging / staging overrides
+
+Override install paths with `DESTDIR`, `PREFIX`, `BINDIR`, `DATADIR`, or
+`SYSCONFDIR` when packaging or staging an install:
+
+```sh
+DESTDIR="$PWD/pkg" PREFIX=/usr just install
+DESTDIR="$PWD/pkg" PREFIX=/usr just deinstall
+```
+
+`just uninstall` is an alias for `just deinstall`.
+
+Defaults: `PREFIX=/usr`, `BINDIR=$PREFIX/bin`, `DATADIR=$PREFIX/share`,
+`SYSCONFDIR=/etc/default`.
+
+## The generated `bin/shuriken` artifact
+
+`bin/shuriken` is a committed generated artifact kept in sync with
+`src/shuriken.sh` for compatibility with existing checkouts and packaging. Its
+source of truth is `src/shuriken.sh` rendered through the `VERSION` and
+`LIB_SOURCES` values in `Justfile`.
+
+* Run `just build` after changing `src/shuriken.sh` or any `src/lib/*.source.sh`
+ file, and keep `bin/shuriken` synchronized.
+* Run `just check-generated` to verify that the tracked script has not drifted.
+ `just test` and `just install` run that drift check before rebuilding, so a
+ stale committed output is never silently hidden.
+
+## Running from a checkout (no install)
+
+You can run `./bin/shuriken` directly from a source checkout. The stock default
+template directory resolves to the installed location when it exists, and
+otherwise falls back to the source tree's `share/templates/default`. Likewise
+the bundled favicon falls back to `assets/site/favicon.ico`. \ No newline at end of file
diff --git a/docs/publishing.md b/docs/publishing.md
new file mode 100644
index 0000000..f31eaaf
--- /dev/null
+++ b/docs/publishing.md
@@ -0,0 +1,45 @@
+# Publishing
+
+To publish generated output, configure destinations and run `shuriken --sync`.
+
+## `SYNC_DESTINATIONS`
+
+`SYNC_DESTINATIONS` must be a Bash array, even for a single destination:
+
+```sh
+SYNC_DESTINATIONS=(
+ admin@fishfinger.buetow.org:/var/www/htdocs/example.org/
+ admin@blowfish.buetow.org:/var/www/htdocs/example.org/
+)
+```
+
+A scalar string is rejected with an error, since word-splitting would break
+destinations that contain spaces (for example
+`SYNC_DESTINATIONS=( '/path/with spaces/' )` is the correct spelling).
+
+`--sync` validates that `SYNC_DESTINATIONS` contains at least one destination and
+that rsync is installed.
+
+## Overriding destinations per run
+
+Pass `--sync-destination DEST` one or more times with `--sync` to override the
+configured destinations for that run only:
+
+```sh
+shuriken --sync --sync-destination user@host:/var/www/htdocs/example.org/
+```
+
+## The rsync command
+
+`--sync` runs, for each destination:
+
+```sh
+rsync -av --delete "$DIST_DIR/" "$destination"
+```
+
+The trailing slash on `DIST_DIR/` means the generated **contents** are copied
+into the target directory. Set `SYNC_DELETE=no` or pass `--no-sync-delete` to
+omit `--delete`.
+
+`--sync` reads `./shuriken.conf` by default; pass `--config PATH` to select a
+different config file. \ No newline at end of file
diff --git a/docs/stats-exif-audit.md b/docs/stats-exif-audit.md
index 1b1023b..2da9cae 100644
--- a/docs/stats-exif-audit.md
+++ b/docs/stats-exif-audit.md
@@ -1,5 +1,13 @@
# Stats Page EXIF Field Coverage Audit
+> **Status note (added after implementation):** This is a historical design
+> record from task `nm0`. The stats site it scopes has since been implemented
+> (`src/lib/stats-aggregate.source.sh`, `src/lib/stats-render.source.sh`,
+> `src/lib/stats-filter-album.source.sh`), including the "native-field parser
+> extension" described below as required work — `Geometry`/`Format` lines are
+> now read alongside the `exif:` lines. The verdicts and decode maps below
+> remain accurate as a description of what the EXIF data can and cannot support.
+
Task `nm0` for the stats site feature (see
`/home/paul/.pi/plans/shuriken-stats-site.md`). This document decides which of
the planned stats categories are actually buildable from the EXIF/identify data
@@ -13,8 +21,10 @@ library (there is none to point at):
* `src/lib/imagemagick.source.sh` runs `magick identify -verbose <file>` (or
`convert <file> -verbose info:` when only the legacy `convert` is present).
-* `src/lib/album.source.sh` caches that raw output per photo under a volatile
- `./cache/exif/<photo>.txt` (parallel to `./dist`) and parses it.
+* The shared EXIF cache in `src/lib/metadata-cache.source.sh` (promoted out of
+ the album module after this audit was written) caches that raw output per
+ photo under a volatile `./cache/exif/<photo>.txt` (parallel to `./dist`);
+ `src/lib/album-metadata.source.sh` parses it.
The parser detail that drives every decision below: shuriken only reads EXIF
through this regex (`photo_exif_details_html`, `_photo_exif_values_to`):
diff --git a/docs/templates.md b/docs/templates.md
new file mode 100644
index 0000000..f34be9d
--- /dev/null
+++ b/docs/templates.md
@@ -0,0 +1,34 @@
+# HTML templates
+
+Templates live under `share/templates/default` in the source tree and under the
+installed template directory (`/usr/share/shuriken/templates/default`) after
+installation.
+
+The stock default resolves to the installed template directory when it exists,
+and otherwise falls back to the source tree's `share/templates/default` when
+running from a checkout. Copy and edit templates, then point `TEMPLATE_DIR` or
+`--template PATH` at the customized directory.
+
+## Required templates
+
+A template directory must contain these `.tmpl` files (validated before
+generation):
+
+`details`, `footer`, `header`, `next`, `prev`, `preview`, `previewpage`,
+`redirect`, `view`.
+
+When `SPLASH_PAGE=yes`, the `splash` template is also required.
+
+## Stats templates
+
+The stats site adds two more templates, `camera.tmpl` and `cameraview.tmpl`
+(per-camera mini-album), plus `stats.tmpl` for the overview page. These are only
+used when `STATS_PAGE=yes`.
+
+## Header bar and footer
+
+* The page **header bar** (`header.tmpl`) renders the "Site generated … with
+ &lt;SOURCE_URL&gt;" link and, when `STATS_PAGE=yes`, the `Stats` navigation
+ link.
+* The page **footer** (`footer.tmpl`) renders the "Download all photos in
+ original size" tarball link, shown only when `TARBALL_INCLUDE=yes`. \ No newline at end of file
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..b11514b
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,105 @@
+# CLI reference
+
+```
+shuriken --init
+shuriken --generate [--config PATH] [OPTIONS]
+shuriken --refresh-splash [--config PATH] [OPTIONS]
+shuriken --sync [--config PATH] [OPTIONS]
+shuriken --dry-run [--config PATH] [OPTIONS]
+shuriken --print-config [--config PATH] [OPTIONS]
+shuriken --clean [--config PATH] [OPTIONS]
+shuriken --version
+```
+
+## Actions
+
+* `--init` creates `./shuriken.conf` in the current working directory from the
+ default config. It refuses to overwrite an existing file. `--init` is a
+ non-config action: it rejects `--config`, any config override, and `--force`.
+* `--generate` builds the static album.
+* `--force` (only valid with `--generate`) rebuilds from scratch instead of
+ reusing cached scaled photos, thumbnails, blurs, or EXIF data from the existing
+ output. Using `--force` with any other action is a usage error.
+* `--refresh-splash` rewrites only the generated root splash page. Requires
+ `SPLASH_PAGE=yes`.
+* `--sync` publishes the generated output directory to configured rsync
+ destinations. See [publishing.md](publishing.md).
+* `--dry-run` loads the config and overrides, validates the planned generation,
+ and prints the effective paths, image count, tarball plan, and generated file