added missing icons
All checks were successful
Deploy / build-deploy (push) Successful in 3m0s

This commit is contained in:
Jorijn van der Graaf 2026-08-18 21:21:41 +02:00
commit faf881fa0c
10 changed files with 201 additions and 8 deletions

BIN
apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -51,6 +51,15 @@ ensure('link[rel="icon"]', () => {
return el; return el;
}); });
// Keeps Safari from probing /apple-touch-icon{,-precomposed}.png at the root.
// Both files exist anyway, for the clients that probe regardless of markup.
ensure('link[rel="apple-touch-icon"]', () => {
const el = document.createElement("link");
el.rel = "apple-touch-icon";
el.href = "/apple-touch-icon.png";
return el;
});
ensure('meta[name="viewport"]', () => { ensure('meta[name="viewport"]', () => {
const el = document.createElement("meta"); const el = document.createElement("meta");
el.name = "viewport"; el.name = "viewport";

View file

@ -80,8 +80,18 @@ catcrafts.net {
Cross-Origin-Resource-Policy "same-origin" Cross-Origin-Resource-Policy "same-origin"
} }
# Subresources an isolated document pulls in must carry CORP themselves. # Subresources an isolated document pulls in must carry CORP themselves.
# The icons need it for the same reason the stylesheet does: "/" boots the
# wasm, so its icon fetches happen inside a require-corp document.
#
# -precomposed is listed here even though the rewrite below folds it onto
# apple-touch-icon.png: Caddy's directive order runs `header` BEFORE
# `rewrite`, so this matcher sees the URI as it arrived. (`handle` runs after,
# which is why @static does NOT need to list it.) Drop the path here and the
# probe still 200s, just bare.
header /styles.css Cross-Origin-Resource-Policy "same-origin" header /styles.css Cross-Origin-Resource-Policy "same-origin"
header /favicon.svg Cross-Origin-Resource-Policy "same-origin" @icons path /favicon.svg /favicon.ico /apple-touch-icon.png \
/apple-touch-icon-precomposed.png
header @icons Cross-Origin-Resource-Policy "same-origin"
# ── analytics, two tiers ────────────────────────────────────────────── # ── analytics, two tiers ──────────────────────────────────────────────
# #
@ -120,9 +130,23 @@ catcrafts.net {
# better than the backend would. `precompressed` serves the .zst / .gz # better than the backend would. `precompressed` serves the .zst / .gz
# siblings the CI build produces, so the ~800 KB module is never recompressed # siblings the CI build produces, so the ~800 KB module is never recompressed
# per request. Cache-busted by the ?v=<buildId> in index.html. # per request. Cache-busted by the ?v=<buildId> in index.html.
#
# CAVEAT for the icons in here: nothing cache-busts them. Clients ask for
# /favicon.svg, /favicon.ico and /apple-touch-icon.png by bare path, so an
# immutable year is exactly how long a redrawn icon takes to reach a repeat
# visitor. Long-standing for favicon.svg; the rasters inherit it deliberately
# rather than diverge. Shorten all three together if that ever bites.
#
# The rewrite is Safari's legacy Home Screen probe, from before it honoured
# <link rel="apple-touch-icon">. What it asks for is byte-identical to
# apple-touch-icon.png, so it is folded onto that rather than committed twice
# — a second copy would also ride in the wasm bundle's VFS and be fetched at
# every boot for nothing. @static below matches the rewritten path.
rewrite /apple-touch-icon-precomposed.png /apple-touch-icon.png
@static path /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js \ @static path /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js \
/catcrafts-head.js /files.json /variants.json /styles.css \ /catcrafts-head.js /files.json /variants.json /styles.css \
/favicon.svg /robots.txt /*.wgsl /*.jpg /posts.json /rates.json /favicon.svg /favicon.ico /apple-touch-icon.png \
/robots.txt /*.wgsl /*.jpg /posts.json /rates.json
handle @static { handle @static {
header Cache-Control "public, max-age=31536000, immutable" header Cache-Control "public, max-age=31536000, immutable"
file_server { file_server {

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -333,6 +333,18 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.files.emplace_back(fs::path("sitemap.xml")); cfg.files.emplace_back(fs::path("sitemap.xml"));
cfg.files.emplace_back(fs::path("feed.xml")); cfg.files.emplace_back(fs::path("feed.xml"));
cfg.files.emplace_back(fs::path("favicon.svg")); cfg.files.emplace_back(fs::path("favicon.svg"));
// Icon paths clients ask for without ever being told to. /favicon.ico is
// the root-path fallback a client uses when it cannot render the SVG above
// (Safari, historically) or never runs the JS that declares it (feed
// readers, the chat-app unfurlers, crawlers hitting the static shell); the
// apple-touch-icon.png is what Safari wants for its Home Screen and
// Favorites tile — its legacy -precomposed path is a Caddy rewrite onto
// this same file rather than a second entry here. Both were top entries in
// the 404 report until they shipped. Rasterised from favicon.svg by
// tools/make-icons.sh and committed — re-run it whenever that SVG is
// redrawn.
cfg.files.emplace_back(fs::path("favicon.ico"));
cfg.files.emplace_back(fs::path("apple-touch-icon.png"));
// WGSL for the ray-traced WebGPU demo embedded in the blog (see // WGSL for the ray-traced WebGPU demo embedded in the blog (see
// interfaces/Catcrafts-Demo.cppm). Fetched at runtime by WebGPUShader. // interfaces/Catcrafts-Demo.cppm). Fetched at runtime by WebGPUShader.
cfg.files.emplace_back(fs::path("shaders/raygen.wgsl")); cfg.files.emplace_back(fs::path("shaders/raygen.wgsl"));

View file

@ -2260,6 +2260,13 @@ export std::string RenderDocument(const RenderedPage& page,
"{}{}{}{}{}" "{}{}{}{}{}"
"<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Catcrafts\" href=\"/feed.xml\">\n" "<link rel=\"alternate\" type=\"application/atom+xml\" title=\"Catcrafts\" href=\"/feed.xml\">\n"
"<link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\">\n" "<link rel=\"icon\" href=\"/favicon.svg\" type=\"image/svg+xml\">\n"
// Declaring this is what stops Safari probing the root for
// /apple-touch-icon-precomposed.png and /apple-touch-icon.png, which is
// its fallback when a page names no Home Screen icon. /favicon.ico
// deliberately gets NO <link>: it exists purely so the by-path request
// resolves for clients that ignore the SVG, and declaring it would give
// SVG-capable browsers a reason to fetch the raster as well.
"<link rel=\"apple-touch-icon\" href=\"/apple-touch-icon.png\">\n"
"<link rel=\"stylesheet\" href=\"{}\">\n" "<link rel=\"stylesheet\" href=\"{}\">\n"
"{}" "{}"
"</head>\n<body>\n" "</head>\n<body>\n"

View file

@ -115,6 +115,7 @@ int main(int argc, char** argv) {
{ {
const std::string body = srv.Body("/demos/raytracer"); const std::string body = srv.Body("/demos/raytracer");
for (std::string_view probe : { "rel=\"stylesheet\"", "rel=\"icon\"", for (std::string_view probe : { "rel=\"stylesheet\"", "rel=\"icon\"",
"rel=\"apple-touch-icon\"",
"name=\"viewport\"" }) { "name=\"viewport\"" }) {
const std::size_t n = CountOccurrences(body, probe); const std::size_t n = CountOccurrences(body, probe);
Check(n == 1, std::format("demo page has exactly one {}", probe), Check(n == 1, std::format("demo page has exactly one {}", probe),

View file

@ -113,7 +113,8 @@ cat > "$WORK/Caddyfile" <<EOF
Cross-Origin-Resource-Policy "same-origin" Cross-Origin-Resource-Policy "same-origin"
} }
@static path /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js /catcrafts-head.js /files.json /variants.json /styles.css /favicon.svg /robots.txt /*.wgsl /*.jpg /posts.json /rates.json rewrite /apple-touch-icon-precomposed.png /apple-touch-icon.png
@static path /catcrafts*.wasm /runtime.js /dom-env.js /dom-webgpu.js /catcrafts-head.js /files.json /variants.json /styles.css /favicon.svg /favicon.ico /apple-touch-icon.png /robots.txt /*.wgsl /*.jpg /posts.json /rates.json
handle @static { handle @static {
header Cache-Control "no-store" header Cache-Control "no-store"
file_server file_server

80
tools/make-icons.sh Executable file
View file

@ -0,0 +1,80 @@
#!/bin/sh
# Rasterise favicon.svg into the icon files clients ask for by path.
#
# favicon.svg alone is not enough. Two families of request arrive at the web
# root for icons no markup ever points at, and both were the top entries in the
# 404 report before these files existed:
#
# /favicon.ico
# The unconditional root-path fallback. A client that cannot use an SVG
# favicon — Safari, historically — ignores the declared <link rel="icon">
# and asks for this instead, and every non-browser client that never runs
# JS (feed readers, the Slack/Discord/WhatsApp/Telegram unfurlers, crawlers)
# does the same on the static shell where the head is injected by
# catcrafts-head.js rather than served. Nothing declares this file; the
# fix is purely that the path resolves.
#
# /apple-touch-icon.png, /apple-touch-icon-precomposed.png
# What iOS/iPadOS and macOS Safari probe at the root when a page declares
# no <link rel="apple-touch-icon"> — the Add to Home Screen, Favorites and
# Start Page tile. The pages DO declare one now (RenderDocument and
# catcrafts-head.js), so the plain path is the one actually in use, and
# -precomposed is only reached by clients predating that link being
# honoured. ("Precomposed" meant "gloss and rounded corners are already
# baked in", from when iOS <7 applied a glossy overlay to the plain one.)
#
# So only ONE file is produced here: -precomposed is a Caddy rewrite onto
# apple-touch-icon.png (deploy/Caddyfile.example). A byte-identical second
# copy would be a duplicate to keep in sync in git AND a second entry in
# the wasm bundle's VFS — one more file fetched at every boot that nothing
# ever reads. The size-suffixed names (apple-touch-icon-180x180.png and
# friends) are not produced at all: they have never appeared in the logs,
# so shipping them would answer a question nobody asked.
#
# Checked in rather than built, unlike sitemap.xml and feed.xml. Those drift
# with every post; these drift only when favicon.svg is deliberately redrawn,
# and generating them in CI would mean installing librsvg and ImageMagick into
# the deploy container to produce identical bytes on every push.
#
# SO: re-run this after ANY edit to favicon.svg, and commit the results.
#
# Third copy of the same geometry to keep in sync — favicon.svg is the source,
# RenderNav in shared/interfaces/Catcrafts.Shared-Views.cppm inlines it as
# paths with CSS-variable colours, and these rasters fall out of the SVG here.
set -eu
cd "$(dirname "$0")/.."
SRC="favicon.svg"
# The <rect> fill from favicon.svg. Only used to flatten the Apple icon: iOS
# composites alpha against black and applies its own squircle mask, so the
# artwork must reach the edges fully opaque instead of carrying the SVG's own
# rounded corners.
BG="#0b0d10"
for cmd in rsvg-convert magick; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "make-icons: $cmd not found (pacman -S librsvg imagemagick)" >&2
exit 1
}
done
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
# favicon.ico: multi-resolution, transparency kept. The rounded corners read
# correctly against both a light and a dark tab bar, and 48px is what Windows
# shortcut and some bookmark surfaces pick.
for px in 16 32 48; do
rsvg-convert -w "$px" -h "$px" "$SRC" -o "$TMP/ico-$px.png"
done
magick "$TMP/ico-16.png" "$TMP/ico-32.png" "$TMP/ico-48.png" favicon.ico
# apple-touch-icon.png: 180x180 is the largest size iOS asks for, and it
# downsamples the rest itself. Opaque, per the note on BG above.
rsvg-convert -w 180 -h 180 "$SRC" -o "$TMP/touch.png"
magick "$TMP/touch.png" -background "$BG" -alpha remove -alpha off \
-strip apple-touch-icon.png
echo "make-icons: wrote favicon.ico apple-touch-icon.png"

View file

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
# Publish a recording or screenshot to catcrafts.net and print the URL to put in # Publish a recording, screenshot or audio file to catcrafts.net and print the
# the fediverse post. # URL to put in the fediverse post.
# #
# usage: tools/publish-media.sh [-r] [-c CRF] <file>... # usage: tools/publish-media.sh [-r] [-c CRF] <file>...
# #
@ -40,6 +40,24 @@
# with no <source> negotiation in front of it — and it must be the encoding # with no <source> negotiation in front of it — and it must be the encoding
# everything can play. Our own pages recognise the .h264.mp4 name, serve the AV1 # everything can play. Our own pages recognise the .h264.mp4 name, serve the AV1
# to browsers that can take it, and keep the H.264 as the <source> fallback. # to browsers that can take it, and keep the H.264 as the <source> fallback.
#
# AUDIO: the same two rails, for the same reason. Opus in WebM is the efficient
# one — the first file through here, 93 s of speech, went from a 1.5 MB 128k MP3
# to 553 KB — and an MP3 sibling is the one that plays everywhere, so the MP3 is
# the URL printed for the post. The primary does not have to be universal
# precisely because the sibling is.
#
# Two ways the audio path deliberately differs from the video path above. It does
# not fold everything to mono: here the audio IS the content rather than a
# rounding error next to a picture, so the source's channel layout survives and
# sets the bitrate. And a source that is ALREADY an MP3 is copied to the
# compatibility rail untouched rather than re-encoded — MP3 to MP3 spends a
# second generation of lossy artefacts to arrive at the same rail, and speech,
# which is most of what gets published here, is where that is most audible.
#
# Nothing downstream builds an <audio> element yet: fetch-media.sh does not match
# these names and the post renderer has no audio case. So the printed URL is for
# posting and for linking by hand, not yet for embedding in a post body.
set -eu set -eu
@ -116,10 +134,12 @@ for src in $FILES; do
case "$ext" in case "$ext" in
mp4|webm|mov|mkv|avi) kind=video ;; mp4|webm|mov|mkv|avi) kind=video ;;
png|jpg|jpeg|webp|gif|avif) kind=image ;; png|jpg|jpeg|webp|gif|avif) kind=image ;;
mp3|m4a|aac|opus|oga|ogg|flac|wav|wma|aiff) kind=audio ;;
esac esac
poster="" poster=""
fallback="" fallback=""
fallback_suffix=""
if [ "$RAW" = 1 ] || [ "$kind" = other ]; then if [ "$RAW" = 1 ] || [ "$kind" = other ]; then
payload="$src" payload="$src"
out_ext="$ext" out_ext="$ext"
@ -137,6 +157,7 @@ for src in $FILES; do
# the players most likely to be shaky about AV1 anyway, and the audio is # the players most likely to be shaky about AV1 anyway, and the audio is
# a rounding error next to the video either way. # a rounding error next to the video either way.
fallback="$WORK/h264.mp4" fallback="$WORK/h264.mp4"
fallback_suffix=h264.mp4
echo " transcoding H.264 fallback (crf 23)…" echo " transcoding H.264 fallback (crf 23)…"
# Same source and same denoise as the AV1, so the two encodings show the # Same source and same denoise as the AV1, so the two encodings show the
# same frames. CRF is x264's own scale (not comparable to SVT-AV1's); # same frames. CRF is x264's own scale (not comparable to SVT-AV1's);
@ -156,6 +177,42 @@ for src in $FILES; do
at=$(awk -v d="$dur" 'BEGIN { printf "%.2f", (d > 3 ? d / 3 : 0) }') at=$(awk -v d="$dur" 'BEGIN { printf "%.2f", (d > 3 ? d / 3 : 0) }')
ffmpeg -nostdin -v error -ss "$at" -i "$payload" -frames:v 1 \ ffmpeg -nostdin -v error -ss "$at" -i "$payload" -frames:v 1 \
-vf "scale=720:-2" -c:v libwebp -quality 80 "$poster" -y -vf "scale=720:-2" -c:v libwebp -quality 80 "$poster" -y
elif [ "$kind" = audio ]; then
payload="$WORK/out.webm"
out_ext=webm
fallback_suffix=mp3
# Channel count sets the bitrate instead of being flattened by it: 48k of
# mono Opus is comfortable for speech, and stereo music at that figure
# would not be. These are VBR targets, not ceilings, and libopus does not
# track them evenly — on the first file through here the 48k target came
# out at 45 kbps while 64k ran away to 76, which is why the mono rail sits
# at the lower figure rather than splitting the difference.
ch=$(ffprobe -v error -select_streams a:0 -show_entries stream=channels \
-of default=nw=1:nk=1 "$src" </dev/null 2>/dev/null \
| head -n1 || true)
case "$ch" in ''|*[!0-9]*) ch=1 ;; esac
if [ "$ch" -gt 1 ]; then ab=96k; else ab=48k; fi
echo " transcoding to Opus ($ab, ${ch}ch)…"
# -vn drops cover art, which arrives as a still video stream and belongs
# in neither output — without it an MP3 carrying album art fails the
# encode outright. -map_metadata -1 drops the ID3 tags, which on a file
# from someone else carry their tooling and their file naming. 48 kHz is
# the rate Opus works at internally, so saying it makes the resample of
# the usual 44.1 kHz source explicit rather than implied.
ffmpeg -nostdin -v error -i "$src" -vn -map_metadata -1 \
-c:a libopus -b:a "$ab" -ar 48000 \
"$payload" -y
fallback="$WORK/fallback.mp3"
if [ "$ext" = mp3 ]; then
echo " MP3 fallback: source is already MP3, copied unchanged"
cp "$src" "$fallback"
else
echo " transcoding MP3 fallback (128k)…"
# Fixed 128k, decodable by anything, and the same "bigger is
# acceptable on the compatibility rail" trade the H.264 above makes.
ffmpeg -nostdin -v error -i "$src" -vn -map_metadata -1 \
-c:a libmp3lame -b:a 128k "$fallback" -y
fi
else else
payload="$WORK/out.webp" payload="$WORK/out.webp"
out_ext=webp out_ext=webp
@ -171,17 +228,19 @@ for src in $FILES; do
# their own. That is the whole point: fetch-media.sh finds both by name, # their own. That is the whole point: fetch-media.sh finds both by name,
# with nothing to look up — the poster when the instance made no thumbnail # with nothing to look up — the poster when the instance made no thumbnail
# (pict-rs will not read AV1, so for these uploads that is the normal # (pict-rs will not read AV1, so for these uploads that is the normal
# case), the fallback for every browser that cannot decode AV1. # case), the fallback for every browser that cannot decode AV1. Audio's MP3
# sibling is named the same way, after the Opus file's hash.
if [ -n "$poster" ] && [ -f "$poster" ]; then if [ -n "$poster" ] && [ -f "$poster" ]; then
publish_file "$poster" "$hash.poster.webp" publish_file "$poster" "$hash.poster.webp"
fi fi
post_name="$name" post_name="$name"
if [ -n "$fallback" ] && [ -f "$fallback" ]; then if [ -n "$fallback" ] && [ -f "$fallback" ]; then
post_name="$hash.h264.mp4" post_name="$hash.$fallback_suffix"
publish_file "$fallback" "$post_name" publish_file "$fallback" "$post_name"
fi fi
# The postable URL is the H.264 one when it exists — see FALLBACK above. # The postable URL is the compatibility rail when there is one: the H.264
# video or the MP3 audio — see FALLBACK and AUDIO above.
url="$SITE_ORIGIN/media/$post_name" url="$SITE_ORIGIN/media/$post_name"
# Confirm it is actually being served before handing over a URL that is about # Confirm it is actually being served before handing over a URL that is about
# to be pasted into a post, where a 404 is public and permanent. # to be pasted into a post, where a 404 is public and permanent.