summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-15 23:36:14 +0300
committerPaul Buetow <paul@buetow.org>2026-06-15 23:50:41 +0300
commit72d24e3b13560f94bdcfd986fda47b20f0ad4227 (patch)
tree1e8b7e8e733214b0ed151e4bf7478ce20c9dccdb
parent06b910863906faa5f01d7f7067cdbaa8af89dfba (diff)
Make the favicon configurable via FAVICON config / --favicon flag
The generated pages link a favicon.ico that was always the bundled shuriken favicon (copy_site_favicon hard-copied share/shuriken/assets/favicon.ico). Add a FAVICON config variable and a --favicon PATH CLI flag: when set, that file is published as favicon.ico instead of the bundled default; when empty, the bundled favicon is used as before. Plumbed through apply_config_defaults, CLI_OPTION_SPEC + override allowlist, usage, print_config, the action config list and effective-setting log, and validated (a non-empty FAVICON must be a readable file) before generation. shuriken.default.conf and the README document it; a test covers a custom favicon, its appearance in --print-config, and rejection of a missing file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--README.md14
-rwxr-xr-xbin/shuriken32
-rw-r--r--src/lib/action.source.sh2
-rw-r--r--src/lib/album.source.sh10
-rw-r--r--src/lib/bootstrap.source.sh1
-rw-r--r--src/lib/config.print.source.sh1
-rw-r--r--src/lib/config.source.sh3
-rw-r--r--src/lib/config.validate.source.sh13
-rw-r--r--src/shuriken.default.conf4
-rwxr-xr-xsrc/shuriken.sh2
-rwxr-xr-xtests/cli.sh51
11 files changed, 124 insertions, 9 deletions
diff --git a/README.md b/README.md
index 219d051..9e9dc81 100644
--- a/README.md
+++ b/README.md
@@ -74,9 +74,9 @@ When `--config PATH` is not provided, `--generate`, `--dry-run`,
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`, `TITLE`, `HEIGHT`, `THUMBHEIGHT`, `MAXPREVIEWS`,
-`IMAGE_JOBS`, `IMAGEMAGICK_TIMEOUT`, `RANDOM_SEED`, `SHUFFLE`, `SPLASH_PAGE`,
-`STATS_PAGE`, `TARBALL_INCLUDE`, `TAR_TIMEOUT`, `SYNC_DELETE`, and
+`DIST_DIR`, `TEMPLATE_DIR`, `FAVICON`, `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
@@ -95,7 +95,8 @@ 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`, `TITLE`, `HEIGHT`,
+`CONFIG_SOURCE`, `INCOMING_DIR`, `DIST_DIR`, `TEMPLATE_DIR`, `FAVICON`, `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`,
@@ -110,7 +111,9 @@ 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.
+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`).
Normal generation preserves reusable generated artifacts from the previous
`DIST_DIR` while still rerendering HTML, random splash/background choices,
@@ -139,6 +142,7 @@ The following long options override config values:
| `--incoming PATH` | `INCOMING_DIR` |
| `--dist PATH` | `DIST_DIR` |
| `--template PATH` | `TEMPLATE_DIR` |
+| `--favicon PATH` | `FAVICON` |
| `--title TEXT` | `TITLE` |
| `--height VALUE` | `HEIGHT` |
| `--thumbheight VALUE` | `THUMBHEIGHT` |
diff --git a/bin/shuriken b/bin/shuriken
index 71c2407..84fae84 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -32,6 +32,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=(
INCOMING_DIR
DIST_DIR
TEMPLATE_DIR
+ FAVICON
TITLE
HEIGHT
THUMBHEIGHT
@@ -46,6 +47,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=(
)
declare -Ar CLI_OPTION_SPEC=(
[--config]='kind=value target=SHURIKEN_CLI_CONFIG_FILE argument=path'
+ [--favicon]='kind=value config=FAVICON argument=path'
[--incoming]='kind=value config=INCOMING_DIR'
[--dist]='kind=value config=DIST_DIR'
[--template]='kind=value config=TEMPLATE_DIR'
@@ -102,6 +104,7 @@ usage() {
--incoming PATH
--dist PATH
--template PATH
+ --favicon PATH
--title TEXT
--height VALUE
--thumbheight VALUE
@@ -2562,8 +2565,14 @@ copy_site_favicon() {
local asset_dir
local favicon_src
- asset_dir=$(resolve_default_asset_dir)
- favicon_src="$asset_dir/favicon.ico"
+ # Use the configured favicon when set, otherwise the bundled default. Either
+ # way it is published as favicon.ico (the name the templates link to).
+ if [ -n "${FAVICON:-}" ]; then
+ favicon_src="$FAVICON"
+ else
+ asset_dir=$(resolve_default_asset_dir)
+ favicon_src="$asset_dir/favicon.ico"
+ fi
if [ ! -r "$favicon_src" ]; then
config_error "favicon file $favicon_src must be readable"
@@ -4098,6 +4107,9 @@ missing_config() {
}
apply_config_defaults() {
+ # Empty FAVICON means use the bundled default favicon; otherwise it is a path
+ # to a custom favicon file copied into the album as favicon.ico.
+ FAVICON="${FAVICON:-}"
HEIGHT="${HEIGHT:-}"
IMAGE_JOBS="${IMAGE_JOBS:-3}"
IMAGEMAGICK_TIMEOUT="${IMAGEMAGICK_TIMEOUT:-60}"
@@ -4148,6 +4160,7 @@ print_config() {
print_shell_assignment INCOMING_DIR "$INCOMING_DIR"
print_shell_assignment DIST_DIR "$DIST_DIR"
print_shell_assignment TEMPLATE_DIR "$TEMPLATE_DIR"
+ print_shell_assignment FAVICON "$FAVICON"
print_shell_assignment TITLE "$TITLE"
print_shell_assignment HEIGHT "$HEIGHT"
print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT"
@@ -4625,6 +4638,19 @@ validate_common_config() {
validate_yes_no_config_var SPLASH_PAGE || return
validate_yes_no_config_var STATS_PAGE || return
validate_yes_no_config_var TARBALL_INCLUDE || return
+ validate_favicon_config || return
+}
+
+# A custom FAVICON (when set) must be a readable file; empty means the bundled
+# default favicon is used.
+validate_favicon_config() {
+ if [ -z "${FAVICON:-}" ]; then
+ return
+ fi
+ if [ ! -f "$FAVICON" ] || [ ! -r "$FAVICON" ]; then
+ config_error "FAVICON file $FAVICON must be a readable file"
+ return 1
+ fi
}
validate_generation_config() {
@@ -4882,6 +4908,7 @@ run_action_body_context() {
INCOMING_DIR
DIST_DIR
TEMPLATE_DIR
+ FAVICON
TITLE
HEIGHT
THUMBHEIGHT
@@ -4995,6 +5022,7 @@ log_configured_action() {
log_verbose "Effective incoming directory: ${INCOMING_DIR:-}"
log_verbose "Effective output directory: ${DIST_DIR:-}"
log_verbose "Effective template directory: ${TEMPLATE_DIR:-}"
+ log_verbose "Effective favicon: ${FAVICON:-(bundled default)}"
log_verbose "Effective image jobs: $IMAGE_JOBS"
log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT}s"
log_verbose "Effective tar timeout: ${TAR_TIMEOUT}s"
diff --git a/src/lib/action.source.sh b/src/lib/action.source.sh
index e2649d7..12be053 100644
--- a/src/lib/action.source.sh
+++ b/src/lib/action.source.sh
@@ -42,6 +42,7 @@ run_action_body_context() {
INCOMING_DIR
DIST_DIR
TEMPLATE_DIR
+ FAVICON
TITLE
HEIGHT
THUMBHEIGHT
@@ -155,6 +156,7 @@ log_configured_action() {
log_verbose "Effective incoming directory: ${INCOMING_DIR:-}"
log_verbose "Effective output directory: ${DIST_DIR:-}"
log_verbose "Effective template directory: ${TEMPLATE_DIR:-}"
+ log_verbose "Effective favicon: ${FAVICON:-(bundled default)}"
log_verbose "Effective image jobs: $IMAGE_JOBS"
log_verbose "Effective ImageMagick timeout: ${IMAGEMAGICK_TIMEOUT}s"
log_verbose "Effective tar timeout: ${TAR_TIMEOUT}s"
diff --git a/src/lib/album.source.sh b/src/lib/album.source.sh
index 532c218..08509fb 100644
--- a/src/lib/album.source.sh
+++ b/src/lib/album.source.sh
@@ -1047,8 +1047,14 @@ copy_site_favicon() {
local asset_dir
local favicon_src
- asset_dir=$(resolve_default_asset_dir)
- favicon_src="$asset_dir/favicon.ico"
+ # Use the configured favicon when set, otherwise the bundled default. Either
+ # way it is published as favicon.ico (the name the templates link to).
+ if [ -n "${FAVICON:-}" ]; then
+ favicon_src="$FAVICON"
+ else
+ asset_dir=$(resolve_default_asset_dir)
+ favicon_src="$asset_dir/favicon.ico"
+ fi
if [ ! -r "$favicon_src" ]; then
config_error "favicon file $favicon_src must be readable"
diff --git a/src/lib/bootstrap.source.sh b/src/lib/bootstrap.source.sh
index a047f5d..2b19e58 100644
--- a/src/lib/bootstrap.source.sh
+++ b/src/lib/bootstrap.source.sh
@@ -15,6 +15,7 @@ usage() {
--incoming PATH
--dist PATH
--template PATH
+ --favicon PATH
--title TEXT
--height VALUE
--thumbheight VALUE
diff --git a/src/lib/config.print.source.sh b/src/lib/config.print.source.sh
index 7b91b60..c5fe2b4 100644
--- a/src/lib/config.print.source.sh
+++ b/src/lib/config.print.source.sh
@@ -27,6 +27,7 @@ print_config() {
print_shell_assignment INCOMING_DIR "$INCOMING_DIR"
print_shell_assignment DIST_DIR "$DIST_DIR"
print_shell_assignment TEMPLATE_DIR "$TEMPLATE_DIR"
+ print_shell_assignment FAVICON "$FAVICON"
print_shell_assignment TITLE "$TITLE"
print_shell_assignment HEIGHT "$HEIGHT"
print_shell_assignment THUMBHEIGHT "$THUMBHEIGHT"
diff --git a/src/lib/config.source.sh b/src/lib/config.source.sh
index 50c331a..c13e807 100644
--- a/src/lib/config.source.sh
+++ b/src/lib/config.source.sh
@@ -30,6 +30,9 @@ missing_config() {
}
apply_config_defaults() {
+ # Empty FAVICON means use the bundled default favicon; otherwise it is a path
+ # to a custom favicon file copied into the album as favicon.ico.
+ FAVICON="${FAVICON:-}"
HEIGHT="${HEIGHT:-}"
IMAGE_JOBS="${IMAGE_JOBS:-3}"
IMAGEMAGICK_TIMEOUT="${IMAGEMAGICK_TIMEOUT:-60}"
diff --git a/src/lib/config.validate.source.sh b/src/lib/config.validate.source.sh
index bd2fccf..fda20f0 100644
--- a/src/lib/config.validate.source.sh
+++ b/src/lib/config.validate.source.sh
@@ -195,6 +195,19 @@ validate_common_config() {
validate_yes_no_config_var SPLASH_PAGE || return
validate_yes_no_config_var STATS_PAGE || return
validate_yes_no_config_var TARBALL_INCLUDE || return
+ validate_favicon_config || return
+}
+
+# A custom FAVICON (when set) must be a readable file; empty means the bundled
+# default favicon is used.
+validate_favicon_config() {
+ if [ -z "${FAVICON:-}" ]; then
+ return
+ fi
+ if [ ! -f "$FAVICON" ] || [ ! -r "$FAVICON" ]; then
+ config_error "FAVICON file $FAVICON must be a readable file"
+ return 1
+ fi
}
validate_generation_config() {
diff --git a/src/shuriken.default.conf b/src/shuriken.default.conf
index c0fe41a..9d0b1a8 100644
--- a/src/shuriken.default.conf
+++ b/src/shuriken.default.conf
@@ -28,6 +28,10 @@ DIST_DIR=$(pwd)/dist
TEMPLATE_DIR=/usr/share/shuriken/templates/default
#TEMPLATE_DIR=/usr/share/shuriken/templates/minimal
+# Custom favicon, published as favicon.ico. Leave unset/empty to use the bundled
+# shuriken favicon. Can also be set with --favicon PATH.
+#FAVICON=$(pwd)/my-favicon.ico
+
# Includes a .tar of the incoming dir in the dist, can be yes or no
TARBALL_INCLUDE=yes
TARBALL_SUFFIX=.tar
diff --git a/src/shuriken.sh b/src/shuriken.sh
index b6a6cc5..36f3b57 100755
--- a/src/shuriken.sh
+++ b/src/shuriken.sh
@@ -32,6 +32,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=(
INCOMING_DIR
DIST_DIR
TEMPLATE_DIR
+ FAVICON
TITLE
HEIGHT
THUMBHEIGHT
@@ -46,6 +47,7 @@ declare -ra CLI_CONFIG_OVERRIDE_TARGETS=(
)
declare -Ar CLI_OPTION_SPEC=(
[--config]='kind=value target=SHURIKEN_CLI_CONFIG_FILE argument=path'
+ [--favicon]='kind=value config=FAVICON argument=path'
[--incoming]='kind=value config=INCOMING_DIR'
[--dist]='kind=value config=DIST_DIR'
[--template]='kind=value config=TEMPLATE_DIR'
diff --git a/tests/cli.sh b/tests/cli.sh
index aa869cc..1dac04a 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -1300,6 +1300,7 @@ CONFIG_SOURCE=$TEST_REPO_ROOT/src/shuriken.default.conf
INCOMING_DIR=$TEST_TMPDIR/incoming
DIST_DIR=$TEST_TMPDIR/dist
TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default
+FAVICON=''
TITLE=A\\ simple\\ Shuriken
HEIGHT=1200
THUMBHEIGHT=300
@@ -1347,6 +1348,7 @@ CONFIG_SOURCE=$config_file
INCOMING_DIR=$TEST_TMPDIR/incoming
DIST_DIR=$TEST_TMPDIR/dist
TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default
+FAVICON=''
TITLE=Minimal\\ defaults
HEIGHT=''
THUMBHEIGHT=30
@@ -1487,6 +1489,7 @@ CONFIG_SOURCE=$config_file
INCOMING_DIR=$TEST_TMPDIR/custom-incoming
DIST_DIR=$TEST_TMPDIR/custom-dist
TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default
+FAVICON=''
TITLE=Selected\\ config
HEIGHT=120
THUMBHEIGHT=30
@@ -1529,6 +1532,7 @@ CONFIG_SOURCE=./shuriken.conf
INCOMING_DIR=$TEST_TMPDIR/incoming
DIST_DIR=$TEST_TMPDIR/dist
TEMPLATE_DIR=$TEST_REPO_ROOT/share/templates/default
+FAVICON=''
TITLE=Current\\ directory\\ config
HEIGHT=120
THUMBHEIGHT=30
@@ -1596,6 +1600,7 @@ CONFIG_SOURCE=./shuriken.conf
INCOMING_DIR=$TEST_TMPDIR/cli-incoming
DIST_DIR=$dist_dir
TEMPLATE_DIR=$TEST_TMPDIR/cli-template
+FAVICON=''
TITLE=CLI\\ title
HEIGHT=456
THUMBHEIGHT=45
@@ -3052,6 +3057,49 @@ test_generate_cli_no_splash_overrides_config() {
test::teardown
}
+test_generate_uses_custom_favicon() {
+ local config_file
+ local fake_bin
+ local favicon_src
+ local output
+
+ test::setup
+ fake_bin="$TEST_TMPDIR/bin"
+ config_file="$TEST_TMPDIR/shuriken.conf"
+ favicon_src="$TEST_TMPDIR/my-favicon.ico"
+
+ test::install_fake_imagemagick "$fake_bin"
+ PATH="$fake_bin:$PATH" \
+ test::generate_fixture_images "$TEST_TMPDIR/incoming"
+ printf 'CUSTOM-FAVICON-MARKER\n' > "$favicon_src"
+ test::write_album_config \
+ "$config_file" "$TEST_TMPDIR/incoming" "$TEST_TMPDIR/dist" \
+ 'Favicon album' 40
+
+ (
+ cd "$TEST_TMPDIR"
+ PATH="$fake_bin:$PATH" "$TEST_SHURIKEN" --generate --favicon "$favicon_src"
+ )
+
+ # The published favicon.ico is the custom file, not the bundled default.
+ test::assert_file_exists "$TEST_TMPDIR/dist/favicon.ico"
+ test "$(<"$TEST_TMPDIR/dist/favicon.ico")" = 'CUSTOM-FAVICON-MARKER'
+ # --print-config reports the configured favicon path.
+ test::assert_contains "FAVICON=$favicon_src" \
+ "$(cd "$TEST_TMPDIR" && "$TEST_SHURIKEN" --print-config --favicon "$favicon_src")"
+
+ # A missing favicon is rejected before any output is written.
+ rm -rf "$TEST_TMPDIR/dist"
+ output=$(
+ cd "$TEST_TMPDIR"
+ test::capture_failure_output "$TEST_SHURIKEN" \
+ --generate --favicon "$TEST_TMPDIR/nope.ico"
+ )
+ test::assert_contains 'FAVICON file' "$output"
+ test::assert_path_absent "$TEST_TMPDIR/dist"
+ test::teardown
+}
+
# Synthetic `identify -verbose` output with EXIF the stats aggregation can parse,
# so a full --generate produces a real camera leaderboard + per-camera page.
test::stats_identify_output() {
@@ -5702,6 +5750,9 @@ main() {
'--generate --no-splash keeps root index redirect' \
test_generate_cli_no_splash_overrides_config
test::run_case \
+ '--generate --favicon uses a custom favicon' \
+ test_generate_uses_custom_favicon
+ test::run_case \
'--generate creates stats and per-camera pages with nav link' \
test_generate_stats_pages_created_and_nav_linked
test::run_case \