feature: favicon per site

This commit is contained in:
Benno Tielen 2026-04-09 15:38:32 +02:00
parent 3e05a9df82
commit a49daabac7
5 changed files with 40 additions and 1 deletions

3
.gitignore vendored
View file

@ -47,3 +47,6 @@ next-env.d.ts
/media
*storybook.log
# Generated per-site favicon (copied from sites/<NEXT_PUBLIC_SITE_ID>/icon.ico by scripts/copy-favicon.mjs)
/src/app/(home)/icon.ico

View file

@ -5,9 +5,11 @@
"license": "MIT",
"type": "module",
"scripts": {
"prebuild": "node --env-file-if-exists=.env scripts/copy-favicon.mjs",
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
"predev": "node --env-file-if-exists=.env scripts/copy-favicon.mjs",
"dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
"devsafe": "rm -rf .next && node --env-file-if-exists=.env scripts/copy-favicon.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:types": "payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",

34
scripts/copy-favicon.mjs Normal file
View file

@ -0,0 +1,34 @@
// White-labeling glue: each client under sites/<id>/ ships its own icon.ico.
// Next.js's App Router picks up the favicon via the file convention at
// src/app/(home)/icon.ico, which can only point to one file. This script runs
// before `next dev` / `next build` (see predev/prebuild in package.json) and
// copies the right per-site icon into that location based on NEXT_PUBLIC_SITE_ID.
// The destination is gitignored so it's treated as a build artifact.
import { copyFileSync, existsSync, readdirSync, statSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = resolve(__dirname, '..')
// Mirror the default in src/config/site.ts so an unset env var produces the
// same site here as it does at runtime.
const siteId = process.env.NEXT_PUBLIC_SITE_ID || 'dreikoenige'
const sitesDir = join(repoRoot, 'sites')
const source = join(sitesDir, siteId, 'icon.ico')
const destination = join(repoRoot, 'src', 'app', '(home)', 'icon.ico')
// Fail loudly with the list of valid site ids — same UX as the runtime check
// in src/config/site.ts when an unknown NEXT_PUBLIC_SITE_ID is supplied.
if (!existsSync(source)) {
const available = readdirSync(sitesDir)
.filter((entry) => statSync(join(sitesDir, entry)).isDirectory())
.join(', ')
throw new Error(
`[copy-favicon] No icon.ico for site "${siteId}" at ${source}. Available sites: ${available}`,
)
}
copyFileSync(source, destination)
console.log(`[copy-favicon] ${siteId} → src/app/(home)/icon.ico`)

BIN
sites/chemnitz/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB