Auto-detect content icon collections for the server bundle instead of hand-maintaining the list

Fixes noto, carbon, cbi, cib, fluent-color, logos, and solar icons silently
falling back to a live api.iconify.design call blocked by the CSP, which
only showed up after a hard refresh once the browser cached a partial
response. nuxt.config.ts now scans content/*.md for every icon collection
actually referenced and bundles it automatically, so a newly introduced
collection only needs npm install, not a manual config edit.
This commit is contained in:
Djeex
2026-09-07 16:39:23 +02:00
parent 0450a78478
commit 88e52a976a
3 changed files with 129 additions and 1 deletions
+52 -1
View File
@@ -1,9 +1,56 @@
import { fileURLToPath } from 'node:url'
import { execFileSync } from 'node:child_process'
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { createResolver } from '@nuxt/kit'
const { resolve } = createResolver(import.meta.url)
// Every `i-<collection>-<name>` icon referenced from content/*.md needs its
// collection listed below, but Nuxt Icon's own static scanner only looks at
// .vue/.ts source, not markdown content, so it can't find these on its own.
// Scanning content here instead of hand-maintaining the list means a new
// icon collection used in an article gets bundled automatically on the next
// build; the only manual step left is `npm install @iconify-json/<name>`
// for a genuinely new collection, and a missing one now fails the build
// loudly (unresolved import) instead of silently breaking at runtime behind
// the CSP (icons falling back to a live, blocked api.iconify.design call).
function scanContentIconCollections(): string[] {
// Collection prefixes can contain hyphens themselves (simple-icons,
// fluent-color), same as the separator before the icon name, so a plain
// "first segment" split is ambiguous. Matching against the actual
// installed @iconify-json package names, longest first, resolves it.
const installed = readdirSync(resolve('./node_modules/@iconify-json'))
.sort((a, b) => b.length - a.length)
const found = new Set<string>()
const pattern = /icon=["']i-([a-z0-9-]+)["']|icon:\s*["']?i-([a-z0-9-]+)/g
function walk(dir: string) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name)
if (entry.isDirectory()) {
walk(full)
}
else if (entry.name.endsWith('.md')) {
const text = readFileSync(full, 'utf8')
for (const match of text.matchAll(pattern)) {
const iconRef = match[1] ?? match[2]
const collection = installed.find(name => iconRef === name || iconRef.startsWith(`${name}-`))
// Falls back to the first segment for a collection that isn't
// installed yet: still wrong, but it now surfaces as a clear
// "cannot resolve @iconify-json/<name>" build error to fix,
// rather than a silent runtime CSP block.
found.add(collection ?? iconRef.split('-')[0])
}
}
}
}
walk(resolve('./content'))
// 'brand' is the local customCollections prefix (app/assets/brand-icons),
// not an installable Iconify package.
found.delete('brand')
return [...found]
}
// Contributors per page: read straight from git history rather than an
// API, so it needs no token and no network call, but the CI checkout must
// fetch full history (not a shallow clone) or every file will only show
@@ -84,8 +131,12 @@ export default defineNuxtConfig({
// bundle. Without this, they fall back to a live api.iconify.design
// request at prerender time, which times out wherever outbound access
// is restricted. Bundling all three collections in full avoids that.
// Everything else used as a literal icon in content/*.md is added by
// scanContentIconCollections() above, so a new collection introduced in
// an article gets bundled automatically instead of needing a manual
// addition here.
serverBundle: {
collections: ['simple-icons', 'lucide', 'vscode-icons'],
collections: [...new Set(['simple-icons', 'lucide', 'vscode-icons', ...scanContentIconCollections()])],
},
// The fixed, small set of codeIcon values above. Forces them into the
// content-hashed client bundle instead of Nuxt Icon's runtime
+70
View File
@@ -6,6 +6,13 @@
"": {
"name": "docudjeex",
"dependencies": {
"@iconify-json/carbon": "^1.2.26",
"@iconify-json/cbi": "^1.2.59",
"@iconify-json/cib": "^1.2.3",
"@iconify-json/fluent-color": "^1.2.21",
"@iconify-json/logos": "^1.2.14",
"@iconify-json/noto": "^1.2.7",
"@iconify-json/solar": "^1.2.10",
"@nuxtjs/i18n": "^10.6.0",
"better-sqlite3": "^12.11.1",
"docus": "^5.13.0",
@@ -1355,6 +1362,51 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@iconify-json/carbon": {
"version": "1.2.26",
"resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.2.26.tgz",
"integrity": "sha512-UVzlnCAzSkrNydW3GYESeggndriR1JSgU5M8DNz9S0Fu10I3NpJUYPWar+WYjMAQUIm9B7mljPpc30oal3PRSQ==",
"license": "Apache-2.0",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/cbi": {
"version": "1.2.59",
"resolved": "https://registry.npmjs.org/@iconify-json/cbi/-/cbi-1.2.59.tgz",
"integrity": "sha512-kvp05ix+eJk509JJ8CTEUvdNx0F5x9Xg5oBMMDnjyLzKYZTh7nEWswvGKPmAmnEQ0pVjt8R+eS65LwYmi1OvFA==",
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/cib": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@iconify-json/cib/-/cib-1.2.3.tgz",
"integrity": "sha512-MMvRXVTHt82z3bYN19JDAjv/X8OBNC/1B3I334SV/1nyhdPeeyvJZVoI1cGuNqYznBjMnWxKPtIhhiVgGQ1CzQ==",
"license": "CC0-1.0",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/fluent-color": {
"version": "1.2.21",
"resolved": "https://registry.npmjs.org/@iconify-json/fluent-color/-/fluent-color-1.2.21.tgz",
"integrity": "sha512-VETjGx7BMpVsCR3HV5g5t6oBGZ92XKzIRClS5xUvDn1YHAkQTF9OyF/aFdtxYOC8JROoI1SNO/Ql6jZeK7mroA==",
"license": "MIT",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/logos": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@iconify-json/logos/-/logos-1.2.14.tgz",
"integrity": "sha512-O36DicXkgAMT6NAsH7MTWlOql8NktHz9fBCItTjz6L/9gyRXw4vQj6qTeVgww5UZWiVugUV1MhVFA/g9HWIkBw==",
"license": "CC0-1.0",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/lucide": {
"version": "1.2.127",
"resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.127.tgz",
@@ -1364,6 +1416,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/noto": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@iconify-json/noto/-/noto-1.2.7.tgz",
"integrity": "sha512-2pNHliXnMLpnepNTZwR9hPBSl6GmhIfXPnudFCHY5v9pJhmA7LoVb/pFY0qLDY/k+3ikET8RXRf7XFxc/pHObA==",
"license": "Apache-2.0",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/simple-icons": {
"version": "1.2.94",
"resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.94.tgz",
@@ -1373,6 +1434,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/solar": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@iconify-json/solar/-/solar-1.2.10.tgz",
"integrity": "sha512-ChpZ3kNw8nDhHfUoLq5JJ3AdIzytFm57dFkjAS0fjXnL5a7WwibTINjMKfvqQDhuXBrgqejNLyGp8CCpG8UyCg==",
"license": "CC-BY-4.0",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/vscode-icons": {
"version": "1.2.76",
"resolved": "https://registry.npmjs.org/@iconify-json/vscode-icons/-/vscode-icons-1.2.76.tgz",
+7
View File
@@ -6,6 +6,13 @@
"build": "NUXT_SITE_URL=https://docu.djeex.fr nuxt build"
},
"dependencies": {
"@iconify-json/carbon": "^1.2.26",
"@iconify-json/cbi": "^1.2.59",
"@iconify-json/cib": "^1.2.3",
"@iconify-json/fluent-color": "^1.2.21",
"@iconify-json/logos": "^1.2.14",
"@iconify-json/noto": "^1.2.7",
"@iconify-json/solar": "^1.2.10",
"@nuxtjs/i18n": "^10.6.0",
"better-sqlite3": "^12.11.1",
"docus": "^5.13.0",