diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs
new file mode 100644
index 0000000..a48692b
--- /dev/null
+++ b/scripts/generate-icons.mjs
@@ -0,0 +1,144 @@
+#!/usr/bin/env node
+/**
+ * Tree-shake @iconify-json/mdi: scan src/ for `icon="mdi:..."` references and
+ * emit a minimal IconifyJSON containing only those icons. Saves ~3 MB in the
+ * client bundle vs. shipping the full 7000+ icon collection.
+ *
+ * Dynamic icons (from CMS data, conditional names) cannot be detected
+ * statically — list them under EXTRA_ICONS below. Anything missing falls
+ * through to the iconify API at runtime, which adds a network roundtrip but
+ * does not break.
+ *
+ * Run via `npm run generate:icons`. Hooked into `prebuild`.
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.resolve(__dirname, '..');
+const SRC_DIR = path.join(ROOT, 'src');
+const ICONS_JSON = path.join(ROOT, 'node_modules', '@iconify-json', 'mdi', 'icons.json');
+const OUT_FILE = path.join(ROOT, 'src', 'lib', 'iconify-mdi-subset.generated.json');
+
+/**
+ * Icons that appear via dynamic bindings (CMS data, conditional expressions)
+ * and would otherwise miss the static scan. Add new ones here when editors
+ * pick icons that appear blank — better than shipping the full collection.
+ */
+const EXTRA_ICONS = [
+ // PostActions: dynamic share/copy state
+ 'share-variant-outline',
+ 'link-variant',
+ // Search: file-type fallback
+ 'file-outline',
+ // Common social icons editors pick (Header.svelte: social.icon)
+ 'facebook',
+ 'instagram',
+ 'twitter',
+ 'youtube',
+ 'linkedin',
+ 'mastodon',
+ 'github',
+ 'email',
+ 'email-outline',
+ 'phone',
+ 'whatsapp',
+ 'telegram',
+ // Common file-type icons (FilesBlock: f.iconName)
+ 'file-pdf-box',
+ 'file-word-box',
+ 'file-excel-box',
+ 'file-image',
+ 'file-video',
+ 'file-music',
+ // Tag/link/banner fallbacks (LinkListBlock, Tags, DeadlineBannerBlock)
+ 'link',
+ 'tag-outline',
+ 'flag-outline',
+ 'information-outline',
+];
+
+const STATIC_PATTERN = /icon="mdi:([a-z0-9-]+)"/g;
+
+function walk(dir) {
+ const out = [];
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ out.push(...walk(full));
+ } else if (/\.(svelte|ts|tsx|js|mjs)$/.test(entry.name)) {
+ out.push(full);
+ }
+ }
+ return out;
+}
+
+function scanStatic() {
+ const found = new Set();
+ for (const file of walk(SRC_DIR)) {
+ const content = fs.readFileSync(file, 'utf8');
+ let m;
+ while ((m = STATIC_PATTERN.exec(content)) !== null) {
+ found.add(m[1]);
+ }
+ }
+ return found;
+}
+
+function main() {
+ if (!fs.existsSync(ICONS_JSON)) {
+ console.error(
+ `[generate-icons] missing ${ICONS_JSON} — run \`npm install\` first.`,
+ );
+ process.exit(1);
+ }
+ const collection = JSON.parse(fs.readFileSync(ICONS_JSON, 'utf8'));
+ const wanted = new Set([...scanStatic(), ...EXTRA_ICONS]);
+
+ const icons = {};
+ const aliases = {};
+ const missing = [];
+
+ for (const name of wanted) {
+ if (collection.icons[name]) {
+ icons[name] = collection.icons[name];
+ } else if (collection.aliases?.[name]) {
+ aliases[name] = collection.aliases[name];
+ const parent = collection.aliases[name].parent;
+ if (parent && collection.icons[parent] && !icons[parent]) {
+ icons[parent] = collection.icons[parent];
+ }
+ } else {
+ missing.push(name);
+ }
+ }
+
+ const subset = {
+ prefix: collection.prefix,
+ width: collection.width,
+ height: collection.height,
+ icons,
+ ...(Object.keys(aliases).length ? { aliases } : {}),
+ };
+
+ fs.writeFileSync(OUT_FILE, JSON.stringify(subset, null, 2) + '\n');
+
+ const sizeBefore = fs.statSync(ICONS_JSON).size;
+ const sizeAfter = fs.statSync(OUT_FILE).size;
+ console.log(
+ `[generate-icons] ${Object.keys(icons).length} icons + ${Object.keys(aliases).length} aliases → ${path.relative(ROOT, OUT_FILE)}`,
+ );
+ console.log(
+ `[generate-icons] ${(sizeBefore / 1024 / 1024).toFixed(2)} MB → ${(sizeAfter / 1024).toFixed(1)} KB (${((1 - sizeAfter / sizeBefore) * 100).toFixed(1)}% smaller)`,
+ );
+ if (missing.length) {
+ console.warn(
+ `[generate-icons] ${missing.length} icon(s) not found in MDI: ${missing.join(', ')}`,
+ );
+ }
+}
+
+main();
diff --git a/src/lib/iconify-mdi-subset.generated.json b/src/lib/iconify-mdi-subset.generated.json
new file mode 100644
index 0000000..c958ed8
--- /dev/null
+++ b/src/lib/iconify-mdi-subset.generated.json
@@ -0,0 +1,170 @@
+{
+ "prefix": "mdi",
+ "width": 24,
+ "height": 24,
+ "icons": {
+ "chevron-right": {
+ "body": ""
+ },
+ "magnify": {
+ "body": ""
+ },
+ "close": {
+ "body": ""
+ },
+ "calendar": {
+ "body": ""
+ },
+ "chevron-down": {
+ "body": ""
+ },
+ "calendar-clock": {
+ "body": ""
+ },
+ "rss": {
+ "body": ""
+ },
+ "pencil-outline": {
+ "body": ""
+ },
+ "trash-can-outline": {
+ "body": ""
+ },
+ "clock-outline": {
+ "body": ""
+ },
+ "alert-circle-outline": {
+ "body": ""
+ },
+ "comment-text-outline": {
+ "body": ""
+ },
+ "reply": {
+ "body": ""
+ },
+ "shield-check-outline": {
+ "body": ""
+ },
+ "map-marker": {
+ "body": ""
+ },
+ "cursor-default-click": {
+ "body": ""
+ },
+ "open-in-new": {
+ "body": ""
+ },
+ "menu": {
+ "body": ""
+ },
+ "chevron-left": {
+ "body": ""
+ },
+ "comment-outline": {
+ "body": ""
+ },
+ "printer-outline": {
+ "body": ""
+ },
+ "download-outline": {
+ "body": ""
+ },
+ "help-circle-outline": {
+ "body": ""
+ },
+ "check": {
+ "body": ""
+ },
+ "content-copy": {
+ "body": ""
+ },
+ "play": {
+ "body": ""
+ },
+ "home": {
+ "body": ""
+ },
+ "home-outline": {
+ "body": ""
+ },
+ "file-document-outline": {
+ "body": ""
+ },
+ "share-variant-outline": {
+ "body": ""
+ },
+ "link-variant": {
+ "body": ""
+ },
+ "file-outline": {
+ "body": ""
+ },
+ "facebook": {
+ "body": ""
+ },
+ "instagram": {
+ "body": ""
+ },
+ "twitter": {
+ "body": ""
+ },
+ "youtube": {
+ "body": ""
+ },
+ "linkedin": {
+ "body": ""
+ },
+ "mastodon": {
+ "body": ""
+ },
+ "github": {
+ "body": ""
+ },
+ "email": {
+ "body": ""
+ },
+ "email-outline": {
+ "body": ""
+ },
+ "phone": {
+ "body": ""
+ },
+ "whatsapp": {
+ "body": ""
+ },
+ "telegram": {
+ "body": "",
+ "hidden": true
+ },
+ "file-pdf-box": {
+ "body": ""
+ },
+ "file-word-box": {
+ "body": ""
+ },
+ "file-excel-box": {
+ "body": ""
+ },
+ "file-image": {
+ "body": ""
+ },
+ "file-video": {
+ "body": ""
+ },
+ "file-music": {
+ "body": ""
+ },
+ "link": {
+ "body": ""
+ },
+ "tag-outline": {
+ "body": ""
+ },
+ "flag-outline": {
+ "body": ""
+ },
+ "information-outline": {
+ "body": ""
+ }
+ }
+}
diff --git a/src/lib/iconify-offline.ts b/src/lib/iconify-offline.ts
index c79a430..6cc13d6 100644
--- a/src/lib/iconify-offline.ts
+++ b/src/lib/iconify-offline.ts
@@ -1,12 +1,20 @@
/**
- * Iconify Offline: MDI-Collection für Server und Client registrieren.
- * Muss an zwei Stellen importiert werden (Side-Effect-Import), damit
- * überall gleich rendert (kein Hydration-Mismatch):
- * 1. Layout.astro (--- Block) → läuft beim SSR
- * 2. Header.svelte (oder andere frühe client:load-Komponente) → läuft im Browser
- * Danach können alle Svelte-Komponenten Icon ohne API nutzen.
+ * Iconify-Subset für MDI-Icons. Statt der kompletten Collection (~3 MB) wird
+ * nur eine generierte Untermenge eingebunden — siehe `scripts/generate-icons.mjs`
+ * und `iconify-mdi-subset.generated.json`. Statisch verwendete Icons werden
+ * automatisch erkannt; Icons aus CMS-Daten / dynamischen Expressions stehen
+ * unter `EXTRA_ICONS` im Generator.
+ *
+ * Side-Effect-Import: an zwei Stellen einbinden, damit SSR und Client-Hydration
+ * dieselbe Collection sehen (kein Hydration-Mismatch):
+ * 1. `+layout.svelte` / `+error.svelte` (top-level import) → Server + Client
+ * 2. überall sonst implizit über das Layout
+ *
+ * Fehlt ein Icon, fällt @iconify/svelte zur Laufzeit auf api.iconify.design
+ * zurück. Lieber `EXTRA_ICONS` ergänzen + neu generieren.
*/
-import { addCollection } from "@iconify/svelte";
-import { icons as mdiIcons } from "@iconify-json/mdi";
+import { addCollection } from '@iconify/svelte';
+import type { IconifyJSON } from '@iconify/types';
+import mdiSubset from './iconify-mdi-subset.generated.json';
-addCollection(mdiIcons);
+addCollection(mdiSubset as IconifyJSON);