From 3c35381b71f992eb7a86552697ea647bb76b68b1 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 18 Jul 2026 12:19:40 +0300 Subject: Add DETAILS_PAGE setting to make per-photo details pages optional Album owners can now set DETAILS_PAGE=no (or pass --no-details) to skip generating each photo's *-details.html EXIF summary page and its "Details" navigation redirects, without touching the normal thumbnail overview, per-photo view pages, EXIF tooltips, or STATS_PAGE, which all stay independently controlled. Every "Details" link (on view pages and stats filter mini-album view pages) and every "-details" redirect stub is gated on the setting so no generated page ever links to a file that was not rendered. Wired the new field through CONFIG_SPECS (registry-driven defaults/validation/print-config/CLI override), the --details/--no-details CLI flags, usage() help, the verbose effective-config log, shuriken.json generation metadata, and the dry-run plan. DETAILS_PAGE=yes (the default) keeps prior output byte-for-byte identical. Co-Authored-By: Claude Sonnet 5 --- README.md | 13 ++- bin/shuriken | 203 +++++++++++++++++++++++++--------- docs/configuration.md | 39 ++++++- docs/generation.md | 19 +++- docs/usage.md | 2 + share/templates/default/view.tmpl | 17 ++- src/lib/action.source.sh | 1 + src/lib/album-render.source.sh | 131 +++++++++++++++------- src/lib/bootstrap.source.sh | 2 + src/lib/config.spec.source.sh | 1 + src/lib/config.validate.source.sh | 19 +++- src/lib/dry-run.source.sh | 33 +++++- src/lib/generation-metadata.source.sh | 3 + src/lib/stats-filter-album.source.sh | 10 +- src/lib/template.source.sh | 1 + src/shuriken.default.conf | 7 ++ src/shuriken.sh | 2 + tests/cli.sh | 195 +++++++++++++++++++++++++++++++- 18 files changed, 584 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index ee381a3..8e9eca3 100644 --- a/README.md +++ b/README.md @@ -66,15 +66,20 @@ Common per-run overrides (see the full reference table in [docs/usage.md](docs/u `--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`. +`--splash`/`--no-splash`, `--details`/`--no-details`, `--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`. +* **Details pages** (`DETAILS_PAGE=yes`, the default): every photo gets a + `*-details.html` EXIF summary page linked from its normal view page. + `--no-details` skips these pages and removes their "Details" links (from the + normal view pages and, when stats are enabled, the stats filter mini-albums) + without affecting EXIF tooltips or the stats site itself. * **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 @@ -90,7 +95,7 @@ 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/generation.md](docs/generation.md) — how generation works: artifact reuse, the EXIF cache, `--force`, splash/details/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 4e23419..a475a3a 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -55,6 +55,8 @@ declare -Ar CLI_OPTION_SPEC=( [--no-shuffle]='kind=flag value=no config=SHUFFLE' [--splash]='kind=flag value=yes config=SPLASH_PAGE' [--no-splash]='kind=flag value=no config=SPLASH_PAGE' + [--details]='kind=flag value=yes config=DETAILS_PAGE' + [--no-details]='kind=flag value=no config=DETAILS_PAGE' [--stats]='kind=flag value=yes config=STATS_PAGE' [--no-stats]='kind=flag value=no config=STATS_PAGE' [--tarball]='kind=flag value=yes config=TARBALL_INCLUDE' @@ -212,6 +214,8 @@ usage() { --random-seed VALUE --splash --no-splash + --details + --no-details --stats --no-stats --shuffle @@ -834,6 +838,7 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_cameraview_body_html|context_raw|cameraview_body|cameraview_body|cameraview' 'render_camera_thumbs_html|context_raw|camera_thumbs|camera_thumbs|camera' 'render_current_date_text|current_date_html|||header' + 'render_details_page_html|config_html|DETAILS_PAGE||view' 'render_enter_page_html|context_html|enter_page|enter_page|splash' 'render_exif_details_html|context_raw|exif_details|exif_details|details' 'render_exif_tooltip_html|context_html|exif_tooltip|exif_tooltip|details view' @@ -2523,6 +2528,7 @@ _collect_generation_metadata() { _GENERATION_METADATA["settings_random_seed"]="$RANDOM_SEED" _GENERATION_METADATA["settings_shuffle"]="$SHUFFLE" _GENERATION_METADATA["settings_splash_page"]="$SPLASH_PAGE" + _GENERATION_METADATA["settings_details_page"]="$DETAILS_PAGE" _GENERATION_METADATA["settings_stats_page"]="$STATS_PAGE" _GENERATION_METADATA["settings_original_basepath"]="$ORIGINAL_BASEPATH" } @@ -2594,6 +2600,8 @@ _generation_metadata_json_settings() { "$(json_bool "${_GENERATION_METADATA["settings_shuffle"]}")" printf ' "splash_page": %s,\n' \ "$(json_bool "${_GENERATION_METADATA["settings_splash_page"]}")" + printf ' "details_page": %s,\n' \ + "$(json_bool "${_GENERATION_METADATA["settings_details_page"]}")" printf ' "stats_page": %s,\n' \ "$(json_bool "${_GENERATION_METADATA["settings_stats_page"]}")" printf ' "original_basepath": %s\n' \ @@ -2660,9 +2668,14 @@ collect_dry_run_page_plan() { # files are touched here, so dry-run stays side-effect free. page_count=$(album_page_count_for_image_count "$image_count") redirect_count=$(album_redirect_count_for_page_count "$page_count") - plan_ref["details_count"]="$image_count" plan_ref["page_count"]="$page_count" plan_ref["redirect_count"]="$redirect_count" + # One details page per photo, but only when DETAILS_PAGE=yes -- mirrors + # render_photo_view_and_details skipping render_details_page entirely, + # so the plan cannot drift from what --generate actually writes. + if [ "$DETAILS_PAGE" = yes ]; then + plan_ref["details_count"]="$image_count" + fi fi } @@ -2707,6 +2720,7 @@ collect_dry_run_plan() { plan_ref["random_seed"]="$RANDOM_SEED" plan_ref["shuffle"]="$SHUFFLE" plan_ref["splash_page"]="$SPLASH_PAGE" + plan_ref["details_page"]="$DETAILS_PAGE" plan_ref["stats_page"]="$STATS_PAGE" plan_ref["image_count"]="$image_count" plan_ref["tarball_include"]="$TARBALL_INCLUDE" @@ -2741,14 +2755,17 @@ _print_dry_run_settings() { printf 'Random seed: %s\n' "${plan_ref["random_seed"]}" printf 'Shuffle: %s\n' "${plan_ref["shuffle"]}" printf 'Splash page: %s\n' "${plan_ref["splash_page"]}" + printf 'Details page: %s\n' "${plan_ref["details_page"]}" printf 'Stats page: %s\n' "${plan_ref["stats_page"]}" printf 'Image count: %s\n' "${plan_ref["image_count"]}" printf 'Tarball setting: %s\n' "${plan_ref["tarball_include"]}" printf 'Tarball name plan: %s\n' "${plan_ref["tarball_name_plan"]}" } -# Print the planned directories and generated-files listing (index/favicon/json, -# image dirs, page/view/details/redirect counts, optional stats + tarball lines). +# Print the planned directories, plus the generated-files lines that are always +# present regardless of any page toggle (index/favicon/json/image dirs/page/view +# counts). Split from the optional-lines half below (_print_dry_run_optional_files) +# so each stays around 30 lines, matching the generation-metadata JSON split. _print_dry_run_files() { local -r plan_name="$1"; shift # shellcheck disable=SC2178 @@ -2780,8 +2797,21 @@ _print_dry_run_files() { "${plan_ref["dist_dir"]}" "${plan_ref["page_count"]}" printf ' %s/[page]-[image].html (%s view pages)\n' \ "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}" - printf ' %s/[page]-[image]-details.html (%s details pages)\n' \ - "${plan_ref["dist_dir"]}" "${plan_ref["details_count"]}" +} + +# Print the generated-files lines gated behind a page toggle (details, the +# navigation redirect count that itself depends on DETAILS_PAGE, stats, and the +# tarball). Split out of _print_dry_run_files (see its comment) purely to keep +# both halves short; output is unchanged from the previous single function. +_print_dry_run_optional_files() { + local -r plan_name="$1"; shift + # shellcheck disable=SC2178 + local -n plan_ref="$plan_name" + + if [ "${plan_ref["details_page"]}" = yes ]; then + printf ' %s/[page]-[image]-details.html (%s details pages)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["details_count"]}" + fi printf ' %s/[redirect].html (%s navigation redirects)\n' \ "${plan_ref["dist_dir"]}" "${plan_ref["redirect_count"]}" if [ "${plan_ref["stats_page"]}" = yes ]; then @@ -2805,6 +2835,7 @@ print_dry_run_plan() { _print_dry_run_settings "$plan_name" _print_dry_run_files "$plan_name" + _print_dry_run_optional_files "$plan_name" } # Inlined from src/lib/album-tile-layout.source.sh @@ -3712,6 +3743,10 @@ render_view_page() { tarball_name "$tarball_name" } +# Render one photo's *-details.html page. Only called by +# render_photo_view_and_details when DETAILS_PAGE=yes; callers must not invoke +# this directly for a photo when details are disabled, or a dangling file would +# be produced with no page linking to it. render_details_page() { local -r html_dir="$1"; shift local -r photos_dir="$1"; shift @@ -3774,15 +3809,22 @@ render_photo_view_and_details() { "$page_num" \ "$preview_num" \ "$photo" - render_details_page \ - "$html_dir" \ - "$photos_dir" \ - "$blurs_dir" \ - "$backhref" \ - "$tarball_name" \ - "$page_num" \ - "$preview_num" \ - "$photo" + + # DETAILS_PAGE=no skips the *-details.html file entirely (view.tmpl's Details + # link is likewise suppressed via render_details_page_html, so nothing links + # to it). This is the only place a photo's details page would be rendered, so + # skipping the call here is sufficient to omit the file for every photo. + if [ "$DETAILS_PAGE" = yes ]; then + render_details_page \ + "$html_dir" \ + "$photos_dir" \ + "$blurs_dir" \ + "$backhref" \ + "$tarball_name" \ + "$page_num" \ + "$preview_num" \ + "$photo" + fi } record_rendered_view_page() { @@ -3799,32 +3841,78 @@ record_rendered_view_page() { last_views_ref["$page"]="$preview" } -# Navigation-redirect count single source of truth (task nr0). Every view page -# gets ALBUM_REDIRECTS_PER_PAGE wrap-around redirect files: the prev stub -# (N-0.html) and its details twin, plus the next stub (N-(last+1).html) and its -# details twin -- four files, emitted by render_page_view_redirects below for -# every page. The LAST page additionally emits ALBUM_REDIRECTS_LAST_PAGE_EXTRA -# files: the 0-MAXPREVIEWS / 0-MAXPREVIEWS-details entry stubs that bounce into -# the album. Keep these two numbers in lockstep with render_page_view_redirects; -# the dry-run plan predicts redirect_count from them via -# album_redirect_count_for_page_count instead of a magic "*4+2". -declare -gri ALBUM_REDIRECTS_PER_PAGE=4 -declare -gri ALBUM_REDIRECTS_LAST_PAGE_EXTRA=2 +# Navigation-redirect count single source of truth (task nr0; extended for +# DETAILS_PAGE by task 6v0). Every view page gets ALBUM_VIEW_REDIRECTS_PER_PAGE +# wrap-around redirect files: the prev stub (N-0.html) and the next stub +# (N-(last+1).html). When DETAILS_PAGE=yes, each of those also gets a +# "-details" twin, adding ALBUM_DETAILS_REDIRECTS_PER_PAGE more -- so +# render_page_view_redirects emits 2 files per page with details disabled, 4 +# with them enabled. The LAST page additionally emits +# ALBUM_VIEW_REDIRECTS_LAST_PAGE_EXTRA entry stubs (plus their details twins +# when enabled): the 0-MAXPREVIEWS / loop-to-1 stubs that bounce into the +# album. Keep these numbers in lockstep with render_page_view_redirects; the +# dry-run plan predicts redirect_count from them via +# album_redirect_count_for_page_count instead of a magic formula. +declare -gri ALBUM_VIEW_REDIRECTS_PER_PAGE=2 +declare -gri ALBUM_DETAILS_REDIRECTS_PER_PAGE=2 +declare -gri ALBUM_VIEW_REDIRECTS_LAST_PAGE_EXTRA=1 +declare -gri ALBUM_DETAILS_REDIRECTS_LAST_PAGE_EXTRA=1 + +# Per-page / last-page-extra redirect file counts for the CURRENT DETAILS_PAGE +# setting: the view-only counts, plus the details counts when DETAILS_PAGE=yes. +# Split out of album_redirect_count_for_page_count so that function stays a +# short arithmetic one-liner. +album_redirects_per_page() { + local -i count=$ALBUM_VIEW_REDIRECTS_PER_PAGE + + if [ "$DETAILS_PAGE" = yes ]; then + count+=$ALBUM_DETAILS_REDIRECTS_PER_PAGE + fi + printf '%d\n' "$count" +} + +album_redirects_last_page_extra() { + local -i count=$ALBUM_VIEW_REDIRECTS_LAST_PAGE_EXTRA + + if [ "$DETAILS_PAGE" = yes ]; then + count+=$ALBUM_DETAILS_REDIRECTS_LAST_PAGE_EXTRA + fi + printf '%d\n' "$count" +} # Total navigation redirects a run produces for a given number of preview pages: -# four per page plus the last page's extra entry stubs. Zero pages -> zero -# redirects (render_view_redirects returns early on an empty album). This is the -# count render_page_view_redirects actually writes across all pages, expressed -# once so the dry-run plan cannot drift from real generation. +# album_redirects_per_page per page plus the last page's extra entry stubs. +# Zero pages -> zero redirects (render_view_redirects returns early on an empty +# album). This is the count render_page_view_redirects actually writes across +# all pages, expressed once so the dry-run plan cannot drift from real +# generation. album_redirect_count_for_page_count() { local -ri page_count="$1"; shift + local -i per_page + local -i last_page_extra if (( page_count <= 0 )); then printf '0\n' return fi - printf '%d\n' "$(( page_count * ALBUM_REDIRECTS_PER_PAGE \ - + ALBUM_REDIRECTS_LAST_PAGE_EXTRA ))" + per_page=$(album_redirects_per_page) + last_page_extra=$(album_redirects_last_page_extra) + printf '%d\n' "$(( page_count * per_page + last_page_extra ))" +} + +# Render one details-redirect stub, but only when DETAILS_PAGE=yes (mirrors +# render_photo_view_and_details skipping render_details_page entirely). A no-op +# under DETAILS_PAGE=no keeps every "-details.html" navigation stub from ever +# being written, so no generated page can link to a missing one. +_render_details_redirect() { + local -r html_dir="$1"; shift + local -r filename="$1"; shift + local -r target="$1"; shift + + if [ "$DETAILS_PAGE" != yes ]; then + return + fi + template redirect "$filename" html_dir "$html_dir" redirect_page "$target" } # Render every navigation redirect for a single view page (the prev/next @@ -3834,7 +3922,7 @@ album_redirect_count_for_page_count() { # files for distinct pages are produced here. The wrap-around redirects for the # very last page (0-MAXPREVIEWS and the loop-to-1 links) are emitted as part of # that page's group. Per-page / last-page file counts are fixed by -# ALBUM_REDIRECTS_PER_PAGE / ALBUM_REDIRECTS_LAST_PAGE_EXTRA above. +# album_redirects_per_page / album_redirects_last_page_extra above. render_page_view_redirects() { local -r html_dir="$1"; shift local -ri page="$1"; shift @@ -3846,30 +3934,26 @@ render_page_view_redirects() { template redirect "$prevredirect.html" \ html_dir "$html_dir" \ redirect_page "$(( page - 1 ))-${MAXPREVIEWS}" - template redirect "$prevredirect-details.html" \ - html_dir "$html_dir" \ - redirect_page "$(( page - 1 ))-${MAXPREVIEWS}-details" + _render_details_redirect "$html_dir" "$prevredirect-details.html" \ + "$(( page - 1 ))-${MAXPREVIEWS}-details" if (( page == max_page )); then template redirect "0-$MAXPREVIEWS.html" \ html_dir "$html_dir" \ redirect_page "${page}-$lastview" - template redirect "0-$MAXPREVIEWS-details.html" \ - html_dir "$html_dir" \ - redirect_page "${page}-$lastview-details" + _render_details_redirect "$html_dir" "0-$MAXPREVIEWS-details.html" \ + "${page}-$lastview-details" template redirect "$nextredirect.html" \ html_dir "$html_dir" \ redirect_page '1-1' - template redirect "$nextredirect-details.html" \ - html_dir "$html_dir" \ - redirect_page '1-1-details' + _render_details_redirect "$html_dir" "$nextredirect-details.html" \ + '1-1-details' else template redirect "$nextredirect.html" \ html_dir "$html_dir" \ redirect_page "$(( page + 1 ))-1" - template redirect "$nextredirect-details.html" \ - html_dir "$html_dir" \ - redirect_page "$(( page + 1 ))-1-details" + _render_details_redirect "$html_dir" "$nextredirect-details.html" \ + "$(( page + 1 ))-1-details" fi } @@ -5460,7 +5544,9 @@ render_stats_page() { # within that filter. The "--" suffix cannot collide with another gallery # name because a pagebase never contains "--". All pages reuse the album's shared # photos/thumbs/blurs assets (only the HTML differs); view pages link "Details" -# to the album's own details page via the album_view_page_for_photo accessor. +# to the album's own details page via the album_view_page_for_photo accessor, +# but only when DETAILS_PAGE=yes actually rendered that page (see +# _stats_build_filterview_body below). # Pages render in # parallel through the shared job pool, throttled to IMAGE_JOBS. The galleries # reuse camera.tmpl and the view pages reuse cameraview.tmpl. @@ -5603,7 +5689,11 @@ _stats_build_filterview_body() { tooltip_attr=" title=\"$(html_escape "$tooltip")\"" fi view_page=$(album_view_page_for_photo "$photo") - if [ -n "$view_page" ]; then + # Only link to the album's details page when it was actually rendered + # (DETAILS_PAGE=yes); otherwise album_view_page_for_photo resolving a page + # would still point at a details file that render_photo_view_and_details + # never wrote, leaving a dangling link. + if [ -n "$view_page" ] && [ "$DETAILS_PAGE" = yes ]; then details_link=$(printf \ ' Details |' \ "$backhref_html" "$view_page") @@ -5772,6 +5862,7 @@ declare -gra CONFIG_SPECS=( 'RANDOM_SEED||yes|yes||scalar' 'SHUFFLE|no|yes|yes|yesno|scalar' 'SPLASH_PAGE|yes|yes|yes|yesno|scalar' + 'DETAILS_PAGE|yes|yes|yes|yesno|scalar' 'STATS_PAGE|no|yes|yes|yesno|scalar' 'TARBALL_INCLUDE|yes|yes|yes|yesno|scalar' 'TARBALL_SUFFIX|.tar|yes|no||scalar' @@ -6517,8 +6608,20 @@ validate_template_file() { validate_template_dir() { local template_name - local -a required_templates=( - details + local -a required_templates=() + + validate_template_dir_access || return + + # details.tmpl is only needed when DETAILS_PAGE=yes actually renders it + # (mirrors the SPLASH_PAGE=yes conditional below), so an album that opts out + # of details pages does not need to keep a details.tmpl around at all. Kept + # first in the list (as it always was) so the reporting order for the + # unconditional templates below is unchanged from before DETAILS_PAGE + # existed. + if [ "$DETAILS_PAGE" = yes ]; then + required_templates+=(details) + fi + required_templates+=( footer header next @@ -6529,8 +6632,6 @@ validate_template_dir() { view ) - validate_template_dir_access || return - if [ "$SPLASH_PAGE" = yes ]; then required_templates+=(splash) fi @@ -6694,6 +6795,7 @@ validate_common_config() { SYNC_TIMEOUT SHUFFLE SPLASH_PAGE + DETAILS_PAGE STATS_PAGE TARBALL_INCLUDE FAVICON @@ -7115,6 +7217,7 @@ log_configured_action() { log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT}s" log_verbose "Effective tar timeout: ${TAR_TIMEOUT}s" log_verbose "Effective splash page setting: $SPLASH_PAGE" + log_verbose "Effective details page setting: $DETAILS_PAGE" log_verbose "Effective stats page setting: $STATS_PAGE" log_verbose "Effective tarball setting: $TARBALL_INCLUDE" log_verbose "Effective sync delete setting: $SYNC_DELETE" diff --git a/docs/configuration.md b/docs/configuration.md index 69a8835..e07227a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,6 +20,7 @@ values for the current run. | `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`. | +| `DETAILS_PAGE` | `yes` | Generate each photo's `*-details.html` page (and its "Details" link). `yes`/`no`. See "Details pages" below. | | `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). | @@ -38,6 +39,32 @@ values for the current run. > `yes`, so a freshly `--init`'d config enables the tarball. The runtime default > applied when a config file leaves it unset is `no`. +## Details pages + +`DETAILS_PAGE` (default `yes`) controls whether each photo gets its own +`*-details.html` page -- a dedicated view showing the full EXIF summary table, +reachable via the "Details" link on that photo's normal view page and, when +`STATS_PAGE=yes`, from the matching filter mini-album's view pages too. + +Setting `DETAILS_PAGE=no` (or passing `--no-details`) skips generating these +pages entirely and removes every "Details" link that would point at one, so no +generated page ever links to a missing file. Everything else keeps working +unchanged: + +* The normal thumbnail overview pages and per-photo view pages are still + generated. +* The per-photo EXIF tooltip (the `title=""` attribute shown on hover) is + unaffected -- it is controlled independently of the details page. +* `STATS_PAGE` is unaffected: the EXIF stats site and its filter mini-albums + still generate normally with `DETAILS_PAGE=no`; only their "Details" links + (which would otherwise point at the main album's per-photo details page) are + omitted. + +A later `--generate` run that switches `DETAILS_PAGE` from `yes` back to `no` +does not leave stale `*-details.html` files behind: generation stages the new +output in a fresh directory and atomically replaces `DIST_DIR`, so files an +older generation wrote but the current run does not produce are naturally gone. + ## Supported source images Only regular files found directly in `INCOMING_DIR` (not in subdirectories) with @@ -57,11 +84,11 @@ The checks (details in `src/lib/config.validate.source.sh`): `IMAGEMAGICK_TIMEOUT`, `TAR_TIMEOUT`; `HEIGHT` is an optional positive integer. * **Percentage (0-100 integer)**: `THUMB_SUBDIVIDE_PERCENT`, `THUMB_FEATURE_PERCENT`. -* **`yes`/`no` settings**: `SHUFFLE`, `SPLASH_PAGE`, `STATS_PAGE`, - `TARBALL_INCLUDE`, `SYNC_DELETE` (where applicable). +* **`yes`/`no` settings**: `SHUFFLE`, `SPLASH_PAGE`, `DETAILS_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`). + when `SPLASH_PAGE=yes`, and `details` when `DETAILS_PAGE=yes`). * **Writable output**: `DIST_DIR` (or its nearest existing parent) must be writable. * **ImageMagick** availability (`magick` or `convert`). @@ -80,9 +107,9 @@ Generation stops before writing album output when validation fails. `CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `FAVICON`, `SOURCE_URL`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`, `THUMB_SUBDIVIDE_PERCENT`, `THUMB_FEATURE_PERCENT`, `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`. +`IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`, `DETAILS_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 diff --git a/docs/generation.md b/docs/generation.md index 4d29840..126f2bd 100644 --- a/docs/generation.md +++ b/docs/generation.md @@ -45,6 +45,20 @@ 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. +## Details pages + +Every photo gets a `*-details.html` page by default: a dedicated view with the +full EXIF summary table, reachable via the "Details" link on that photo's +normal view page. Set `DETAILS_PAGE=no` or pass `--no-details` to skip +generating these pages; the "Details" link (on the normal view pages and, when +`STATS_PAGE=yes`, on the filter mini-album view pages) is removed along with +it, so no generated page links to a missing file. + +`DETAILS_PAGE=no` only affects the details pages and their links. It does not +disable the per-photo EXIF tooltip (the `title=""` attribute shown on hover +over a photo) or `STATS_PAGE`: both stay independently controlled and keep +working exactly as configured. + ## Stats site `shuriken` can also generate a no-JavaScript stats site with EXIF-derived @@ -97,8 +111,9 @@ metadata records: * generated photo, thumbnail, and HTML file counts; * tarball status (included + file); * effective settings (title, height, thumbheight, maxpreviews, subdivide - percent, feature percent, image jobs, random seed, shuffle, splash page, stats - page, original basepath) useful for debugging a published album. + percent, feature percent, image jobs, random seed, shuffle, splash page, + details page, stats page, original basepath) useful for debugging a published + album. ## Favicon diff --git a/docs/usage.md b/docs/usage.md index 3f85471..9436e36 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -76,6 +76,8 @@ config variable documented in [configuration.md](configuration.md). | `--no-shuffle` | `SHUFFLE=no` | | `--splash` | `SPLASH_PAGE=yes` | | `--no-splash` | `SPLASH_PAGE=no` | +| `--details` | `DETAILS_PAGE=yes` | +| `--no-details` | `DETAILS_PAGE=no` | | `--stats` | `STATS_PAGE=yes` | | `--no-stats` | `STATS_PAGE=no` | | `--tarball` | `TARBALL_INCLUDE=yes` | diff --git a/share/templates/default/view.tmpl b/share/templates/default/view.tmpl index c28859b..afabe29 100644 --- a/share/templates/default/view.tmpl +++ b/share/templates/default/view.tmpl @@ -1,5 +1,7 @@ # A non-empty EXIF summary becomes a title="" attribute so hovering the image -# shows the camera/exposure details, mirroring the details view. +# shows the camera/exposure details, mirroring the details view. This tooltip is +# independent of the Details link below: it stays available even when +# DETAILS_PAGE=no has suppressed the details page itself. if [ -n "$render_exif_tooltip_html" ]; then render_exif_tooltip_attr=" title=\"$render_exif_tooltip_html\"" else @@ -14,7 +16,20 @@ cat < Thumbnails | +END + +# The Details link is only emitted when DETAILS_PAGE=yes actually rendered the +# *-details.html page it points to; otherwise it would be a dangling link. +# Keeping this as its own conditional cat block (rather than inlining the link +# into a shell variable) keeps the DETAILS_PAGE=yes output byte-for-byte +# identical to before this toggle existed. +if [ "${render_details_page_html}" = 'yes' ]; then + cat <Details | +END +fi + +cat <Direct link diff --git a/src/lib/action.source.sh b/src/lib/action.source.sh index b0460f0..cbc928e 100644 --- a/src/lib/action.source.sh +++ b/src/lib/action.source.sh @@ -177,6 +177,7 @@ log_configured_action() { log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT}s" log_verbose "Effective tar timeout: ${TAR_TIMEOUT}s" log_verbose "Effective splash page setting: $SPLASH_PAGE" + log_verbose "Effective details page setting: $DETAILS_PAGE" log_verbose "Effective stats page setting: $STATS_PAGE" log_verbose "Effective tarball setting: $TARBALL_INCLUDE" log_verbose "Effective sync delete setting: $SYNC_DELETE" diff --git a/src/lib/album-render.source.sh b/src/lib/album-render.source.sh index 7cfff0c..a6012bb 100644 --- a/src/lib/album-render.source.sh +++ b/src/lib/album-render.source.sh @@ -223,6 +223,10 @@ render_view_page() { tarball_name "$tarball_name" } +# Render one photo's *-details.html page. Only called by +# render_photo_view_and_details when DETAILS_PAGE=yes; callers must not invoke +# this directly for a photo when details are disabled, or a dangling file would +# be produced with no page linking to it. render_details_page() { local -r html_dir="$1"; shift local -r photos_dir="$1"; shift @@ -285,15 +289,22 @@ render_photo_view_and_details() { "$page_num" \ "$preview_num" \ "$photo" - render_details_page \ - "$html_dir" \ - "$photos_dir" \ - "$blurs_dir" \ - "$backhref" \ - "$tarball_name" \ - "$page_num" \ - "$preview_num" \ - "$photo" + + # DETAILS_PAGE=no skips the *-details.html file entirely (view.tmpl's Details + # link is likewise suppressed via render_details_page_html, so nothing links + # to it). This is the only place a photo's details page would be rendered, so + # skipping the call here is sufficient to omit the file for every photo. + if [ "$DETAILS_PAGE" = yes ]; then + render_details_page \ + "$html_dir" \ + "$photos_dir" \ + "$blurs_dir" \ + "$backhref" \ + "$tarball_name" \ + "$page_num" \ + "$preview_num" \ + "$photo" + fi } record_rendered_view_page() { @@ -310,32 +321,78 @@ record_rendered_view_page() { last_views_ref["$page"]="$preview" } -# Navigation-redirect count single source of truth (task nr0). Every view page -# gets ALBUM_REDIRECTS_PER_PAGE wrap-around redirect files: the prev stub -# (N-0.html) and its details twin, plus the next stub (N-(last+1).html) and its -# details twin -- four files, emitted by render_page_view_redirects below for -# every page. The LAST page additionally emits ALBUM_REDIRECTS_LAST_PAGE_EXTRA -# files: the 0-MAXPREVIEWS / 0-MAXPREVIEWS-details entry stubs that bounce into -# the album. Keep these two numbers in lockstep with render_page_view_redirects; -# the dry-run plan predicts redirect_count from them via -# album_redirect_count_for_page_count instead of a magic "*4+2". -declare -gri ALBUM_REDIRECTS_PER_PAGE=4 -declare -gri ALBUM_REDIRECTS_LAST_PAGE_EXTRA=2 +# Navigation-redirect count single source of truth (task nr0; extended for +# DETAILS_PAGE by task 6v0). Every view page gets ALBUM_VIEW_REDIRECTS_PER_PAGE +# wrap-around redirect files: the prev stub (N-0.html) and the next stub +# (N-(last+1).html). When DETAILS_PAGE=yes, each of those also gets a +# "-details" twin, adding ALBUM_DETAILS_REDIRECTS_PER_PAGE more -- so +# render_page_view_redirects emits 2 files per page with details disabled, 4 +# with them enabled. The LAST page additionally emits +# ALBUM_VIEW_REDIRECTS_LAST_PAGE_EXTRA entry stubs (plus their details twins +# when enabled): the 0-MAXPREVIEWS / loop-to-1 stubs that bounce into the +# album. Keep these numbers in lockstep with render_page_view_redirects; the +# dry-run plan predicts redirect_count from them via +# album_redirect_count_for_page_count instead of a magic formula. +declare -gri ALBUM_VIEW_REDIRECTS_PER_PAGE=2 +declare -gri ALBUM_DETAILS_REDIRECTS_PER_PAGE=2 +declare -gri ALBUM_VIEW_REDIRECTS_LAST_PAGE_EXTRA=1 +declare -gri ALBUM_DETAILS_REDIRECTS_LAST_PAGE_EXTRA=1 + +# Per-page / last-page-extra redirect file counts for the CURRENT DETAILS_PAGE +# setting: the view-only counts, plus the details counts when DETAILS_PAGE=yes. +# Split out of album_redirect_count_for_page_count so that function stays a +# short arithmetic one-liner. +album_redirects_per_page() { + local -i count=$ALBUM_VIEW_REDIRECTS_PER_PAGE + + if [ "$DETAILS_PAGE" = yes ]; then + count+=$ALBUM_DETAILS_REDIRECTS_PER_PAGE + fi + printf '%d\n' "$count" +} + +album_redirects_last_page_extra() { + local -i count=$ALBUM_VIEW_REDIRECTS_LAST_PAGE_EXTRA + + if [ "$DETAILS_PAGE" = yes ]; then + count+=$ALBUM_DETAILS_REDIRECTS_LAST_PAGE_EXTRA + fi + printf '%d\n' "$count" +} # Total navigation redirects a run produces for a given number of preview pages: -# four per page plus the last page's extra entry stubs. Zero pages -> zero -# redirects (render_view_redirects returns early on an empty album). This is the -# count render_page_view_redirects actually writes across all pages, expressed -# once so the dry-run plan cannot drift from real generation. +# album_redirects_per_page per page plus the last page's extra entry stubs. +# Zero pages -> zero redirects (render_view_redirects returns early on an empty +# album). This is the count render_page_view_redirects actually writes across +# all pages, expressed once so the dry-run plan cannot drift from real +# generation. album_redirect_count_for_page_count() { local -ri page_count="$1"; shift + local -i per_page + local -i last_page_extra if (( page_count <= 0 )); then printf '0\n' return fi - printf '%d\n' "$(( page_count * ALBUM_REDIRECTS_PER_PAGE \ - + ALBUM_REDIRECTS_LAST_PAGE_EXTRA ))" + per_page=$(album_redirects_per_page) + last_page_extra=$(album_redirects_last_page_extra) + printf '%d\n' "$(( page_count * per_page + last_page_extra ))" +} + +# Render one details-redirect stub, but only when DETAILS_PAGE=yes (mirrors +# render_photo_view_and_details skipping render_details_page entirely). A no-op +# under DETAILS_PAGE=no keeps every "-details.html" navigation stub from ever +# being written, so no generated page can link to a missing one. +_render_details_redirect() { + local -r html_dir="$1"; shift + local -r filename="$1"; shift + local -r target="$1"; shift + + if [ "$DETAILS_PAGE" != yes ]; then + return + fi + template redirect "$filename" html_dir "$html_dir" redirect_page "$target" } # Render every navigation redirect for a single view page (the prev/next @@ -345,7 +402,7 @@ album_redirect_count_for_page_count() { # files for distinct pages are produced here. The wrap-around redirects for the # very last page (0-MAXPREVIEWS and the loop-to-1 links) are emitted as part of # that page's group. Per-page / last-page file counts are fixed by -# ALBUM_REDIRECTS_PER_PAGE / ALBUM_REDIRECTS_LAST_PAGE_EXTRA above. +# album_redirects_per_page / album_redirects_last_page_extra above. render_page_view_redirects() { local -r html_dir="$1"; shift local -ri page="$1"; shift @@ -357,30 +414,26 @@ render_page_view_redirects() { template redirect "$prevredirect.html" \ html_dir "$html_dir" \ redirect_page "$(( page - 1 ))-${MAXPREVIEWS}" - template redirect "$prevredirect-details.html" \ - html_dir "$html_dir" \ - redirect_page "$(( page - 1 ))-${MAXPREVIEWS}-details" + _render_details_redirect "$html_dir" "$prevredirect-details.html" \ + "$(( page - 1 ))-${MAXPREVIEWS}-details" if (( page == max_page )); then template redirect "0-$MAXPREVIEWS.html" \ html_dir "$html_dir" \ redirect_page "${page}-$lastview" - template redirect "0-$MAXPREVIEWS-details.html" \ - html_dir "$html_dir" \ - redirect_page "${page}-$lastview-details" + _render_details_redirect "$html_dir" "0-$MAXPREVIEWS-details.html" \ + "${page}-$lastview-details" template redirect "$nextredirect.html" \ html_dir "$html_dir" \ redirect_page '1-1' - template redirect "$nextredirect-details.html" \ - html_dir "$html_dir" \ - redirect_page '1-1-details' + _render_details_redirect "$html_dir" "$nextredirect-details.html" \ + '1-1-details' else template redirect "$nextredirect.html" \ html_dir "$html_dir" \ redirect_page "$(( page + 1 ))-1" - template redirect "$nextredirect-details.html" \ - html_dir "$html_dir" \ - redirect_page "$(( page + 1 ))-1-details" + _render_details_redirect "$html_dir" "$nextredirect-details.html" \ + "$(( page + 1 ))-1-details" fi } diff --git a/src/lib/bootstrap.source.sh b/src/lib/bootstrap.source.sh index 66193e3..1cb40eb 100644 --- a/src/lib/bootstrap.source.sh +++ b/src/lib/bootstrap.source.sh @@ -34,6 +34,8 @@ usage() { --random-seed VALUE --splash --no-splash + --details + --no-details --stats --no-stats --shuffle diff --git a/src/lib/config.spec.source.sh b/src/lib/config.spec.source.sh index d4778a9..764cba6 100644 --- a/src/lib/config.spec.source.sh +++ b/src/lib/config.spec.source.sh @@ -73,6 +73,7 @@ declare -gra CONFIG_SPECS=( 'RANDOM_SEED||yes|yes||scalar' 'SHUFFLE|no|yes|yes|yesno|scalar' 'SPLASH_PAGE|yes|yes|yes|yesno|scalar' + 'DETAILS_PAGE|yes|yes|yes|yesno|scalar' 'STATS_PAGE|no|yes|yes|yesno|scalar' 'TARBALL_INCLUDE|yes|yes|yes|yesno|scalar' 'TARBALL_SUFFIX|.tar|yes|no||scalar' diff --git a/src/lib/config.validate.source.sh b/src/lib/config.validate.source.sh index 3ca720a..345ec6d 100644 --- a/src/lib/config.validate.source.sh +++ b/src/lib/config.validate.source.sh @@ -113,8 +113,20 @@ validate_template_file() { validate_template_dir() { local template_name - local -a required_templates=( - details + local -a required_templates=() + + validate_template_dir_access || return + + # details.tmpl is only needed when DETAILS_PAGE=yes actually renders it + # (mirrors the SPLASH_PAGE=yes conditional below), so an album that opts out + # of details pages does not need to keep a details.tmpl around at all. Kept + # first in the list (as it always was) so the reporting order for the + # unconditional templates below is unchanged from before DETAILS_PAGE + # existed. + if [ "$DETAILS_PAGE" = yes ]; then + required_templates+=(details) + fi + required_templates+=( footer header next @@ -125,8 +137,6 @@ validate_template_dir() { view ) - validate_template_dir_access || return - if [ "$SPLASH_PAGE" = yes ]; then required_templates+=(splash) fi @@ -290,6 +300,7 @@ validate_common_config() { SYNC_TIMEOUT SHUFFLE SPLASH_PAGE + DETAILS_PAGE STATS_PAGE TARBALL_INCLUDE FAVICON diff --git a/src/lib/dry-run.source.sh b/src/lib/dry-run.source.sh index eaaffeb..4bdb66e 100644 --- a/src/lib/dry-run.source.sh +++ b/src/lib/dry-run.source.sh @@ -39,9 +39,14 @@ collect_dry_run_page_plan() { # files are touched here, so dry-run stays side-effect free. page_count=$(album_page_count_for_image_count "$image_count") redirect_count=$(album_redirect_count_for_page_count "$page_count") - plan_ref["details_count"]="$image_count" plan_ref["page_count"]="$page_count" plan_ref["redirect_count"]="$redirect_count" + # One details page per photo, but only when DETAILS_PAGE=yes -- mirrors + # render_photo_view_and_details skipping render_details_page entirely, + # so the plan cannot drift from what --generate actually writes. + if [ "$DETAILS_PAGE" = yes ]; then + plan_ref["details_count"]="$image_count" + fi fi } @@ -86,6 +91,7 @@ collect_dry_run_plan() { plan_ref["random_seed"]="$RANDOM_SEED" plan_ref["shuffle"]="$SHUFFLE" plan_ref["splash_page"]="$SPLASH_PAGE" + plan_ref["details_page"]="$DETAILS_PAGE" plan_ref["stats_page"]="$STATS_PAGE" plan_ref["image_count"]="$image_count" plan_ref["tarball_include"]="$TARBALL_INCLUDE" @@ -120,14 +126,17 @@ _print_dry_run_settings() { printf 'Random seed: %s\n' "${plan_ref["random_seed"]}" printf 'Shuffle: %s\n' "${plan_ref["shuffle"]}" printf 'Splash page: %s\n' "${plan_ref["splash_page"]}" + printf 'Details page: %s\n' "${plan_ref["details_page"]}" printf 'Stats page: %s\n' "${plan_ref["stats_page"]}" printf 'Image count: %s\n' "${plan_ref["image_count"]}" printf 'Tarball setting: %s\n' "${plan_ref["tarball_include"]}" printf 'Tarball name plan: %s\n' "${plan_ref["tarball_name_plan"]}" } -# Print the planned directories and generated-files listing (index/favicon/json, -# image dirs, page/view/details/redirect counts, optional stats + tarball lines). +# Print the planned directories, plus the generated-files lines that are always +# present regardless of any page toggle (index/favicon/json/image dirs/page/view +# counts). Split from the optional-lines half below (_print_dry_run_optional_files) +# so each stays around 30 lines, matching the generation-metadata JSON split. _print_dry_run_files() { local -r plan_name="$1"; shift # shellcheck disable=SC2178 @@ -159,8 +168,21 @@ _print_dry_run_files() { "${plan_ref["dist_dir"]}" "${plan_ref["page_count"]}" printf ' %s/[page]-[image].html (%s view pages)\n' \ "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}" - printf ' %s/[page]-[image]-details.html (%s details pages)\n' \ - "${plan_ref["dist_dir"]}" "${plan_ref["details_count"]}" +} + +# Print the generated-files lines gated behind a page toggle (details, the +# navigation redirect count that itself depends on DETAILS_PAGE, stats, and the +# tarball). Split out of _print_dry_run_files (see its comment) purely to keep +# both halves short; output is unchanged from the previous single function. +_print_dry_run_optional_files() { + local -r plan_name="$1"; shift + # shellcheck disable=SC2178 + local -n plan_ref="$plan_name" + + if [ "${plan_ref["details_page"]}" = yes ]; then + printf ' %s/[page]-[image]-details.html (%s details pages)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["details_count"]}" + fi printf ' %s/[redirect].html (%s navigation redirects)\n' \ "${plan_ref["dist_dir"]}" "${plan_ref["redirect_count"]}" if [ "${plan_ref["stats_page"]}" = yes ]; then @@ -184,4 +206,5 @@ print_dry_run_plan() { _print_dry_run_settings "$plan_name" _print_dry_run_files "$plan_name" + _print_dry_run_optional_files "$plan_name" } diff --git a/src/lib/generation-metadata.source.sh b/src/lib/generation-metadata.source.sh index f70bc23..20f5d09 100644 --- a/src/lib/generation-metadata.source.sh +++ b/src/lib/generation-metadata.source.sh @@ -44,6 +44,7 @@ _collect_generation_metadata() { _GENERATION_METADATA["settings_random_seed"]="$RANDOM_SEED" _GENERATION_METADATA["settings_shuffle"]="$SHUFFLE" _GENERATION_METADATA["settings_splash_page"]="$SPLASH_PAGE" + _GENERATION_METADATA["settings_details_page"]="$DETAILS_PAGE" _GENERATION_METADATA["settings_stats_page"]="$STATS_PAGE" _GENERATION_METADATA["settings_original_basepath"]="$ORIGINAL_BASEPATH" } @@ -115,6 +116,8 @@ _generation_metadata_json_settings() { "$(json_bool "${_GENERATION_METADATA["settings_shuffle"]}")" printf ' "splash_page": %s,\n' \ "$(json_bool "${_GENERATION_METADATA["settings_splash_page"]}")" + printf ' "details_page": %s,\n' \ + "$(json_bool "${_GENERATION_METADATA["settings_details_page"]}")" printf ' "stats_page": %s,\n' \ "$(json_bool "${_GENERATION_METADATA["settings_stats_page"]}")" printf ' "original_basepath": %s\n' \ diff --git a/src/lib/stats-filter-album.source.sh b/src/lib/stats-filter-album.source.sh index da37886..09dffb1 100644 --- a/src/lib/stats-filter-album.source.sh +++ b/src/lib/stats-filter-album.source.sh @@ -22,7 +22,9 @@ # within that filter. The "--" suffix cannot collide with another gallery # name because a pagebase never contains "--". All pages reuse the album's shared # photos/thumbs/blurs assets (only the HTML differs); view pages link "Details" -# to the album's own details page via the album_view_page_for_photo accessor. +# to the album's own details page via the album_view_page_for_photo accessor, +# but only when DETAILS_PAGE=yes actually rendered that page (see +# _stats_build_filterview_body below). # Pages render in # parallel through the shared job pool, throttled to IMAGE_JOBS. The galleries # reuse camera.tmpl and the view pages reuse cameraview.tmpl. @@ -165,7 +167,11 @@ _stats_build_filterview_body() { tooltip_attr=" title=\"$(html_escape "$tooltip")\"" fi view_page=$(album_view_page_for_photo "$photo") - if [ -n "$view_page" ]; then + # Only link to the album's details page when it was actually rendered + # (DETAILS_PAGE=yes); otherwise album_view_page_for_photo resolving a page + # would still point at a details file that render_photo_view_and_details + # never wrote, leaving a dangling link. + if [ -n "$view_page" ] && [ "$DETAILS_PAGE" = yes ]; then details_link=$(printf \ ' Details |' \ "$backhref_html" "$view_page") diff --git a/src/lib/template.source.sh b/src/lib/template.source.sh index c66e955..8938a1d 100644 --- a/src/lib/template.source.sh +++ b/src/lib/template.source.sh @@ -240,6 +240,7 @@ declare -ra TEMPLATE_RENDER_FIELD_SPECS=( 'render_cameraview_body_html|context_raw|cameraview_body|cameraview_body|cameraview' 'render_camera_thumbs_html|context_raw|camera_thumbs|camera_thumbs|camera' 'render_current_date_text|current_date_html|||header' + 'render_details_page_html|config_html|DETAILS_PAGE||view' 'render_enter_page_html|context_html|enter_page|enter_page|splash' 'render_exif_details_html|context_raw|exif_details|exif_details|details' 'render_exif_tooltip_html|context_html|exif_tooltip|exif_tooltip|details view' diff --git a/src/shuriken.default.conf b/src/shuriken.default.conf index eaa30a4..79413a5 100644 --- a/src/shuriken.default.conf +++ b/src/shuriken.default.conf @@ -25,6 +25,13 @@ IMAGEMAGICK_TIMEOUT=60 # SHUFFLE=yes # Generate a splash landing page at index.html. SPLASH_PAGE=yes +# Generate a per-photo *-details.html page (the EXIF details view reachable via +# the "Details" link on each photo's view page) for every photo. Can be yes or +# no. On by default; set to no (or pass --no-details) to skip these pages and +# their "Details" links -- the normal thumbnail overview and per-photo view +# pages, EXIF tooltips, and the EXIF stats site are unaffected and stay +# independently controlled by STATS_PAGE. +DETAILS_PAGE=yes # Generate an EXIF stats site (stats/ overview plus per-camera and filter # mini-albums) and show the Stats navigation link in page headers. Can be yes or # no. Off by default; set to yes (or pass --stats) to enable it. diff --git a/src/shuriken.sh b/src/shuriken.sh index e26bbf8..65dae43 100755 --- a/src/shuriken.sh +++ b/src/shuriken.sh @@ -55,6 +55,8 @@ declare -Ar CLI_OPTION_SPEC=( [--no-shuffle]='kind=flag value=no config=SHUFFLE' [--splash]='kind=flag value=yes config=SPLASH_PAGE' [--no-splash]='kind=flag value=no config=SPLASH_PAGE' + [--details]='kind=flag value=yes config=DETAILS_PAGE' + [--no-details]='kind=flag value=no config=DETAILS_PAGE' [--stats]='kind=flag value=yes config=STATS_PAGE' [--no-stats]='kind=flag value=no config=STATS_PAGE' [--tarball]='kind=flag value=yes config=TARBALL_INCLUDE' diff --git a/tests/cli.sh b/tests/cli.sh index 73748b2..52952d5 100755 --- a/tests/cli.sh +++ b/tests/cli.sh @@ -144,6 +144,7 @@ assert metadata["settings"]["feature_percent"] == "10" assert metadata["settings"]["image_jobs"] == "3" assert metadata["settings"]["shuffle"] is False assert isinstance(metadata["settings"]["splash_page"], bool) +assert isinstance(metadata["settings"]["details_page"], bool) assert isinstance(metadata["settings"]["stats_page"], bool) assert "original_basepath" in metadata["settings"] PY @@ -1729,6 +1730,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED='' SHUFFLE=no SPLASH_PAGE=yes +DETAILS_PAGE=yes STATS_PAGE=no TARBALL_INCLUDE=yes TARBALL_SUFFIX=.tar @@ -1781,6 +1783,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED='' SHUFFLE=no SPLASH_PAGE=yes +DETAILS_PAGE=yes STATS_PAGE=no TARBALL_INCLUDE=yes TARBALL_SUFFIX=.tar @@ -1926,6 +1929,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED='' SHUFFLE=yes SPLASH_PAGE=yes +DETAILS_PAGE=yes STATS_PAGE=no TARBALL_INCLUDE=no TARBALL_SUFFIX=.tar @@ -1973,6 +1977,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED='' SHUFFLE=no SPLASH_PAGE=yes +DETAILS_PAGE=yes STATS_PAGE=no TARBALL_INCLUDE=no TARBALL_SUFFIX=.tar @@ -2047,6 +2052,7 @@ IMAGEMAGICK_TIMEOUT=60 RANDOM_SEED=cli-seed SHUFFLE=yes SPLASH_PAGE=no +DETAILS_PAGE=yes STATS_PAGE=no TARBALL_INCLUDE=yes TARBALL_SUFFIX=.tar @@ -2084,15 +2090,40 @@ test_print_config_applies_negative_cli_overrides() { output=$( cd "$TEST_TMPDIR" "$TEST_SHURIKEN" \ - --print-config --no-shuffle --no-splash --no-tarball + --print-config --no-shuffle --no-splash --no-details --no-tarball ) test::assert_contains 'SHUFFLE=no' "$output" test::assert_contains 'SPLASH_PAGE=no' "$output" + test::assert_contains 'DETAILS_PAGE=no' "$output" test::assert_contains 'TARBALL_INCLUDE=no' "$output" test::teardown } +# task 6v0: --details must override a config file that turns details pages off, +# proving the positive direction of the CLI flag (the negative direction, +# --no-details overriding a config that leaves it at the yes default, is +# covered by test_print_config_applies_negative_cli_overrides above). +test_print_config_applies_details_cli_override_over_config_no() { + local config_file + local output + + test::setup + config_file="$TEST_TMPDIR/shuriken.conf" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'Details override' 40 + printf 'DETAILS_PAGE=no\n' >> "$config_file" + + output=$( + cd "$TEST_TMPDIR" + "$TEST_SHURIKEN" --print-config --details + ) + + test::assert_contains 'DETAILS_PAGE=yes' "$output" + test::teardown +} + test_print_config_normalizes_scalar_and_array_tar_opts() { local array_config local array_output @@ -2357,6 +2388,38 @@ test_dry_run_no_stats_omits_stats_plan() { test::teardown } +test_dry_run_no_details_omits_details_plan() { + local config_file + local dist_dir + local fake_bin + local output + + test::setup + fake_bin="$TEST_TMPDIR/bin" + config_file="$TEST_TMPDIR/shuriken.conf" + dist_dir="$TEST_TMPDIR/dist" + + test::install_fake_imagemagick "$fake_bin" + PATH="$fake_bin:$PATH" \ + test::generate_fixture_images "$TEST_TMPDIR/incoming" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$dist_dir" 'Dry no details' 40 + + output=$( + cd "$TEST_TMPDIR" + PATH="$fake_bin:$PATH" "$TEST_SHURIKEN" --dry-run --no-details + ) + + test::assert_contains 'Details page: no' "$output" + test::assert_not_contains 'details.html' "$output" + # 6 photos with MAXPREVIEWS=40 is a single page: 2 view-only wrap-around + # redirects plus 1 last-page entry stub = 3, instead of 6 with details + # twins (album_redirect_count_for_page_count halves with DETAILS_PAGE=no). + test::assert_contains '[redirect].html (3 navigation redirects)' "$output" + test::assert_path_absent "$dist_dir" + test::teardown +} + test_dry_run_reports_empty_plan_without_writes() { local config_file local dist_dir @@ -3142,6 +3205,7 @@ test_generate_preflight_rejects_invalid_yes_no_values() { local -a bool_vars=( SHUFFLE SPLASH_PAGE + DETAILS_PAGE TARBALL_INCLUDE ) @@ -3711,6 +3775,42 @@ test_generate_cli_no_splash_overrides_config() { test::teardown } +# task 6v0: mirrors test_generate_cli_no_splash_overrides_config -- validate_template_dir +# only requires details.tmpl when DETAILS_PAGE=yes would actually render it, so +# an album that disables details pages does not need to keep that template. +test_generate_cli_no_details_allows_missing_details_template() { + local config_file + local fake_bin + local template_dir + local view_html + + test::setup + fake_bin="$TEST_TMPDIR/bin" + config_file="$TEST_TMPDIR/shuriken.conf" + template_dir="$TEST_TMPDIR/templates" + + test::install_fake_imagemagick "$fake_bin" + PATH="$fake_bin:$PATH" \ + test::generate_fixture_images "$TEST_TMPDIR/incoming" + cp -R "$TEST_REPO_ROOT/share/templates/default" "$template_dir" + rm -f "$template_dir/details.tmpl" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'No details CLI album' 40 + printf 'TEMPLATE_DIR=%q\n' "$template_dir" >> "$config_file" + + ( + cd "$TEST_TMPDIR" + PATH="$fake_bin:$PATH" "$TEST_SHURIKEN" \ + --generate --no-details + ) + + test::assert_path_absent "$TEST_TMPDIR/dist/1-1-details.html" + view_html=$(<"$TEST_TMPDIR/dist/1-1.html") + test::assert_not_contains 'Details' "$view_html" + test::teardown +} + test_generate_uses_custom_favicon() { local config_file local fake_bin @@ -3895,6 +3995,87 @@ PY test::teardown } +# task 6v0: DETAILS_PAGE=no must omit every *-details.html file (view pages, +# navigation redirects, and stats filter mini-albums alike) and every link that +# would point at one, while leaving the normal thumbnail overview, per-photo +# view pages, and EXIF tooltips untouched. Runs the same generation under both +# STATS_PAGE settings (acceptance criterion: DETAILS_PAGE=no works with stats +# either on or off) so the stats site's own Details links are proven gone too. +test_generate_no_details_omits_pages_and_links() { + local config_file + local fake_bin + local -i dangling_link_count + local -i details_file_count + local stats_flag + local view_html + + for stats_flag in --stats --no-stats; do + test::setup + fake_bin="$TEST_TMPDIR/bin" + config_file="$TEST_TMPDIR/shuriken.conf" + + test::install_fake_imagemagick "$fake_bin" + PATH="$fake_bin:$PATH" \ + test::generate_fixture_images "$TEST_TMPDIR/incoming" + test::write_album_config \ + "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \ + 'No details album' 40 + + ( + cd "$TEST_TMPDIR" + PATH="$fake_bin:$PATH" \ + TEST_IMAGEMAGICK_IDENTIFY_OUTPUT="$(test::stats_identify_output)" \ + "$TEST_SHURIKEN" --generate --no-details "$stats_flag" \ + --random-seed details-seed + ) + + # No *-details.html file anywhere in the output tree (main album, + # navigation redirects, or a stats filter mini-album). + details_file_count=$( + find "$TEST_TMPDIR/dist" -name '*-details.html' | wc -l + ) + test "$details_file_count" -eq 0 + + # No generated page links to a "*-details.html" file (a dangling link + # would exist if any template still emitted the "Details" href). The + # "|| true" keeps a no-match grep (exit 1) from tripping errexit on + # this assignment -- zero dangling links is the expected case. + dangling_link_count=$( + { grep -RFl -- '-details.html' "$TEST_TMPDIR/dist" || true; } \ + | wc -l + ) + test "$dangling_link_count" -eq 0 + + # Normal per-photo view pages still render, and the EXIF tooltip (which + # DETAILS_PAGE must not affect) is still present. + test::assert_file_exists "$TEST_TMPDIR/dist/1-1.html" + view_html=$(<"$TEST_TMPDIR/dist/1-1.html") + test::assert_not_contains 'Details' "$view_html" + test::assert_contains 'title="Camera: ' "$view_html" + + if [ "$stats_flag" = --stats ]; then + # STATS_PAGE stays independently controlled: the stats site and its + # filter mini-albums still generate, just without a Details link. + test::assert_file_exists "$TEST_TMPDIR/dist/stats/index.html" + else + test::assert_path_absent "$TEST_TMPDIR/dist/stats" + fi + + python3 - "$TEST_TMPDIR/dist/shuriken.json" "$stats_flag" <<'PY' +import json +import pathlib +import sys + +metadata_file, stats_flag = sys.argv[1:] +metadata = json.loads(pathlib.Path(metadata_file).read_text()) +assert metadata["settings"]["details_page"] is False +assert metadata["settings"]["stats_page"] is (stats_flag == "--stats") +PY + + test::teardown + done +} + test_refresh_splash_rewrites_only_index_from_existing_assets() { local after_index local after_metadata @@ -7247,6 +7428,9 @@ main() { test::run_case \ '--print-config applies negative CLI overrides' \ test_print_config_applies_negative_cli_overrides + test::run_case \ + '--print-config --details overrides config DETAILS_PAGE=no (6v0)' \ + test_print_config_applies_details_cli_override_over_config_no test::run_case \ '--print-config normalizes scalar and array TAR_OPTS' \ test_print_config_normalizes_scalar_and_array_tar_opts @@ -7265,6 +7449,9 @@ main() { test::run_case \ '--dry-run --no-stats omits stats from the plan' \ test_dry_run_no_stats_omits_stats_plan + test::run_case \ + '--dry-run --no-details omits details from the plan (6v0)' \ + test_dry_run_no_details_omits_details_plan test::run_case \ '--dry-run reports empty plan without writes' \ test_dry_run_reports_empty_plan_without_writes @@ -7343,6 +7530,9 @@ main() { test::run_case \ '--generate --no-splash keeps root index redirect' \ test_generate_cli_no_splash_overrides_config + test::run_case \ + '--generate --no-details allows missing details.tmpl (6v0)' \ + test_generate_cli_no_details_allows_missing_details_template test::run_case \ '--generate --favicon uses a custom favicon' \ test_generate_uses_custom_favicon @@ -7352,6 +7542,9 @@ main() { test::run_case \ '--generate --no-stats suppresses stats pages and nav link' \ test_generate_no_stats_suppresses_pages_and_nav + test::run_case \ + '--generate --no-details omits details pages and links (6v0)' \ + test_generate_no_details_omits_pages_and_links test::run_case \ '--refresh-splash rewrites only root index from existing assets' \ test_refresh_splash_rewrites_only_index_from_existing_assets -- cgit v1.2.3