From 94d2259782d6a53862869f9ed1cb92ebaf8ba261 Mon Sep 17 00:00:00 2001 From: Benjamin Schaaf Date: Tue, 9 Jun 2026 16:00:58 +1000 Subject: [PATCH 1/4] Don't include icons for labels with 1 or fewer uses They also aren't included in the list of labels, so no reason to bloat the icons for insignificant labels. Additionally, this improves exclusions and aliases of icons. C, C# and C++ icons look very silly next to their names, so exclude those. --- label-icons-config.json | 6 +- label-icons.json | 38 ---------- static/label-icons.svg | 148 ------------------------------------- util/build-label-icons.mjs | 7 +- 4 files changed, 8 insertions(+), 191 deletions(-) diff --git a/label-icons-config.json b/label-icons-config.json index 8714418ae..96a344240 100644 --- a/label-icons-config.json +++ b/label-icons-config.json @@ -1,9 +1,11 @@ { "_": "Manually maintained set of icons to exclude and an alias map from labels to icon/language ids", - "exclude": ["ai"], + "exclude": ["ai", "c", "c#", "c++"], "aliases": { "javascript": "js", "ecmascript": "js", - "ecmascript6": "js" + "ecmascript6": "js", + "cpp": "c++", + "mac": "apple" } } diff --git a/label-icons.json b/label-icons.json index 1a2a7730c..fbcf38078 100644 --- a/label-icons.json +++ b/label-icons.json @@ -4,27 +4,16 @@ "arduino": "cyan", "asciidoc": "cyan", "astro": "graphite", - "audio": "red", "azure": "sky", "bazel": "green", "binary": "graphite", - "blade": "orange", - "bower": "yellow", - "c": "purple", - "c#": "blue", - "c++": "sky", - "cairo": "graphite", "clojure": "purple", "cmake": "blue", "coffeescript": "orange", "composer": "orange", - "cpp": "sky", "crystal": "graphite", "css": "blue", "csv": "green", - "cuda": "green", - "dart": "cyan", - "delphi": "red", "diff": "yellow", "dlang": "red", "docker": "sky", @@ -33,14 +22,11 @@ "ember": "green", "erlang": "red", "eslint": "purple", - "excel": "green", "font": "green", "fortran": "red", "git": "orange", - "gleam": "pink", "go": "sky", "graphql": "pink", - "graphviz": "cyan", "groovy": "cyan", "haml": "yellow", "haskell": "purple", @@ -52,21 +38,16 @@ "jq": "orange", "js": "yellow", "json": "green", - "jsp": "purple", "jsx": "blue", "julia": "purple", - "kdl": "purple", "kotlin": "blue", "less": "blue", - "liquid": "green", "lisp": "red", "log": "graphite", - "lsl": "cyan", "lua": "blue", "markdown": "graphite", "markup": "purple", "matlab": "orange", - "maven": "red", "mdx": "cyan", "nginx": "green", "nim": "yellow", @@ -74,24 +55,15 @@ "note": "green", "npm": "red", "nsis": "purple", - "ocaml": "yellow", - "odin": "blue", - "parquet": "sky", - "pawn": "orange", "pdf": "red", "perl": "graphite", "php": "blue", - "postcss": "red", "powershell": "blue", "preferences": "yellow", - "prisma": "graphite", "protobuf": "graphite", - "pug": "yellow", "puppet": "purple", "python": "yellow", - "qt": "green", "r": "blue", - "racket": "red", "rails": "red", "react": "sky", "restructuredtext": "sky", @@ -103,7 +75,6 @@ "scss": "pink", "settings": "graphite", "shell": "green", - "slim": "cyan", "solidity": "graphite", "source": "sky", "sql": "blue", @@ -120,24 +91,15 @@ "textile": "pink", "todo": "green", "toml": "cyan", - "travis": "orange", "twig": "green", "typescript": "sky", - "typst": "cyan", "unreal": "graphite", - "v": "cyan", "vala": "sky", "verilog": "blue", "vhdl": "blue", "vim": "green", "vue": "cyan", - "vyper": "graphite", - "wast": "purple", - "webpack": "blue", - "wgsl": "sky", "windows": "sky", - "wit": "pink", "yaml": "yellow", - "yarn": "blue", "zig": "yellow" } diff --git a/static/label-icons.svg b/static/label-icons.svg index 17527e642..9ffe25eed 100644 --- a/static/label-icons.svg +++ b/static/label-icons.svg @@ -18,9 +18,6 @@ - - - @@ -32,24 +29,6 @@ - - - - - - - - - - - - - - - - - - @@ -62,9 +41,6 @@ - - - @@ -74,21 +50,6 @@ - - - - - - - - - - - - - - - @@ -115,9 +76,6 @@ - - - @@ -127,12 +85,6 @@ - - - - - - @@ -163,9 +115,6 @@ - - - @@ -202,39 +151,24 @@ - - - - - - - - - - - - - - - @@ -248,15 +182,6 @@ - - - - - - - - - @@ -279,19 +204,6 @@ - - - - - - - - - - - - @@ -301,18 +213,12 @@ - - - - - - @@ -322,9 +228,6 @@ - - - @@ -332,17 +235,9 @@ - - - - - - - - @@ -379,9 +274,6 @@ - - - @@ -431,32 +323,16 @@ - - - - - - - - - - - - @@ -472,36 +348,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/util/build-label-icons.mjs b/util/build-label-icons.mjs index edde7b617..09bd5bd21 100644 --- a/util/build-label-icons.mjs +++ b/util/build-label-icons.mjs @@ -64,7 +64,7 @@ function main() { ensureDir(path.dirname(spritePath)) /** @type {Set} */ - const usedLabels = new Set() + const usedLabels = {} /** @type {Record} */ let aliases = {} /** @type {Set} */ @@ -106,7 +106,8 @@ function main() { const key = label.trim().toLowerCase() if (!key) continue const canonical = (aliases[key] && aliases[key].trim().toLowerCase()) || key - usedLabels.add(canonical) + usedLabels[canonical] ??= 0 + usedLabels[canonical] += 1 } } } @@ -140,7 +141,7 @@ function main() { const typeKey = type.trim().toLowerCase() // If we have a set of used labels, restrict icons to those labels (or alias targets) - if (usedLabels.size > 0 && !usedLabels.has(typeKey)) { + if (usedLabels.size > 0 && (!(typeKey in usedLabels) || usedLabels[typeKey] <= 1)) { continue } From 7cbfbff5fc7b4aa203b37168fe47a6604975e7a3 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Wed, 24 Jun 2026 15:08:47 +0200 Subject: [PATCH 2/4] Prune low-use label icons during build Keep build-label-icons focused on shipping the complete used-label icon set, then derive the low-usage subset during the Eleventy build. This lets the generated labels page, search index metadata, and copied SVG sprite agree with the current build dataset. Also keep the manual C-family exclusions and drop the unused mac to apple alias. --- eleventy.config.mjs | 29 +++++++- eleventy.filters.mjs | 79 +++++++++++++++++++-- label-icons-config.json | 3 +- label-icons.json | 34 ++++++++++ static/label-icons.svg | 136 +++++++++++++++++++++++++++++++++++++ util/build-label-icons.mjs | 7 +- 6 files changed, 275 insertions(+), 13 deletions(-) diff --git a/eleventy.config.mjs b/eleventy.config.mjs index 611a9e5c9..9573849fb 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -27,6 +27,7 @@ const FEATURED_LABELS = [ 'theme', ] const LABELS_RANK = new Map(FEATURED_LABELS.map((label, index) => [label, index])) +const LABEL_ICON_MINIMUM_USAGE = 2 const MS_IN_DAY = 24 * 60 * 60 * 1000 const MAGIC_FRESHNESS_WINDOW_DAYS = 365 * 2 // bonus for packages that had updates @@ -374,6 +375,7 @@ export default async function (eleventyConfig) { const siteOrigin = isProd ? prodOrigin : devOrigin const staticOutputDir = isProd ? 'static_' + util.gitHash : 'static' const bundledScriptEntries = new Set() + let labelIcons = null eleventyConfig.addPassthroughCopy( { static: staticOutputDir }, @@ -388,6 +390,11 @@ export default async function (eleventyConfig) { eleventyConfig.on('eleventy.after', async ({ directories } = {}) => { const outputDir = directories?.output ?? '_site' await writeVendorModules(path.join(outputDir, staticOutputDir, 'vendor')) + writePrunedLabelIconSprite( + 'static/label-icons.svg', + path.join(outputDir, staticOutputDir, 'label-icons.svg'), + labelIcons?.sources, + ) if (!isProd) { return @@ -524,6 +531,8 @@ export default async function (eleventyConfig) { const packages = all_packages.map(packageData) const packagesWithMagic = computeMagicMetadata(packages) + const labels = util.collectLabels(all_packages) + labelIcons = filters.configureLabelIcons(labels, { minimumUsage: LABEL_ICON_MINIMUM_USAGE }) const livingHomePackages = packages.filter(pkg => !pkg.removed) @@ -613,9 +622,7 @@ export default async function (eleventyConfig) { } } - eleventyConfig.addCollection('labels', () => { - return util.collectLabels(all_packages) - }) + eleventyConfig.addCollection('labels', () => labels) eleventyConfig.addCollection('libraries', () => { return Object.values(workspace.libraries) @@ -678,6 +685,7 @@ export default async function (eleventyConfig) { // Register all named exports from external module as filters for (const [name, fn] of Object.entries(filters)) { + if (name === 'configureLabelIcons') continue eleventyConfig.addFilter(name, fn) } @@ -696,6 +704,21 @@ export default async function (eleventyConfig) { } } +function writePrunedLabelIconSprite(sourcePath, outputPath, visibleSources) { + if (!(visibleSources instanceof Set) || !fs.existsSync(sourcePath)) { + return + } + + const source = fs.readFileSync(sourcePath, 'utf8') + const pruned = source.replace( + /]*\bid="label-icon-([^"]+)"[^>]*>[\s\S]*?<\/symbol>\r?\n?/g, + (symbol, iconSource) => visibleSources.has(iconSource) ? symbol : '', + ) + + fs.mkdirSync(path.dirname(outputPath), { recursive: true }) + fs.writeFileSync(outputPath, pruned, 'utf8') +} + async function bundleJs(staticOutputDir, entries, isProd) { if (!entries.size) { return diff --git a/eleventy.filters.mjs b/eleventy.filters.mjs index ac04f6547..d1b50c400 100644 --- a/eleventy.filters.mjs +++ b/eleventy.filters.mjs @@ -14,6 +14,8 @@ const configPath = path.join(__dirname, 'label-icons-config.json') let labelIconSourceSet = new Set() let labelIconAliases = {} let labelIconTints = {} +let labelIconVisibleSourceSet = null +let labelIconVisibleTints = null const longDateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }) const compactNumberFormatter = new Intl.NumberFormat('en', { notation: 'compact' }) @@ -67,7 +69,17 @@ export function label_icon_aliases_json() { } export function label_icon_tints_json() { - return JSON.stringify(labelIconTints) + return JSON.stringify(activeLabelIconTints()) +} + +export function configureLabelIcons(labels, { minimumUsage = 1 } = {}) { + labelIconVisibleSourceSet = visibleLabelIconSources(labels, minimumUsage) + labelIconVisibleTints = labelIconTintsForSources(labelIconVisibleSourceSet) + + return { + sources: labelIconVisibleSourceSet, + tints: labelIconVisibleTints, + } } export function label_normalization_note(changes) { @@ -102,7 +114,7 @@ export function search_index_json(packages) { return JSON.stringify({ packages: packages.map(compactSearchPackage), label_icon_aliases: labelIconAliases, - label_icon_tints: labelIconTints, + label_icon_tints: activeLabelIconTints(), }) } @@ -186,7 +198,30 @@ function joinAsSentenceList(parts) { return `${parts.slice(0, -1).join(', ')}, and ${parts.at(-1)}` } -function canonicalLabel(label) { +function visibleLabelIconSources(labels, minimumUsage) { + const counts = new Map() + const threshold = Math.max(1, Number(minimumUsage) || 1) + + for (const item of labels ?? []) { + const key = typeof item?.key === 'string' ? item.key : String(item ?? '') + const canonical = sourceLabelFor(key) + if (!canonical) continue + + const count = Number(item?.count ?? 1) + counts.set(canonical, (counts.get(canonical) ?? 0) + (Number.isFinite(count) ? count : 1)) + } + + const sources = new Set() + for (const source of labelIconSourceSet) { + if ((counts.get(source) ?? 0) >= threshold) { + sources.add(source) + } + } + + return sources +} + +function sourceLabelFor(label) { if (typeof label !== 'string') return '' const normalized = label.trim().toLowerCase() if (!normalized) return '' @@ -203,6 +238,42 @@ function canonicalLabel(label) { return '' } +function labelIconTintsForSources(sources) { + const tints = {} + for (const source of sources) { + if (Object.prototype.hasOwnProperty.call(labelIconTints, source)) { + tints[source] = labelIconTints[source] + } + } + return tints +} + +function activeLabelIconSourceSet() { + return labelIconVisibleSourceSet ?? labelIconSourceSet +} + +function activeLabelIconTints() { + return labelIconVisibleTints ?? labelIconTints +} + +function canonicalLabel(label) { + if (typeof label !== 'string') return '' + const normalized = label.trim().toLowerCase() + if (!normalized) return '' + + const sourceSet = activeLabelIconSourceSet() + const alias = labelIconAliases[normalized] + if (alias && sourceSet.has(alias)) { + return alias + } + + if (sourceSet.has(normalized)) { + return normalized + } + + return '' +} + export function label_icon_id(label) { const canonical = canonicalLabel(label) if (!canonical) return '' @@ -212,7 +283,7 @@ export function label_icon_id(label) { export function label_icon_tint(label) { const canonical = canonicalLabel(label) if (!canonical) return '' - return labelIconTints[canonical] ?? '' + return activeLabelIconTints()[canonical] ?? '' } // number formatting with grouping (e.g. 10,000) diff --git a/label-icons-config.json b/label-icons-config.json index 96a344240..8952e5758 100644 --- a/label-icons-config.json +++ b/label-icons-config.json @@ -5,7 +5,6 @@ "javascript": "js", "ecmascript": "js", "ecmascript6": "js", - "cpp": "c++", - "mac": "apple" + "cpp": "c++" } } diff --git a/label-icons.json b/label-icons.json index fbcf38078..784ca9a6f 100644 --- a/label-icons.json +++ b/label-icons.json @@ -4,9 +4,13 @@ "arduino": "cyan", "asciidoc": "cyan", "astro": "graphite", + "audio": "red", "azure": "sky", "bazel": "green", "binary": "graphite", + "blade": "orange", + "bower": "yellow", + "cairo": "graphite", "clojure": "purple", "cmake": "blue", "coffeescript": "orange", @@ -14,6 +18,9 @@ "crystal": "graphite", "css": "blue", "csv": "green", + "cuda": "green", + "dart": "cyan", + "delphi": "red", "diff": "yellow", "dlang": "red", "docker": "sky", @@ -22,11 +29,14 @@ "ember": "green", "erlang": "red", "eslint": "purple", + "excel": "green", "font": "green", "fortran": "red", "git": "orange", + "gleam": "pink", "go": "sky", "graphql": "pink", + "graphviz": "cyan", "groovy": "cyan", "haml": "yellow", "haskell": "purple", @@ -38,16 +48,21 @@ "jq": "orange", "js": "yellow", "json": "green", + "jsp": "purple", "jsx": "blue", "julia": "purple", + "kdl": "purple", "kotlin": "blue", "less": "blue", + "liquid": "green", "lisp": "red", "log": "graphite", + "lsl": "cyan", "lua": "blue", "markdown": "graphite", "markup": "purple", "matlab": "orange", + "maven": "red", "mdx": "cyan", "nginx": "green", "nim": "yellow", @@ -55,15 +70,24 @@ "note": "green", "npm": "red", "nsis": "purple", + "ocaml": "yellow", + "odin": "blue", + "parquet": "sky", + "pawn": "orange", "pdf": "red", "perl": "graphite", "php": "blue", + "postcss": "red", "powershell": "blue", "preferences": "yellow", + "prisma": "graphite", "protobuf": "graphite", + "pug": "yellow", "puppet": "purple", "python": "yellow", + "qt": "green", "r": "blue", + "racket": "red", "rails": "red", "react": "sky", "restructuredtext": "sky", @@ -75,6 +99,7 @@ "scss": "pink", "settings": "graphite", "shell": "green", + "slim": "cyan", "solidity": "graphite", "source": "sky", "sql": "blue", @@ -91,15 +116,24 @@ "textile": "pink", "todo": "green", "toml": "cyan", + "travis": "orange", "twig": "green", "typescript": "sky", + "typst": "cyan", "unreal": "graphite", + "v": "cyan", "vala": "sky", "verilog": "blue", "vhdl": "blue", "vim": "green", "vue": "cyan", + "vyper": "graphite", + "wast": "purple", + "webpack": "blue", + "wgsl": "sky", "windows": "sky", + "wit": "pink", "yaml": "yellow", + "yarn": "blue", "zig": "yellow" } diff --git a/static/label-icons.svg b/static/label-icons.svg index 9ffe25eed..52e0e3305 100644 --- a/static/label-icons.svg +++ b/static/label-icons.svg @@ -18,6 +18,9 @@ + + + @@ -29,6 +32,15 @@ + + + + + + + + + @@ -50,6 +62,21 @@ + + + + + + + + + + + + + + + @@ -76,6 +103,9 @@ + + + @@ -85,6 +115,12 @@ + + + + + + @@ -115,6 +151,9 @@ + + + @@ -151,24 +190,39 @@ + + + + + + + + + + + + + + + @@ -182,6 +236,15 @@ + + + + + + + + + @@ -204,6 +267,19 @@ + + + + + + + + + + + + @@ -213,12 +289,18 @@ + + + + + + @@ -228,6 +310,9 @@ + + + @@ -235,9 +320,17 @@ + + + + + + + + @@ -274,6 +367,9 @@ + + + @@ -323,16 +419,32 @@ + + + + + + + + + + + + @@ -348,12 +460,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/util/build-label-icons.mjs b/util/build-label-icons.mjs index 09bd5bd21..edde7b617 100644 --- a/util/build-label-icons.mjs +++ b/util/build-label-icons.mjs @@ -64,7 +64,7 @@ function main() { ensureDir(path.dirname(spritePath)) /** @type {Set} */ - const usedLabels = {} + const usedLabels = new Set() /** @type {Record} */ let aliases = {} /** @type {Set} */ @@ -106,8 +106,7 @@ function main() { const key = label.trim().toLowerCase() if (!key) continue const canonical = (aliases[key] && aliases[key].trim().toLowerCase()) || key - usedLabels[canonical] ??= 0 - usedLabels[canonical] += 1 + usedLabels.add(canonical) } } } @@ -141,7 +140,7 @@ function main() { const typeKey = type.trim().toLowerCase() // If we have a set of used labels, restrict icons to those labels (or alias targets) - if (usedLabels.size > 0 && (!(typeKey in usedLabels) || usedLabels[typeKey] <= 1)) { + if (usedLabels.size > 0 && !usedLabels.has(typeKey)) { continue } From d1c200b60fbe667806f9aa898a557a9fd64b34c8 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Wed, 22 Jul 2026 14:57:31 +0200 Subject: [PATCH 3/4] Split label icons into primary and fallback sprites Keep icons used by more than one package in the primary sprite, along with icons shown on the initial homepage and icons belonging to packages updated in the last year. Route less common icons to a second sprite so all labels retain their icons without burdening the initial page load. The current data puts 107 icons in the 41 KiB Brotli primary sprite and 30 icons in the 30 KiB fallback. The fallback is not needed until page 36 of Newest or page 47 of Recently Updated. --- _includes/packages/macros.njk | 3 +- eleventy.config.mjs | 48 ++++++++++++++++----- eleventy.filters.mjs | 81 +++++++++++++++++++++-------------- labels.njk | 3 +- static/module/card.js | 4 +- static/package-search.js | 1 + 6 files changed, 94 insertions(+), 46 deletions(-) diff --git a/_includes/packages/macros.njk b/_includes/packages/macros.njk index 0a1eba647..f3c6b0ba6 100644 --- a/_includes/packages/macros.njk +++ b/_includes/packages/macros.njk @@ -184,8 +184,9 @@ {% set icon_id = (label | label_icon_id) if with_icons else '' %} {% if icon_id %} {% set tint = (label | label_icon_tint) if with_icons else '' %} + {% set icon_sprite = (label | label_icon_sprite) if with_icons else '' %} {% endif %} {{ label }} diff --git a/eleventy.config.mjs b/eleventy.config.mjs index 9573849fb..39d518646 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -28,6 +28,7 @@ const FEATURED_LABELS = [ ] const LABELS_RANK = new Map(FEATURED_LABELS.map((label, index) => [label, index])) const LABEL_ICON_MINIMUM_USAGE = 2 +const LABEL_ICON_RECENT_WINDOW_DAYS = 365 const MS_IN_DAY = 24 * 60 * 60 * 1000 const MAGIC_FRESHNESS_WINDOW_DAYS = 365 * 2 // bonus for packages that had updates @@ -390,10 +391,10 @@ export default async function (eleventyConfig) { eleventyConfig.on('eleventy.after', async ({ directories } = {}) => { const outputDir = directories?.output ?? '_site' await writeVendorModules(path.join(outputDir, staticOutputDir, 'vendor')) - writePrunedLabelIconSprite( + writeLabelIconSprites( 'static/label-icons.svg', - path.join(outputDir, staticOutputDir, 'label-icons.svg'), - labelIcons?.sources, + path.join(outputDir, staticOutputDir), + labelIcons, ) if (!isProd) { @@ -532,7 +533,6 @@ export default async function (eleventyConfig) { const packages = all_packages.map(packageData) const packagesWithMagic = computeMagicMetadata(packages) const labels = util.collectLabels(all_packages) - labelIcons = filters.configureLabelIcons(labels, { minimumUsage: LABEL_ICON_MINIMUM_USAGE }) const livingHomePackages = packages.filter(pkg => !pkg.removed) @@ -544,6 +544,21 @@ export default async function (eleventyConfig) { const newestHomePackages = packagesByDate('first_seen').slice(0, HOME_SECTION_PACKAGE_LIMIT) const updatedHomePackages = packagesByDate('last_modified').slice(0, HOME_SECTION_PACKAGE_LIMIT) + const latestPackageUpdate = livingHomePackages.reduce( + (latest, pkg) => Math.max(latest, new Date(pkg.last_modified ?? 0).getTime() || 0), + 0, + ) + const recentCutoff = latestPackageUpdate - LABEL_ICON_RECENT_WINDOW_DAYS * MS_IN_DAY + const recentlyUpdatedPackages = livingHomePackages.filter( + pkg => new Date(pkg.last_modified ?? 0).getTime() >= recentCutoff, + ) + labelIcons = filters.configureLabelIcons(labels, { + minimumUsage: LABEL_ICON_MINIMUM_USAGE, + preferredPackages: [ + ...newestHomePackages, + ...recentlyUpdatedPackages, + ], + }) const remarkablePackages = () => { const alreadyFeatured = new Set() @@ -704,19 +719,30 @@ export default async function (eleventyConfig) { } } -function writePrunedLabelIconSprite(sourcePath, outputPath, visibleSources) { - if (!(visibleSources instanceof Set) || !fs.existsSync(sourcePath)) { +function writeLabelIconSprites(sourcePath, outputDir, labelIcons) { + if (!(labelIcons?.primarySources instanceof Set) || !fs.existsSync(sourcePath)) { return } const source = fs.readFileSync(sourcePath, 'utf8') - const pruned = source.replace( - /]*\bid="label-icon-([^"]+)"[^>]*>[\s\S]*?<\/symbol>\r?\n?/g, - (symbol, iconSource) => visibleSources.has(iconSource) ? symbol : '', + fs.mkdirSync(outputDir, { recursive: true }) + fs.writeFileSync( + path.join(outputDir, 'label-icons.svg'), + labelIconSpriteForSources(source, labelIcons.primarySources), + 'utf8', ) + fs.writeFileSync( + path.join(outputDir, 'label-icons-extra.svg'), + labelIconSpriteForSources(source, labelIcons.secondarySources), + 'utf8', + ) +} - fs.mkdirSync(path.dirname(outputPath), { recursive: true }) - fs.writeFileSync(outputPath, pruned, 'utf8') +function labelIconSpriteForSources(source, sources) { + return source.replace( + /]*\bid="label-icon-([^"]+)"[^>]*>[\s\S]*?<\/symbol>\r?\n?/g, + (symbol, iconSource) => sources.has(iconSource) ? symbol : '', + ) } async function bundleJs(staticOutputDir, entries, isProd) { diff --git a/eleventy.filters.mjs b/eleventy.filters.mjs index d1b50c400..d56aeb7a1 100644 --- a/eleventy.filters.mjs +++ b/eleventy.filters.mjs @@ -14,8 +14,8 @@ const configPath = path.join(__dirname, 'label-icons-config.json') let labelIconSourceSet = new Set() let labelIconAliases = {} let labelIconTints = {} -let labelIconVisibleSourceSet = null -let labelIconVisibleTints = null +let labelIconPrimarySourceSet = null +let labelIconSecondarySourceSet = null const longDateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }) const compactNumberFormatter = new Intl.NumberFormat('en', { notation: 'compact' }) @@ -69,16 +69,18 @@ export function label_icon_aliases_json() { } export function label_icon_tints_json() { - return JSON.stringify(activeLabelIconTints()) + return JSON.stringify(labelIconTints) } -export function configureLabelIcons(labels, { minimumUsage = 1 } = {}) { - labelIconVisibleSourceSet = visibleLabelIconSources(labels, minimumUsage) - labelIconVisibleTints = labelIconTintsForSources(labelIconVisibleSourceSet) +export function configureLabelIcons(labels, { minimumUsage = 1, preferredPackages = [] } = {}) { + labelIconPrimarySourceSet = primaryLabelIconSources(labels, minimumUsage, preferredPackages) + labelIconSecondarySourceSet = new Set( + Array.from(labelIconSourceSet).filter(source => !labelIconPrimarySourceSet.has(source)), + ) return { - sources: labelIconVisibleSourceSet, - tints: labelIconVisibleTints, + primarySources: labelIconPrimarySourceSet, + secondarySources: labelIconSecondarySourceSet, } } @@ -114,7 +116,8 @@ export function search_index_json(packages) { return JSON.stringify({ packages: packages.map(compactSearchPackage), label_icon_aliases: labelIconAliases, - label_icon_tints: activeLabelIconTints(), + label_icon_tints: labelIconTints, + label_icon_secondary: Array.from(labelIconSecondarySourceSet ?? []), }) } @@ -198,7 +201,7 @@ function joinAsSentenceList(parts) { return `${parts.slice(0, -1).join(', ')}, and ${parts.at(-1)}` } -function visibleLabelIconSources(labels, minimumUsage) { +function primaryLabelIconSources(labels, minimumUsage, preferredPackages) { const counts = new Map() const threshold = Math.max(1, Number(minimumUsage) || 1) @@ -218,6 +221,13 @@ function visibleLabelIconSources(labels, minimumUsage) { } } + for (const pkg of preferredPackages) { + for (const label of pkg.labels ?? []) { + const source = sourceLabelFor(label) + if (source) sources.add(source) + } + } + return sources } @@ -238,36 +248,17 @@ function sourceLabelFor(label) { return '' } -function labelIconTintsForSources(sources) { - const tints = {} - for (const source of sources) { - if (Object.prototype.hasOwnProperty.call(labelIconTints, source)) { - tints[source] = labelIconTints[source] - } - } - return tints -} - -function activeLabelIconSourceSet() { - return labelIconVisibleSourceSet ?? labelIconSourceSet -} - -function activeLabelIconTints() { - return labelIconVisibleTints ?? labelIconTints -} - function canonicalLabel(label) { if (typeof label !== 'string') return '' const normalized = label.trim().toLowerCase() if (!normalized) return '' - const sourceSet = activeLabelIconSourceSet() const alias = labelIconAliases[normalized] - if (alias && sourceSet.has(alias)) { + if (alias && labelIconSourceSet.has(alias)) { return alias } - if (sourceSet.has(normalized)) { + if (labelIconSourceSet.has(normalized)) { return normalized } @@ -283,7 +274,15 @@ export function label_icon_id(label) { export function label_icon_tint(label) { const canonical = canonicalLabel(label) if (!canonical) return '' - return activeLabelIconTints()[canonical] ?? '' + return labelIconTints[canonical] ?? '' +} + +export function label_icon_sprite(label) { + const canonical = canonicalLabel(label) + if (!canonical) return '' + return labelIconSecondarySourceSet?.has(canonical) + ? 'static/label-icons-extra.svg' + : 'static/label-icons.svg' } // number formatting with grouping (e.g. 10,000) @@ -336,6 +335,24 @@ export function bust(p) { if (import.meta.vitest) { const { describe, it, expect } = import.meta.vitest + describe('label icon sprites', () => { + it('keeps common and preferred icons in the primary sprite', () => { + const sprites = configureLabelIcons([ + { key: 'python', count: 2 }, + { key: 'typst', count: 1 }, + ], { + minimumUsage: 2, + preferredPackages: [{ labels: ['typst'] }], + }) + + expect(sprites.primarySources).toContain('python') + expect(sprites.primarySources).toContain('typst') + expect(sprites.secondarySources).toContain('audio') + expect(label_icon_sprite('typst')).toBe('static/label-icons.svg') + expect(label_icon_sprite('audio')).toBe('static/label-icons-extra.svg') + }) + }) + describe('date_time_format', () => { it.each([ [[new Date('2025-09-08T11:12:59Z')], '2025-09-08 11:12'], diff --git a/labels.njk b/labels.njk index 8659dba8f..319cd4000 100644 --- a/labels.njk +++ b/labels.njk @@ -55,8 +55,9 @@ page_type: labels {% set icon_id = item.key | label_icon_id %} {% if icon_id %} {% set tint = item.key | label_icon_tint %} + {% set icon_sprite = item.key | label_icon_sprite %} {% endif %} {{ item.key }} diff --git a/static/module/card.js b/static/module/card.js index f23873578..38ef2ed29 100644 --- a/static/module/card.js +++ b/static/module/card.js @@ -260,7 +260,9 @@ export class Card { svg.setAttribute('class', `label-icon${tint ? ' label-icon--' + tint : ''}`) svg.setAttribute('aria-hidden', 'true') const use = document.createElementNS(svgNS, 'use') - use.setAttribute('href', `${this.staticBase}label-icons.svg#${iconId}`) + const secondary = window.__LABEL_ICON_SECONDARY__?.has(canonical) + const sprite = secondary ? 'label-icons-extra.svg' : 'label-icons.svg' + use.setAttribute('href', `${this.staticBase}${sprite}#${iconId}`) svg.appendChild(use) a.appendChild(svg) } diff --git a/static/package-search.js b/static/package-search.js index f1472efda..8bfccca40 100644 --- a/static/package-search.js +++ b/static/package-search.js @@ -37,6 +37,7 @@ const knownLabels = new Set( ) window.__LABEL_ICON_ALIASES__ = rawIndex.label_icon_aliases ?? {} window.__LABEL_ICON_TINTS__ = rawIndex.label_icon_tints ?? {} +window.__LABEL_ICON_SECONDARY__ = new Set(rawIndex.label_icon_secondary ?? []) const minisrch = createMinisearch(MiniSearch, packages) From ec1440af69d14a8049ec0303dea59f2e0a4de656 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Wed, 22 Jul 2026 14:57:58 +0200 Subject: [PATCH 4/4] Raise primary icon usage threshold Require an icon to be used by more than two packages before including it in the primary sprite. Homepage icons and icons used by packages updated within the last year remain preferred regardless of usage. This reduces the primary sprite from 107 icons and 41.0 KiB Brotli to 90 icons and 35.8 KiB. The fallback still does not appear until page 32 of Newest or page 46 of Recently Updated in the current dataset. --- eleventy.config.mjs | 2 +- eleventy.filters.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eleventy.config.mjs b/eleventy.config.mjs index 39d518646..f71f9584d 100644 --- a/eleventy.config.mjs +++ b/eleventy.config.mjs @@ -27,7 +27,7 @@ const FEATURED_LABELS = [ 'theme', ] const LABELS_RANK = new Map(FEATURED_LABELS.map((label, index) => [label, index])) -const LABEL_ICON_MINIMUM_USAGE = 2 +const LABEL_ICON_MINIMUM_USAGE = 3 const LABEL_ICON_RECENT_WINDOW_DAYS = 365 const MS_IN_DAY = 24 * 60 * 60 * 1000 diff --git a/eleventy.filters.mjs b/eleventy.filters.mjs index d56aeb7a1..7f9457e08 100644 --- a/eleventy.filters.mjs +++ b/eleventy.filters.mjs @@ -338,10 +338,10 @@ if (import.meta.vitest) { describe('label icon sprites', () => { it('keeps common and preferred icons in the primary sprite', () => { const sprites = configureLabelIcons([ - { key: 'python', count: 2 }, + { key: 'python', count: 3 }, { key: 'typst', count: 1 }, ], { - minimumUsage: 2, + minimumUsage: 3, preferredPackages: [{ labels: ['typst'] }], })