fix: optimize and namespace SVG logo IDs at build time with SVGO

- Add `svgo` as a dev dep and run each logo through `optimize` with `multipass: true` + `preset-default` at build time
- Apply the `prefixIds` plugin scoped to the module slug (e.g. `email-resend-light`) so gradient / clip-path `id` collisions between logos are impossible when multiple modules are rendered on the same page
This commit is contained in:
2026-05-25 21:07:54 -04:00
parent c7ba9bec3c
commit 2103e3607e
3 changed files with 133 additions and 5 deletions
+1
View File
@@ -19,6 +19,7 @@
},
"devDependencies": {
"@types/validate-npm-package-name": "^4.0.2",
"svgo": "^4.0.1",
"typescript": "^6.0.3",
"vite-plus": "catalog:",
"vitest": "catalog:"
+20 -5
View File
@@ -15,6 +15,8 @@ import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { optimize } from "svgo";
import { manifestJsonSchema } from "./manifest";
import { CATEGORIES, type Logo, type Module, type RegistryIndex } from "./module";
@@ -46,7 +48,7 @@ async function main() {
// no follow-up requests to retrieve template files. Local dev still works
// because the runner falls back to disk when `content` is absent.
const templatesDir = path.join(modulesDir, dir, "templates");
const logo = readLogo(path.join(modulesDir, dir));
const logo = readLogo(path.join(modulesDir, dir), dir);
const readme = readReadme(path.join(modulesDir, dir));
const inlined: Module = {
...mod,
@@ -102,21 +104,34 @@ async function main() {
* theme-agnostic SVG) or `logo-light.svg` + `logo-dark.svg` (a theme pair)
* in its directory. We read whichever is present and return the inlined
* markup; if neither exists the module just renders without one.
*
* Each SVG is optimized + namespaced through SVGO. The `prefixIds` plugin
* scopes every `id` (and every `url(#…)` / `href="#…"` / `xlink:href="#…"`
* reference) by the module slug, so when many logos share the document
* (slot grids, search results), gradients and clip-paths from one logo
* never bleed into another via collisions like `id="a"`.
*/
function readLogo(moduleDir: string): Logo | undefined {
function readLogo(moduleDir: string, slug: string): Logo | undefined {
const light = path.join(moduleDir, "logo-light.svg");
const dark = path.join(moduleDir, "logo-dark.svg");
if (fs.existsSync(light) && fs.existsSync(dark)) {
return {
light: fs.readFileSync(light, "utf8"),
dark: fs.readFileSync(dark, "utf8"),
light: optimizeLogo(fs.readFileSync(light, "utf8"), `${slug}-light`),
dark: optimizeLogo(fs.readFileSync(dark, "utf8"), `${slug}-dark`),
};
}
const single = path.join(moduleDir, "logo.svg");
if (fs.existsSync(single)) return fs.readFileSync(single, "utf8");
if (fs.existsSync(single)) return optimizeLogo(fs.readFileSync(single, "utf8"), slug);
return undefined;
}
function optimizeLogo(svg: string, prefix: string): string {
return optimize(svg, {
multipass: true,
plugins: ["preset-default", { name: "prefixIds", params: { prefix, delim: "-" } }],
}).data;
}
/**
* Convention: a module ships its README contribution by dropping `readme.md`
* in its directory. We inline the contents into the module manifest so HTTP-
+112
View File
@@ -305,6 +305,9 @@ importers:
'@types/validate-npm-package-name':
specifier: ^4.0.2
version: 4.0.2
svgo:
specifier: ^4.0.1
version: 4.0.1
typescript:
specifier: ^6.0.3
version: 6.0.3
@@ -2636,6 +2639,9 @@ packages:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
engines: {node: 18 || 20 || >=22}
@@ -2813,15 +2819,30 @@ packages:
srvx:
optional: true
css-select@5.2.2:
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
css-tree@2.2.1:
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
css-tree@3.2.1:
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-what@6.2.2:
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
engines: {node: '>= 6'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
csso@5.0.5:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -2934,9 +2955,22 @@ packages:
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.4.5:
resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dotenv@17.4.2:
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
engines: {node: '>=12'}
@@ -2977,6 +3011,10 @@ packages:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -3847,6 +3885,9 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
@@ -4124,6 +4165,9 @@ packages:
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
engines: {node: '>=18'}
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -4565,6 +4609,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
saxes@6.0.0:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
@@ -4755,6 +4803,11 @@ packages:
style-to-object@1.0.14:
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
svgo@4.0.1:
resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==}
engines: {node: '>=16'}
hasBin: true
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@@ -7311,6 +7364,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
boolbase@1.0.0: {}
brace-expansion@5.0.6:
dependencies:
balanced-match: 4.0.4
@@ -7449,13 +7504,32 @@ snapshots:
optionalDependencies:
srvx: 0.11.16
css-select@5.2.2:
dependencies:
boolbase: 1.0.0
css-what: 6.2.2
domhandler: 5.0.3
domutils: 3.2.2
nth-check: 2.1.1
css-tree@2.2.1:
dependencies:
mdn-data: 2.0.28
source-map-js: 1.2.1
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
source-map-js: 1.2.1
css-what@6.2.2: {}
cssesc@3.0.0: {}
csso@5.0.5:
dependencies:
css-tree: 2.2.1
csstype@3.2.3: {}
data-uri-to-buffer@4.0.1: {}
@@ -7518,10 +7592,28 @@ snapshots:
dom-accessibility-api@0.5.16: {}
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
domelementtype@2.3.0: {}
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
dompurify@3.4.5:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dotenv@17.4.2: {}
dotenv@8.6.0: {}
@@ -7559,6 +7651,8 @@ snapshots:
ansi-colors: 4.1.3
strip-ansi: 6.0.1
entities@4.5.0: {}
entities@6.0.1: {}
entities@8.0.0: {}
@@ -8570,6 +8664,8 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
mdn-data@2.0.28: {}
mdn-data@2.27.1: {}
media-typer@1.1.0: {}
@@ -8999,6 +9095,10 @@ snapshots:
path-key: 4.0.0
unicorn-magic: 0.3.0
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -9562,6 +9662,8 @@ snapshots:
safer-buffer@2.1.2: {}
sax@1.6.0: {}
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
@@ -9800,6 +9902,16 @@ snapshots:
dependencies:
inline-style-parser: 0.2.7
svgo@4.0.1:
dependencies:
commander: 11.1.0
css-select: 5.2.2
css-tree: 3.2.1
css-what: 6.2.2
csso: 5.0.5
picocolors: 1.1.1
sax: 1.6.0
symbol-tree@3.2.4: {}
tagged-tag@1.0.0: {}