Skip to main content

A Favicon Pipeline from One SVG

Apr 6, 20263 min readTooling, SEO, Bash, DevOps

The Problem with Favicon Generators

Every time we updated the site logo, regenerating favicons meant uploading an SVG to a "free" web tool, downloading a zip, extracting it, and manually copying files into public/. The output was inconsistent — different tools produce different compression, different background handling, different size matrices. And none of them lived in version control.

We needed a deterministic, local pipeline: one SVG in, all the meta images out.

What Browsers Actually Need

The favicon landscape has consolidated. Here's what modern browsers and platforms actually request:

FileSizeUsed By
favicon.svgVectorModern browsers (preferred)
favicon.ico16+32+48pxLegacy browsers, bookmarks
favicon-16x16.png16x16Browser tabs
favicon-32x32.png32x32Browser tabs (Retina)
favicon-48x48.png48x48Windows shortcuts
favicon-96x96.png96x96Google TV, high-DPI contexts
favicon.png512x512PWA fallback
apple-touch-icon.png180x180iOS home screen
android-chrome-192x192192x192Android home screen
android-chrome-512x512512x512Android splash screen
mstile-150x150.png150x150Windows tiles

That's 11 files from one source SVG. The favicon.ico is actually three PNGs bundled into one file — 16, 32, and 48px — so the browser can pick the best size for the context.

The Script

The pipeline uses two tools: rsvg-convert for SVG-to-PNG rasterization (from the librsvg package) and ImageMagick's magick for combining PNGs into a multi-resolution .ico. Both are available via Homebrew.

The core loop generates each PNG size:

SIZES=(16 32 48 96 150 180 192 512)
 
for size in "${SIZES[@]}"; do
  rsvg-convert -w "$size" -h "$size" \
    --background-color=transparent \
    "$SOURCE" -o "${TMPDIR}/${size}.png"
done

Then we map the temporary files to their final names:

cp "${TMPDIR}/16.png"  "${OUTPUT}/favicon-16x16.png"
cp "${TMPDIR}/32.png"  "${OUTPUT}/favicon-32x32.png"
cp "${TMPDIR}/180.png" "${OUTPUT}/apple-touch-icon.png"
cp "${TMPDIR}/192.png" "${OUTPUT}/android-chrome-192x192.png"
# ...

The .ico combines three sizes into one file. This is what makes it display crisply across different contexts:

magick "${TMPDIR}/16.png" "${TMPDIR}/32.png" "${TMPDIR}/48.png" \
  "${OUTPUT}/favicon.ico"

Finally, we copy the source SVG as favicon.svg for browsers that support it. Modern browsers prefer the SVG when both are available: it scales perfectly at any resolution and supports dark mode via prefers-color-scheme media queries in the SVG itself.

Making It Reusable

The script takes two arguments. The source SVG and an optional output directory (we defaulted our script to apps/root/public/):

./scripts/generate-favicons.sh ~/Downloads/logo.svg
./scripts/generate-favicons.sh icon.svg ./other-app/public/

The Full Script

#!/usr/bin/env bash
set -euo pipefail
 
SRC="${1:?Usage: $0 <source.svg> [output-dir]}"
OUT="${2:-apps/root/public}"
 
if [ ! -f "$SRC" ]; then
  echo "Error: source file '$SRC' not found" >&2
  exit 1
fi
 
for cmd in rsvg-convert magick; do
  if ! command -v "$cmd" >/dev/null 2>&1; then
    echo "Error: '$cmd' is required but not installed" >&2
    echo "  brew install librsvg imagemagick" >&2
    exit 1
  fi
done
 
mkdir -p "$OUT"
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
 
cp "$SRC" "$OUT/favicon.svg"
 
SIZES=(16 32 48 96 150 180 192 512)
for size in "${SIZES[@]}"; do
  rsvg-convert -w "$size" -h "$size" "$SRC" -o "$TMPDIR/icon-${size}.png"
done
 
cp "$TMPDIR/icon-16.png"  "$OUT/favicon-16x16.png"
cp "$TMPDIR/icon-32.png"  "$OUT/favicon-32x32.png"
cp "$TMPDIR/icon-48.png"  "$OUT/favicon-48x48.png"
cp "$TMPDIR/icon-96.png"  "$OUT/favicon-96x96.png"
cp "$TMPDIR/icon-150.png" "$OUT/mstile-150x150.png"
cp "$TMPDIR/icon-180.png" "$OUT/apple-touch-icon.png"
cp "$TMPDIR/icon-192.png" "$OUT/android-chrome-192x192.png"
cp "$TMPDIR/icon-512.png" "$OUT/android-chrome-512x512.png"
cp "$TMPDIR/icon-512.png" "$OUT/favicon.png"
 
magick "$TMPDIR/icon-16.png" "$TMPDIR/icon-32.png" "$TMPDIR/icon-48.png" \
  "$OUT/favicon.ico"

We also wired it up as a Claude Code skill, so regenerating favicons after a logo change is a one-command operation. The skill validates that rsvg-convert and magick are installed, runs the script, and reports which files were generated.

The Takeaway

Favicon generation is a solved problem that doesn't need a web service. Two CLI tools, 64 lines of bash, and a clear mapping from sizes to filenames. The script lives in version control alongside the source SVG, so the pipeline is reproducible and the output is auditable. When the logo changes, and it will, we run one command on our machine instead of visiting a website.