From a49daabac7f91ed1a9f0e642c55a8a4699fb5e99 Mon Sep 17 00:00:00 2001 From: Benno Tielen Date: Thu, 9 Apr 2026 15:38:32 +0200 Subject: [PATCH] feature: favicon per site --- .gitignore | 3 +++ package.json | 4 +++- scripts/copy-favicon.mjs | 34 ++++++++++++++++++++++++++++++++++ sites/chemnitz/icon.ico | Bin 0 -> 4286 bytes src/app/(home)/icon.ico | Bin 15406 -> 0 bytes 5 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 scripts/copy-favicon.mjs create mode 100644 sites/chemnitz/icon.ico delete mode 100644 src/app/(home)/icon.ico diff --git a/.gitignore b/.gitignore index 3a67014..6c8ee1a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ next-env.d.ts /media *storybook.log + +# Generated per-site favicon (copied from sites//icon.ico by scripts/copy-favicon.mjs) +/src/app/(home)/icon.ico diff --git a/package.json b/package.json index 88f6861..d9e3bbd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/copy-favicon.mjs b/scripts/copy-favicon.mjs new file mode 100644 index 0000000..f2c7418 --- /dev/null +++ b/scripts/copy-favicon.mjs @@ -0,0 +1,34 @@ +// White-labeling glue: each client under sites// 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`) diff --git a/sites/chemnitz/icon.ico b/sites/chemnitz/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e123d081420acdfda81fb5ac619e1ce15ed18092 GIT binary patch literal 4286 zcmchZNoW*76o#v3j4?5|LKKORV-N*BcoD>a3VIOrEP9Y+CU_8B4&piqiYS7D2PcyS zqX|abH&jHKkb^`Kym$~nA}$xBCqS6ONF zZ#t(KQ*Ml@0$ipVpIx+%eFx&gxUeM55;f?{Q3jr>wt1%ByOBk%!!cK zTxo9z^?N+%{RvQ#s&yR5tItg}L$NoLcXjXj^_6}Nr{E-Le{{lmxCB?=8r*v6?^4b5L$XUUf2T!D|1ieVu7K zL+U|i-fegQ+N;6u<6wV!s8g-E$BxhoI+C2zst4K=^FjNNy%#CM%CB)$fMTH$-at@O zdBuQYRCW1Qiv*=|=9_Aw?8hjv<2Y@^5A7_?wD}nO39{*0-#f}%Ua;|onLWS4=Z3Icv^0+>w2X3xo&z%0DRq)i!P|okiaI=#cskB eyT%mvLi!Jr)A(P4SDr9Y&o-FJZa?XP;rs?}*4-ok literal 0 HcmV?d00001 diff --git a/src/app/(home)/icon.ico b/src/app/(home)/icon.ico deleted file mode 100644 index 3b0d1c7539dfb5bbf84f6e0e37e025c11f871496..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 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 w?}3kXF=YE%N%vl-W_^DKK6h)HUm*