feat: expand portfolios, drop wedding crowd photos, offer studio on site
- 28 more of the operator's own public images (42 -> 67), alts from his captions - remove group-feier, group-outdoor, group-posiert (wedding guests visible) - studio section now offers Studio Neumuenster and Mobiles Studio - deploy: --force-recreate on release and rollback, plus in-container content probe - verify: DESFOTO_BASE dry-run, redirect, asset and privacy checks - tests: 57 OK (3 new content guards, 2 deploy/verify regression guards)
@@ -39,7 +39,10 @@ if [ "$release_sha" = "rollback" ]; then
|
||||
sudo cp '$target/nginx.conf' '$live_dir/nginx.conf'
|
||||
sudo cp '$target/compose.yml' '$live_dir/compose.yml'
|
||||
sudo cp '$target/compose.vps.yml' '$live_dir/compose.vps.yml'
|
||||
sudo -n -u denny bash -c \"cd '$live_dir' && docker compose -f compose.yml -f compose.vps.yml up -d --remove-orphans\"
|
||||
# --force-recreate for the same reason as in the forward path: the site
|
||||
# tree inode changes, so a merely running container would keep the deleted
|
||||
# directory mounted and serve an empty root.
|
||||
sudo -n -u denny bash -c \"cd '$live_dir' && docker compose -f compose.yml -f compose.vps.yml up -d --force-recreate --remove-orphans\"
|
||||
else
|
||||
echo '[deploy] initial-state snapshot: removing the desfoto stack again'
|
||||
if sudo test -d '$live_dir'; then
|
||||
@@ -98,12 +101,19 @@ sudo chown -R denny:denny "$LIVE_DIR"
|
||||
sudo rm -f "$archive"
|
||||
printf '%s\n' "$REMOTE_SHA" | sudo tee "$LIVE_DIR/RELEASE" >/dev/null
|
||||
|
||||
# 3. Start or update the stack. The live directory belongs to denny and the SSH
|
||||
# login cannot traverse /home/denny, so the directory change and the compose
|
||||
# command run together in one shell under the owning identity.
|
||||
sudo -n -u denny bash -c "cd '$LIVE_DIR' && docker compose -f compose.yml -f compose.vps.yml up -d --remove-orphans"
|
||||
# 3. Start or update the stack. --force-recreate is deliberate: step 2 replaces
|
||||
# the site directory (new inode) and rewrites nginx.conf, and a container that
|
||||
# is merely "running" keeps the bind mount to the deleted directory and then
|
||||
# serves an empty document root (every request 404) while Compose reports no
|
||||
# change. Recreating costs a moment of 502s behind Traefik and guarantees that
|
||||
# a fresh container binds the new tree and reads the new config. The live
|
||||
# directory belongs to denny and the SSH login cannot traverse /home/denny, so
|
||||
# the directory change and the compose command run together in one shell under
|
||||
# the owning identity.
|
||||
sudo -n -u denny bash -c "cd '$LIVE_DIR' && docker compose -f compose.yml -f compose.vps.yml up -d --force-recreate --remove-orphans"
|
||||
|
||||
# 4. Wait for the container health check.
|
||||
# 4. Wait for the container health check. The healthcheck requests "/" from
|
||||
# inside the container, so an empty document root cannot report healthy.
|
||||
for _ in $(seq 1 30); do
|
||||
status="$(sudo docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' desfoto-web-1 2>/dev/null || echo missing)"
|
||||
[ "$status" = "healthy" ] && break
|
||||
@@ -117,6 +127,15 @@ if [ "$status" != "healthy" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4b. Content check, not process check: a stale bind mount served 404 for every
|
||||
# path while Compose still reported the container healthy. Ask the container
|
||||
# itself for the homepage and fail the deploy if it is not served.
|
||||
if ! sudo docker exec desfoto-web-1 wget -q --spider http://127.0.0.1/; then
|
||||
echo "[deploy] ERROR: desfoto-web-1 does not serve the release at / (empty mount?)" >&2
|
||||
sudo docker logs --tail 40 desfoto-web-1 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Remove the obsolete desfoto.de redirect from the shared stack project.
|
||||
# The new stack already owns desfoto.de through a higher Traefik priority,
|
||||
# this only deletes the now-dead labels so Traefik no longer advertises them.
|
||||
|
||||
@@ -12,7 +12,13 @@ cd "$(dirname "$0")/.."
|
||||
root="$(pwd)"
|
||||
release_sha="${1:-unknown}"
|
||||
remote="${DESFOTO_REMOTE:-prod-main}"
|
||||
base="https://desfoto.de"
|
||||
# DESFOTO_BASE lets the hook be dry-run against a local container
|
||||
# (DESFOTO_BASE=http://127.0.0.1:18430 .ocauto/verify unknown); the
|
||||
# domain-specific sections (deployed tree, canonical host, neighbours, TLS)
|
||||
# only run against the real production origin.
|
||||
base="${DESFOTO_BASE:-https://desfoto.de}"
|
||||
live=0
|
||||
if [ "$base" = "https://desfoto.de" ]; then live=1; fi
|
||||
|
||||
failures=0
|
||||
pass() { printf ' ok %s\n' "$1"; }
|
||||
@@ -44,8 +50,29 @@ check_header() {
|
||||
fi
|
||||
}
|
||||
|
||||
check_redirect() {
|
||||
local url="$1" want="$2" got target
|
||||
got="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 30 "$url" || echo 000)"
|
||||
target="$(curl -sS -o /dev/null -w '%{redirect_url}' --max-time 30 "$url" || true)"
|
||||
if [ "$got" = "301" ] && [ "$target" = "$want" ]; then
|
||||
pass "$url -> 301 $want"
|
||||
else
|
||||
fail "$url -> $got ${target:-<no Location>} (want 301 $want)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_not_contains() {
|
||||
local url="$1" needle="$2" body
|
||||
body="$(curl -sS --max-time 30 "$url" || true)"
|
||||
if printf '%s' "$body" | grep -qF -- "$needle"; then
|
||||
fail "$url still contains '$needle'"
|
||||
else
|
||||
pass "$url no longer contains '$needle'"
|
||||
fi
|
||||
}
|
||||
|
||||
printf '\n== release identity\n'
|
||||
if [ "$release_sha" != "unknown" ] && git -C "$root" rev-parse --verify --quiet "$release_sha" >/dev/null; then
|
||||
if [ "$live" = 1 ] && [ "$release_sha" != "unknown" ] && git -C "$root" rev-parse --verify --quiet "$release_sha" >/dev/null; then
|
||||
local_hash="$(cd "$root/site" && find . -type f ! -name sitemap.xml ! -path './.well-known/*' -print0 |
|
||||
sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1)"
|
||||
remote_hash="$(ssh "$remote" "sudo sh -c 'cd /home/denny/stacks/desfoto/site && find . -type f ! -name sitemap.xml ! -path ./.well-known/\* -print0 | sort -z | xargs -0 sha256sum | sha256sum'" | cut -d' ' -f1)"
|
||||
@@ -63,12 +90,18 @@ if [ "$release_sha" != "unknown" ] && git -C "$root" rev-parse --verify --quiet
|
||||
fi
|
||||
|
||||
printf '\n== routes\n'
|
||||
for route in / /fotografie/ /businessfotografie/ /portrait-und-model/ /familien-und-paare/ \
|
||||
for route in / /shootings/ /businessfotografie/ /portrait-und-model/ /familien-und-paare/ \
|
||||
/minishootings/ /video/ /social-media/ /projekte/ /ueber/ /kontakt/ /impressum/ /datenschutz/; do
|
||||
check_status "$base$route" 200
|
||||
done
|
||||
|
||||
# The retired photo URL must keep working: it is the umbrella page now.
|
||||
check_redirect "$base/fotografie" "$base/shootings/"
|
||||
check_redirect "$base/fotografie/" "$base/shootings/"
|
||||
check_redirect "$base/fotografie.html" "$base/shootings/"
|
||||
|
||||
printf '\n== canonical host and redirects\n'
|
||||
if [ "$live" = 1 ]; then
|
||||
www_status="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 30 https://www.desfoto.de/ || echo 000)"
|
||||
www_target="$(curl -sS -o /dev/null -w '%{redirect_url}' --max-time 30 https://www.desfoto.de/ || true)"
|
||||
if [ "$www_status" = "301" ] && [ "$www_target" = "https://desfoto.de/" ]; then
|
||||
@@ -81,15 +114,25 @@ case "$http_target" in
|
||||
https://desfoto.de/*) pass "http://desfoto.de/ -> $http_target" ;;
|
||||
*) fail "http://desfoto.de/ redirects to '$http_target'" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
printf '\n== content\n'
|
||||
check_contains "$base/" "Bilder und Filme, die nicht beliebig aussehen"
|
||||
check_contains "$base/" 'href="/shootings/"'
|
||||
check_contains "$base/" 'href="/video/"'
|
||||
check_contains "$base/" "Mobiles Studio"
|
||||
check_contains "$base/" "Studio Neumünster"
|
||||
check_not_contains "$base/" 'href="/fotografie/'
|
||||
check_contains "$base/shootings/" "Autarke Blitzanlage"
|
||||
check_contains "$base/shootings/" "Minishootings"
|
||||
check_contains "$base/impressum/" "§ 5 DDG"
|
||||
check_contains "$base/impressum/" "DE462149560"
|
||||
check_contains "$base/datenschutz/" "keine Zugriffsprotokolle"
|
||||
check_contains "$base/datenschutz/" "youtube-nocookie.com"
|
||||
check_contains "$base/video/" "NWWFTf7l8g0"
|
||||
check_contains "$base/sitemap.xml" "<loc>https://desfoto.de/video/</loc>"
|
||||
check_contains "$base/" "https://www.dennyschulz.de/"
|
||||
check_contains "$base/" "https://dennyapp.de/"
|
||||
check_contains "$base/sitemap.xml" "<loc>https://desfoto.de/shootings/</loc>"
|
||||
check_not_contains "$base/sitemap.xml" "<loc>https://desfoto.de/fotografie/</loc>"
|
||||
|
||||
printf '\n== error handling and metadata\n'
|
||||
check_status "$base/diese-seite-gibt-es-nicht/" 404
|
||||
@@ -98,11 +141,22 @@ check_status "$base/.well-known/security.txt" 200
|
||||
check_status "$base/robots.txt" 200
|
||||
|
||||
printf '\n== assets\n'
|
||||
check_status "$base/assets/img/hero-studio-1200.webp" 200
|
||||
check_status "$base/assets/img/logo-160.png" 200
|
||||
check_status "$base/assets/img/favicon-32.png" 200
|
||||
check_status "$base/assets/img/og-shootings.jpg" 200
|
||||
check_status "$base/assets/site.css" 200
|
||||
check_status "$base/assets/fonts/fraunces-latin.woff2" 200
|
||||
check_status "$base/assets/img/og-desfoto.jpg" 200
|
||||
# The portfolio expansion ships the operator's own public work...
|
||||
check_status "$base/assets/img/music-dreadlocks-1200.webp" 200
|
||||
check_status "$base/assets/img/business-hotel-window-1200.webp" 200
|
||||
check_status "$base/assets/img/couple-park-1200.webp" 200
|
||||
check_status "$base/assets/img/editorial-leap-1200.webp" 200
|
||||
# ...and the wedding pictures showing the guests stay removed.
|
||||
check_status "$base/assets/img/group-feier-1200.webp" 404
|
||||
check_status "$base/assets/img/group-outdoor-1200.webp" 404
|
||||
check_status "$base/assets/img/group-posiert-1200.webp" 404
|
||||
check_contains "$base/projekte/" "music-dreadlocks-1200.webp"
|
||||
check_contains "$base/familien-und-paare/" "couple-park-1200.webp"
|
||||
|
||||
printf '\n== privacy promises and headers\n'
|
||||
check_header "$base/" "Strict-Transport-Security"
|
||||
@@ -116,12 +170,14 @@ else
|
||||
fi
|
||||
|
||||
printf '\n== neighbouring sites untouched\n'
|
||||
if [ "$live" = 1 ]; then
|
||||
check_status "https://www.dennyschulz.de/" 200
|
||||
check_contains "https://www.dennyschulz.de/impressum" "Denny Schulz"
|
||||
check_status "https://dennyapp.de/" 200
|
||||
fi
|
||||
|
||||
printf '\n== TLS\n'
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
if [ "$live" = 1 ] && command -v openssl >/dev/null 2>&1; then
|
||||
if echo | openssl s_client -servername desfoto.de -connect desfoto.de:443 2>/dev/null |
|
||||
openssl x509 -noout -checkend 604800 -subject 2>/dev/null; then
|
||||
pass "certificate for desfoto.de is valid for at least 7 more days"
|
||||
|
||||
@@ -90,3 +90,10 @@ and removes the obsolete `desfoto.de → dennyschulz.de` labels from `/srv/stack
|
||||
`.ocauto/verify <sha>` proves the live site matches the release. Details, including
|
||||
rollback, are in `docs/deployment.md`. Traefik is shared infrastructure: route through
|
||||
labels, never restart or recreate the proxy.
|
||||
|
||||
The deploy replaces `site/` (new inode) and rewrites `nginx.conf`, so it runs
|
||||
`docker compose up -d --force-recreate`: a container that is merely "running" keeps
|
||||
the deleted directory bind-mounted and then serves an empty document root — every
|
||||
path 404, while Compose reports no change and the stale health status still says
|
||||
`healthy`. The recreate is followed by an explicit in-container request for `/`, and
|
||||
`tests/test_site.py` guards the flag. Never remove either check.
|
||||
|
||||
BIN
site/assets/img/band-extinct-group-1200.webp
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
site/assets/img/band-extinct-group-1600.webp
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
site/assets/img/band-extinct-group-480.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
site/assets/img/band-extinct-group-768.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
site/assets/img/band-extinct-guitar-1200.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
site/assets/img/band-extinct-guitar-1600.webp
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
site/assets/img/band-extinct-guitar-480.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
site/assets/img/band-extinct-guitar-768.webp
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
site/assets/img/business-hotel-laugh-1200.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
site/assets/img/business-hotel-laugh-1600.webp
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
site/assets/img/business-hotel-laugh-480.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
site/assets/img/business-hotel-laugh-768.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
site/assets/img/business-hotel-orange-1200.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
site/assets/img/business-hotel-orange-1600.webp
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
site/assets/img/business-hotel-orange-480.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
site/assets/img/business-hotel-orange-768.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
site/assets/img/business-hotel-tea-1200.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
site/assets/img/business-hotel-tea-1600.webp
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
site/assets/img/business-hotel-tea-480.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
site/assets/img/business-hotel-tea-768.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
site/assets/img/business-hotel-window-1200.webp
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
site/assets/img/business-hotel-window-1600.webp
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
site/assets/img/business-hotel-window-480.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
site/assets/img/business-hotel-window-768.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
site/assets/img/couple-backlight-kiss-1200.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
site/assets/img/couple-backlight-kiss-1600.webp
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
site/assets/img/couple-backlight-kiss-480.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
site/assets/img/couple-backlight-kiss-768.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
site/assets/img/couple-forest-walk-1200.webp
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
site/assets/img/couple-forest-walk-1600.webp
Normal file
|
After Width: | Height: | Size: 466 KiB |
BIN
site/assets/img/couple-forest-walk-480.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
site/assets/img/couple-forest-walk-768.webp
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
site/assets/img/couple-laugh-walk-1200.webp
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
site/assets/img/couple-laugh-walk-1600.webp
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
site/assets/img/couple-laugh-walk-480.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
site/assets/img/couple-laugh-walk-768.webp
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
site/assets/img/couple-park-1200.webp
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
site/assets/img/couple-park-1600.webp
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
site/assets/img/couple-park-480.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
site/assets/img/couple-park-768.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
site/assets/img/couple-sun-silhouette-1200.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
site/assets/img/couple-sun-silhouette-1600.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
site/assets/img/couple-sun-silhouette-480.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
site/assets/img/couple-sun-silhouette-768.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
site/assets/img/editorial-balance-1200.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
site/assets/img/editorial-balance-1600.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
site/assets/img/editorial-balance-480.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
site/assets/img/editorial-balance-768.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
site/assets/img/editorial-leap-1200.webp
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
site/assets/img/editorial-leap-1600.webp
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
site/assets/img/editorial-leap-480.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
site/assets/img/editorial-leap-768.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
site/assets/img/editorial-studio-side-1200.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
site/assets/img/editorial-studio-side-1600.webp
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
site/assets/img/editorial-studio-side-480.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
site/assets/img/editorial-studio-side-768.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
site/assets/img/free-castle-lake-1200.webp
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
site/assets/img/free-castle-lake-1600.webp
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
site/assets/img/free-castle-lake-480.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
site/assets/img/free-castle-lake-768.webp
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
site/assets/img/free-cat-light-1200.webp
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
site/assets/img/free-cat-light-1600.webp
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
site/assets/img/free-cat-light-480.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
site/assets/img/free-cat-light-768.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
site/assets/img/free-cat-white-1200.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
site/assets/img/free-cat-white-1600.webp
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
site/assets/img/free-cat-white-480.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
site/assets/img/free-cat-white-768.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
site/assets/img/free-dog-husky-1200.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
site/assets/img/free-dog-husky-1600.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
site/assets/img/free-dog-husky-480.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
site/assets/img/free-dog-husky-768.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
site/assets/img/free-dogs-forest-1200.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
site/assets/img/free-dogs-forest-1600.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
site/assets/img/free-dogs-forest-480.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
site/assets/img/free-dogs-forest-768.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
site/assets/img/free-sparrow-1200.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
site/assets/img/free-sparrow-1600.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
site/assets/img/free-sparrow-480.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
site/assets/img/free-sparrow-768.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
site/assets/img/free-swans-1200.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
site/assets/img/free-swans-1600.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
site/assets/img/free-swans-480.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
site/assets/img/free-swans-768.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 89 KiB |
BIN
site/assets/img/music-banner-1200.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |