[{"data":1,"prerenderedAt":1876},["ShallowReactive",2],{"\u002Farticles\u002Fsubsetting-material-symbols-icon-font":3,"article-series:none":1818},{"id":4,"title":5,"body":6,"description":1802,"draft":1803,"extension":1804,"meta":1805,"navigation":316,"path":1806,"published":1807,"seo":1808,"seoDescription":1809,"series":1810,"seriesOrder":1810,"stem":1811,"tags":1812,"updated":1810,"__hash__":1817},"articles\u002Farticles\u002Fsubsetting-material-symbols-icon-font.md","Material Symbols Was 97% Dead Weight",{"type":7,"value":8,"toc":1776},"minimark",[9,47,55,58,61,66,69,72,75,109,112,116,122,125,144,147,151,154,158,163,174,178,197,201,212,215,219,223,226,351,372,375,379,386,400,1068,1071,1092,1095,1101,1108,1190,1204,1308,1315,1319,1330,1346,1397,1407,1414,1418,1421,1425,1428,1496,1506,1509,1513,1530,1533,1562,1565,1569,1572,1578,1582,1585,1605,1611,1615,1678,1685,1689,1692,1695,1698,1709,1712,1715,1718,1721,1724,1748,1751,1755,1772],[10,11,12,19],"blockquote",{},[13,14,15],"p",{},[16,17,18],"strong",{},"In short",[20,21,22,29,35,41],"ul",{},[23,24,25,28],"li",{},[16,26,27],{},"Problem:"," one icon font was 83% of all font bytes on our public marketing pages, slowing them during a wider SEO and web vitals audit.",[23,30,31,34],{},[16,32,33],{},"Decision:"," mitigate now by subsetting the font to the icons actually used, rather than rewrite every icon as SVG.",[23,36,37,40],{},[16,38,39],{},"Outcome:"," a 97% smaller font (625 KB to 15 KB), no sideways layout shift while it loads, and tests that keep it that way, delivered in a day.",[23,42,43,46],{},[16,44,45],{},"Trade-off:"," it is still an icon font. For a new project I would start with SVG icons.",[13,48,49,50,54],{},"Material Symbols is a convenient way to put icons in an app. You install one package, import one stylesheet, and write ",[51,52,53],"code",{},"check_circle"," wherever you want a tick. That convenience has a cost that is easy to miss: the font holds every icon in the set, about 4,300 of them, and you download all of them to draw the hundred or so you use.",[13,56,57],{},"This is the story of finding that cost in a production Nuxt app, and of fixing it inside the constraints of a working codebase: what I tried, what broke, the compromise I chose and why, and whether it is the right long-term answer.",[13,59,60],{},"The work ran in five steps: identify, understand, fix, prove, guard.",[62,63,65],"h2",{"id":64},"the-problem","The problem",[13,67,68],{},"This was one task in a wider SEO and web vitals audit of our public marketing pages, carried out over a few days. Alongside it ran a batch of other fixes: subsetting the brand text fonts, sitemap and crawl corrections, meta descriptions, heading levels and image sizing. Each was small; together they targeted the scores that decide how those pages rank and how fast they feel.",[13,70,71],{},"A web vitals scan of those pages showed one file making up 83% of all font bytes: the Material Symbols font, at about 625 KB. We use it across the site for most of our non-bespoke icons, so it loads everywhere, but the cost hurt most on the marketing pages, where load speed feeds directly into search rankings, bounce rates and conversions.",[13,73,74],{},"The size alone was bad. The way it loaded made it worse:",[20,76,77,87,93,103],{},[23,78,79,82,83,86],{},[16,80,81],{},"It was blocking."," The stylesheet used ",[51,84,85],{},"font-display: block",", so every icon on the page stayed blank until the whole 625 KB had arrived.",[23,88,89,92],{},[16,90,91],{},"It was discovered late."," The browser only learns about a font after it has downloaded and parsed the CSS that references it, so the biggest font on the page started downloading last.",[23,94,95,98,99,102],{},[16,96,97],{},"It shifted layout."," Before the font loads, a ligature icon is just its name in invisible text. ",[51,100,101],{},"lock_outline"," is 12 characters wide, about 140 pixels, so rows of icons jumped sideways when the 1em glyphs replaced them.",[23,104,105,108],{},[16,106,107],{},"It competed with the page's main content."," Largest Contentful Paint (LCP) measures how long the biggest element on screen, usually a hero image or heading, takes to appear. An icon is rarely that element, but 625 KB of font downloading at the same time takes bandwidth from the files that are, which matters most on slower mobile connections.",[13,110,111],{},"The app rendered 102 different icons. Roughly 97% of the download was never used.",[62,113,115],{"id":114},"why-it-happens-ligature-fonts","Why it happens: ligature fonts",[13,117,118,119,121],{},"Material Symbols is a ligature font. You do not reference an icon by a code point; you write its name as text, and the font has a ligature rule that turns the 12 characters of ",[51,120,53],{}," into a single icon glyph.",[13,123,124],{},"That design is why icon fonts are pleasant to use, and it is also why they fail in two quiet ways:",[126,127,128,134],"ol",{},[23,129,130,133],{},[16,131,132],{},"The whole set ships by default."," The font cannot know which names your app will type, so it contains all of them.",[23,135,136,139,140,143],{},[16,137,138],{},"A missing icon fails silently, as text."," If a name is not in the font, or is spelt wrong, the browser simply draws the letters. Ligatures are case-sensitive, so ",[51,141,142],{},"Asterisk"," renders the word \"Asterisk\" rather than the icon. Nothing errors; it just looks broken.",[13,145,146],{},"Keep that second point in mind, because shrinking the font makes it much easier to hit.",[62,148,150],{"id":149},"the-constraints","The constraints",[13,152,153],{},"This was not a greenfield choice. The app was live, most icons rendered through one shared component, and other people were shipping changes to the same code every day. Whatever I did had to fix the size problem without rewriting how icons are used, and without leaving a process only I understood.",[62,155,157],{"id":156},"options-considered","Options considered",[159,160,162],"h3",{"id":161},"switch-to-svg-icons","Switch to SVG icons",[13,164,165,166,169,170,173],{},"An SVG icon system (such as ",[51,167,168],{},"@nuxt\u002Ficon",") only ships what you use, and is the right choice for a new project. Here it would have meant touching about 220 call sites in the same change. Subsetting the font kept the existing ",[51,171,172],{},"\u003CAppIcon>"," component, and almost every caller, as they were. I come back to that trade-off at the end.",[159,175,177],{"id":176},"subset-locally-with-fonttools","Subset locally with fonttools",[13,179,180,181,184,185,188,189,192,193,196],{},"The usual way to subset a font is ",[51,182,183],{},"pyftsubset"," with a list of characters. For a ligature font that barely helps. Every icon name is made of ",[51,186,187],{},"a","–",[51,190,191],{},"z"," and ",[51,194,195],{},"_",", so keeping those characters keeps nearly every ligature, and the file hardly shrinks. Pruning ligatures properly means working at the glyph ID level with custom tooling.",[159,198,200],{"id":199},"let-google-subset-it","Let Google subset it",[13,202,203,204,207,208,211],{},"Google Fonts already solves this. The CSS API accepts an ",[51,205,206],{},"icon_names"," parameter and returns a font holding only those icons, with the variable axes you ask for. It prunes ligatures correctly, keeps the ",[51,209,210],{},"FILL"," axis for filled variants, and needs no Python. This is what I used.",[13,213,214],{},"The network is only needed when someone adds or removes an icon. The font is committed to the repository, so builds stay offline and deterministic.",[62,216,218],{"id":217},"the-mitigation","The mitigation",[159,220,222],{"id":221},"_1-a-manifest-of-icon-names","1. A manifest of icon names",[13,224,225],{},"One file lists every icon the app may use:",[227,228,233],"pre",{"className":229,"code":230,"language":231,"meta":232,"style":232},"language-ts shiki shiki-themes one-light one-dark-pro","\u002F\u002F icons\u002Fmanifest.ts\nexport const ICON_NAMES = [\n  'add',\n  'check_circle',\n  'expand_more',\n  \u002F\u002F ...\n] as const\n\nexport type IconName = (typeof ICON_NAMES)[number]\n","ts","",[51,234,235,244,266,276,284,292,298,311,318],{"__ignoreMap":232},[236,237,240],"span",{"class":238,"line":239},"line",1,[236,241,243],{"class":242},"sW2Sy","\u002F\u002F icons\u002Fmanifest.ts\n",[236,245,247,251,254,258,262],{"class":238,"line":246},2,[236,248,250],{"class":249},"sLKXg","export",[236,252,253],{"class":249}," const",[236,255,257],{"class":256},"sNmU0"," ICON_NAMES",[236,259,261],{"class":260},"s_Sar"," =",[236,263,265],{"class":264},"s5ixo"," [\n",[236,267,269,273],{"class":238,"line":268},3,[236,270,272],{"class":271},"sDhpE","  'add'",[236,274,275],{"class":264},",\n",[236,277,279,282],{"class":238,"line":278},4,[236,280,281],{"class":271},"  'check_circle'",[236,283,275],{"class":264},[236,285,287,290],{"class":238,"line":286},5,[236,288,289],{"class":271},"  'expand_more'",[236,291,275],{"class":264},[236,293,295],{"class":238,"line":294},6,[236,296,297],{"class":242},"  \u002F\u002F ...\n",[236,299,301,304,308],{"class":238,"line":300},7,[236,302,303],{"class":264},"] ",[236,305,307],{"class":306},"sblXP","as",[236,309,310],{"class":249}," const\n",[236,312,314],{"class":238,"line":313},8,[236,315,317],{"emptyLinePlaceholder":316},true,"\n",[236,319,321,323,326,330,332,335,339,341,344,348],{"class":238,"line":320},9,[236,322,250],{"class":249},[236,324,325],{"class":249}," type",[236,327,329],{"class":328},"sC09Y"," IconName",[236,331,261],{"class":260},[236,333,334],{"class":264}," (",[236,336,338],{"class":337},"s7DPa","typeof",[236,340,257],{"class":256},[236,342,343],{"class":264},")[",[236,345,347],{"class":346},"sN9Y4","number",[236,349,350],{"class":264},"]\n",[13,352,353,354,357,358,361,362,365,366,368,369,371],{},"The icon component's ",[51,355,356],{},"icon"," prop is typed ",[51,359,360],{},"IconName",", with no ",[51,363,364],{},"| string"," escape hatch. Before the subset, ",[51,367,364],{}," let new icons in without friction. After it, ",[51,370,364],{}," would let you choose icons the font does not contain. Now the editor autocompletes names and flags any that are missing.",[13,373,374],{},"Because the list is committed, every icon change appears as a one-line diff in review.",[159,376,378],{"id":377},"_2-a-script-to-regenerate-the-font","2. A script to regenerate the font",[13,380,381,382,385],{},"A small script, run as ",[51,383,384],{},"pnpm icons:build",", does four things:",[126,387,388,391,394,397],{},[23,389,390],{},"reads the manifest;",[23,392,393],{},"checks every name against Google's official codepoints list;",[23,395,396],{},"downloads the subset font from Google Fonts;",[23,398,399],{},"writes a JSON file with the names baked into the font and the font's sha256.",[227,401,403],{"className":229,"code":402,"language":231,"meta":232,"style":232},"const CODEPOINTS_URL =\n  'https:\u002F\u002Fraw.githubusercontent.com\u002Fgoogle\u002Fmaterial-design-icons\u002Fmaster\u002Fvariablefont\u002F' +\n  'MaterialSymbolsRounded%5BFILL,GRAD,opsz,wght%5D.codepoints'\n\n\u002F\u002F icon_names must be sorted, or Google rejects the request.\nconst names = [...ICON_NAMES].sort()\n\nconst codepoints = await (await fetchOk(CODEPOINTS_URL)).text()\nconst known = new Set(codepoints.split('\\n').map((line) => line.split(' ')[0]))\nconst unknown = names.filter((name) => !known.has(name))\nif (unknown.length) {\n  throw new Error(`Not Material Symbols icons (names are lowercase): ${unknown.join(', ')}`)\n}\n\nconst cssUrl =\n  'https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,700,0..1,0' +\n  `&icon_names=${names.join(',')}&display=block`\n\nconst css = await (await fetchOk(cssUrl)).text()\nconst fontUrl = css.match(\u002Fsrc:\\s*url\\((https:[^)]+)\\)\\s*format\\('woff2'\\)\u002F)?.[1]\nif (!fontUrl) throw new Error(`No woff2 url in Google Fonts response:\\n${css}`)\n\nconst font = Buffer.from(await (await fetchOk(fontUrl)).arrayBuffer())\nwriteFileSync(FONT_PATH, font)\nconst sha256 = createHash('sha256').update(font).digest('hex')\nwriteFileSync(NAMES_PATH, `${JSON.stringify({ names, sha256 }, null, 2)}\\n`)\n",[51,404,405,416,424,429,433,438,467,471,505,581,626,646,696,702,707,717,725,756,761,790,872,912,917,957,975,1014],{"__ignoreMap":232},[236,406,407,410,413],{"class":238,"line":239},[236,408,409],{"class":249},"const",[236,411,412],{"class":256}," CODEPOINTS_URL",[236,414,415],{"class":260}," =\n",[236,417,418,421],{"class":238,"line":246},[236,419,420],{"class":271},"  'https:\u002F\u002Fraw.githubusercontent.com\u002Fgoogle\u002Fmaterial-design-icons\u002Fmaster\u002Fvariablefont\u002F'",[236,422,423],{"class":260}," +\n",[236,425,426],{"class":238,"line":268},[236,427,428],{"class":271},"  'MaterialSymbolsRounded%5BFILL,GRAD,opsz,wght%5D.codepoints'\n",[236,430,431],{"class":238,"line":278},[236,432,317],{"emptyLinePlaceholder":316},[236,434,435],{"class":238,"line":286},[236,436,437],{"class":242},"\u002F\u002F icon_names must be sorted, or Google rejects the request.\n",[236,439,440,442,445,447,450,454,457,460,464],{"class":238,"line":294},[236,441,409],{"class":249},[236,443,444],{"class":256}," names",[236,446,261],{"class":260},[236,448,449],{"class":264}," [",[236,451,453],{"class":452},"st7oF","...",[236,455,456],{"class":256},"ICON_NAMES",[236,458,459],{"class":264},"].",[236,461,463],{"class":462},"sAdtL","sort",[236,465,466],{"class":264},"()\n",[236,468,469],{"class":238,"line":300},[236,470,317],{"emptyLinePlaceholder":316},[236,472,473,475,478,480,483,485,488,491,494,497,500,503],{"class":238,"line":313},[236,474,409],{"class":249},[236,476,477],{"class":256}," codepoints",[236,479,261],{"class":260},[236,481,482],{"class":249}," await",[236,484,334],{"class":264},[236,486,487],{"class":249},"await",[236,489,490],{"class":462}," fetchOk",[236,492,493],{"class":264},"(",[236,495,496],{"class":256},"CODEPOINTS_URL",[236,498,499],{"class":264},")).",[236,501,502],{"class":462},"text",[236,504,466],{"class":264},[236,506,507,509,512,514,517,520,522,526,529,532,534,537,540,542,545,548,551,554,557,560,563,565,567,569,572,574,578],{"class":238,"line":320},[236,508,409],{"class":249},[236,510,511],{"class":256}," known",[236,513,261],{"class":260},[236,515,516],{"class":249}," new",[236,518,519],{"class":462}," Set",[236,521,493],{"class":264},[236,523,525],{"class":524},"s7GmK","codepoints",[236,527,528],{"class":264},".",[236,530,531],{"class":462},"split",[236,533,493],{"class":264},[236,535,536],{"class":271},"'",[236,538,539],{"class":260},"\\n",[236,541,536],{"class":271},[236,543,544],{"class":264},").",[236,546,547],{"class":462},"map",[236,549,550],{"class":264},"((",[236,552,238],{"class":553},"s8iYz",[236,555,556],{"class":264},") ",[236,558,559],{"class":249},"=>",[236,561,562],{"class":524}," line",[236,564,528],{"class":264},[236,566,531],{"class":462},[236,568,493],{"class":264},[236,570,571],{"class":271},"' '",[236,573,343],{"class":264},[236,575,577],{"class":576},"sAGMh","0",[236,579,580],{"class":264},"]))\n",[236,582,584,586,589,591,593,595,598,600,603,605,607,610,613,615,618,620,623],{"class":238,"line":583},10,[236,585,409],{"class":249},[236,587,588],{"class":256}," unknown",[236,590,261],{"class":260},[236,592,444],{"class":524},[236,594,528],{"class":264},[236,596,597],{"class":462},"filter",[236,599,550],{"class":264},[236,601,602],{"class":553},"name",[236,604,556],{"class":264},[236,606,559],{"class":249},[236,608,609],{"class":260}," !",[236,611,612],{"class":524},"known",[236,614,528],{"class":264},[236,616,617],{"class":462},"has",[236,619,493],{"class":264},[236,621,602],{"class":622},"sz0mV",[236,624,625],{"class":264},"))\n",[236,627,629,632,634,637,639,643],{"class":238,"line":628},11,[236,630,631],{"class":249},"if",[236,633,334],{"class":264},[236,635,636],{"class":524},"unknown",[236,638,528],{"class":264},[236,640,642],{"class":641},"sJa8x","length",[236,644,645],{"class":264},") {\n",[236,647,649,652,654,657,659,662,666,668,671,674,676,679,682,684,687,690,693],{"class":238,"line":648},12,[236,650,651],{"class":249},"  throw",[236,653,516],{"class":249},[236,655,656],{"class":462}," Error",[236,658,493],{"class":264},[236,660,661],{"class":271},"`Not Material Symbols icons (names are lowercase): ",[236,663,665],{"class":664},"sAOjX","${",[236,667,636],{"class":524},[236,669,528],{"class":670},"sMj0N",[236,672,673],{"class":462},"join",[236,675,493],{"class":264},[236,677,536],{"class":678},"sWwuK",[236,680,681],{"class":271},", ",[236,683,536],{"class":678},[236,685,686],{"class":264},")",[236,688,689],{"class":664},"}",[236,691,692],{"class":271},"`",[236,694,695],{"class":264},")\n",[236,697,699],{"class":238,"line":698},13,[236,700,701],{"class":264},"}\n",[236,703,705],{"class":238,"line":704},14,[236,706,317],{"emptyLinePlaceholder":316},[236,708,710,712,715],{"class":238,"line":709},15,[236,711,409],{"class":249},[236,713,714],{"class":256}," cssUrl",[236,716,415],{"class":260},[236,718,720,723],{"class":238,"line":719},16,[236,721,722],{"class":271},"  'https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,700,0..1,0'",[236,724,423],{"class":260},[236,726,728,731,733,736,738,740,742,744,747,749,751,753],{"class":238,"line":727},17,[236,729,730],{"class":271},"  `&icon_names=",[236,732,665],{"class":664},[236,734,735],{"class":524},"names",[236,737,528],{"class":670},[236,739,673],{"class":462},[236,741,493],{"class":264},[236,743,536],{"class":678},[236,745,746],{"class":271},",",[236,748,536],{"class":678},[236,750,686],{"class":264},[236,752,689],{"class":664},[236,754,755],{"class":271},"&display=block`\n",[236,757,759],{"class":238,"line":758},18,[236,760,317],{"emptyLinePlaceholder":316},[236,762,764,766,769,771,773,775,777,779,781,784,786,788],{"class":238,"line":763},19,[236,765,409],{"class":249},[236,767,768],{"class":256}," css",[236,770,261],{"class":260},[236,772,482],{"class":249},[236,774,334],{"class":264},[236,776,487],{"class":249},[236,778,490],{"class":462},[236,780,493],{"class":264},[236,782,783],{"class":622},"cssUrl",[236,785,499],{"class":264},[236,787,502],{"class":462},[236,789,466],{"class":264},[236,791,793,795,798,800,802,804,807,809,813,817,821,824,827,830,833,836,839,842,844,847,849,851,854,856,859,861,864,867,870],{"class":238,"line":792},20,[236,794,409],{"class":249},[236,796,797],{"class":256}," fontUrl",[236,799,261],{"class":260},[236,801,768],{"class":524},[236,803,528],{"class":264},[236,805,806],{"class":462},"match",[236,808,493],{"class":264},[236,810,812],{"class":811},"sDaw7","\u002Fsrc:",[236,814,816],{"class":815},"sRZ4U","\\s",[236,818,820],{"class":819},"sYoRg","*",[236,822,823],{"class":811},"url",[236,825,826],{"class":260},"\\(",[236,828,829],{"class":811},"(https:",[236,831,832],{"class":576},"[",[236,834,835],{"class":452},"^",[236,837,838],{"class":576},")]",[236,840,841],{"class":819},"+",[236,843,686],{"class":811},[236,845,846],{"class":260},"\\)",[236,848,816],{"class":815},[236,850,820],{"class":819},[236,852,853],{"class":811},"format",[236,855,826],{"class":260},[236,857,858],{"class":811},"'woff2'",[236,860,846],{"class":260},[236,862,863],{"class":811},"\u002F",[236,865,866],{"class":264},")?.[",[236,868,869],{"class":576},"1",[236,871,350],{"class":264},[236,873,875,877,879,882,885,887,890,892,894,896,899,901,903,906,908,910],{"class":238,"line":874},21,[236,876,631],{"class":249},[236,878,334],{"class":264},[236,880,881],{"class":260},"!",[236,883,884],{"class":622},"fontUrl",[236,886,556],{"class":264},[236,888,889],{"class":249},"throw",[236,891,516],{"class":249},[236,893,656],{"class":462},[236,895,493],{"class":264},[236,897,898],{"class":271},"`No woff2 url in Google Fonts response:",[236,900,539],{"class":260},[236,902,665],{"class":664},[236,904,905],{"class":622},"css",[236,907,689],{"class":664},[236,909,692],{"class":271},[236,911,695],{"class":264},[236,913,915],{"class":238,"line":914},22,[236,916,317],{"emptyLinePlaceholder":316},[236,918,920,922,925,927,930,932,935,937,939,941,943,945,947,949,951,954],{"class":238,"line":919},23,[236,921,409],{"class":249},[236,923,924],{"class":256}," font",[236,926,261],{"class":260},[236,928,929],{"class":524}," Buffer",[236,931,528],{"class":264},[236,933,934],{"class":462},"from",[236,936,493],{"class":264},[236,938,487],{"class":249},[236,940,334],{"class":264},[236,942,487],{"class":249},[236,944,490],{"class":462},[236,946,493],{"class":264},[236,948,884],{"class":622},[236,950,499],{"class":264},[236,952,953],{"class":462},"arrayBuffer",[236,955,956],{"class":264},"())\n",[236,958,960,963,965,968,970,973],{"class":238,"line":959},24,[236,961,962],{"class":462},"writeFileSync",[236,964,493],{"class":264},[236,966,967],{"class":256},"FONT_PATH",[236,969,681],{"class":264},[236,971,972],{"class":622},"font",[236,974,695],{"class":264},[236,976,978,980,983,985,988,990,993,995,998,1000,1002,1004,1007,1009,1012],{"class":238,"line":977},25,[236,979,409],{"class":249},[236,981,982],{"class":256}," sha256",[236,984,261],{"class":260},[236,986,987],{"class":462}," createHash",[236,989,493],{"class":264},[236,991,992],{"class":271},"'sha256'",[236,994,544],{"class":264},[236,996,997],{"class":462},"update",[236,999,493],{"class":264},[236,1001,972],{"class":622},[236,1003,544],{"class":264},[236,1005,1006],{"class":462},"digest",[236,1008,493],{"class":264},[236,1010,1011],{"class":271},"'hex'",[236,1013,695],{"class":264},[236,1015,1017,1019,1021,1024,1026,1028,1030,1033,1035,1038,1041,1043,1045,1047,1050,1053,1055,1058,1060,1062,1064,1066],{"class":238,"line":1016},26,[236,1018,962],{"class":462},[236,1020,493],{"class":264},[236,1022,1023],{"class":256},"NAMES_PATH",[236,1025,681],{"class":264},[236,1027,692],{"class":271},[236,1029,665],{"class":664},[236,1031,1032],{"class":256},"JSON",[236,1034,528],{"class":670},[236,1036,1037],{"class":462},"stringify",[236,1039,1040],{"class":264},"({",[236,1042,444],{"class":622},[236,1044,746],{"class":264},[236,1046,982],{"class":622},[236,1048,1049],{"class":264}," },",[236,1051,1052],{"class":576}," null",[236,1054,746],{"class":264},[236,1056,1057],{"class":576}," 2",[236,1059,686],{"class":264},[236,1061,689],{"class":664},[236,1063,539],{"class":260},[236,1065,692],{"class":271},[236,1067,695],{"class":264},[13,1069,1070],{},"Two details here were learnt the hard way:",[20,1072,1073,1079],{},[23,1074,1075,1078],{},[16,1076,1077],{},"Google's subset API quietly accepts names that are not icons."," A made-up name still returns a perfectly good font, and the icon then renders as text. The codepoints check catches typos and capitalised names before the download.",[23,1080,1081,1084,1085,192,1088,1091],{},[16,1082,1083],{},"The npm package's name list is not complete."," It leaves out aliases such as ",[51,1086,1087],{},"expand_more",[51,1089,1090],{},"auto_awesome",". Validating against it dropped eight icons the app really used. The codepoints file in Google's repository includes aliases.",[13,1093,1094],{},"Google picks the font format from the user agent, so the script sends a current Chrome user agent to get woff2.",[159,1096,1098,1099],{"id":1097},"_3-load-it-early-and-keep-font-display-block","3. Load it early, and keep ",[51,1100,85],{},[13,1102,1103,1104,1107],{},"The package's stylesheet was replaced with a local ",[51,1105,1106],{},"@font-face",":",[227,1109,1112],{"className":1110,"code":1111,"language":905,"meta":232,"style":232},"language-css shiki shiki-themes one-light one-dark-pro","@font-face {\n  font-family: 'Material Symbols Rounded';\n  font-style: normal;\n  font-weight: 700;\n  font-display: block;\n  src: url('.\u002Ffonts\u002Ficons.woff2') format('woff2');\n}\n",[51,1113,1114,1121,1132,1143,1153,1163,1186],{"__ignoreMap":232},[236,1115,1116,1118],{"class":238,"line":239},[236,1117,1106],{"class":249},[236,1119,1120],{"class":264}," {\n",[236,1122,1123,1126,1129],{"class":238,"line":246},[236,1124,1125],{"class":264},"  font-family: ",[236,1127,1128],{"class":271},"'Material Symbols Rounded'",[236,1130,1131],{"class":264},";\n",[236,1133,1134,1137,1141],{"class":238,"line":268},[236,1135,1136],{"class":264},"  font-style: ",[236,1138,1140],{"class":1139},"sYebD","normal",[236,1142,1131],{"class":264},[236,1144,1145,1148,1151],{"class":238,"line":278},[236,1146,1147],{"class":264},"  font-weight: ",[236,1149,1150],{"class":576},"700",[236,1152,1131],{"class":264},[236,1154,1155,1158,1161],{"class":238,"line":286},[236,1156,1157],{"class":264},"  font-display: ",[236,1159,1160],{"class":1139},"block",[236,1162,1131],{"class":264},[236,1164,1165,1168,1170,1172,1175,1177,1179,1181,1183],{"class":238,"line":294},[236,1166,1167],{"class":264},"  src: ",[236,1169,823],{"class":260},[236,1171,493],{"class":264},[236,1173,1174],{"class":271},"'.\u002Ffonts\u002Ficons.woff2'",[236,1176,556],{"class":264},[236,1178,853],{"class":260},[236,1180,493],{"class":264},[236,1182,858],{"class":271},[236,1184,1185],{"class":264},");\n",[236,1187,1188],{"class":238,"line":300},[236,1189,701],{"class":264},[13,1191,1192,1193,1196,1197,1200,1201,1203],{},"It is tempting to switch to ",[51,1194,1195],{},"font-display: swap",", but for an icon font that flashes ",[51,1198,1199],{},"keyboard_arrow_down"," as text before each icon appears, which is worse than a brief blank. The problem was never ",[51,1202,1160],{},"; it was size and timing. The subset fixes the size, and a preload fixes the timing:",[227,1205,1207],{"className":229,"code":1206,"language":231,"meta":232,"style":232},"import iconFont from '~\u002Ffonts\u002Ficons.woff2?url'\n\nuseHead({\n  link: [\n    { rel: 'preload', as: 'font', type: 'font\u002Fwoff2', href: iconFont, crossorigin: '' },\n  ],\n})\n",[51,1208,1209,1223,1227,1235,1244,1298,1303],{"__ignoreMap":232},[236,1210,1211,1214,1217,1220],{"class":238,"line":239},[236,1212,1213],{"class":249},"import",[236,1215,1216],{"class":641}," iconFont",[236,1218,1219],{"class":249}," from",[236,1221,1222],{"class":271}," '~\u002Ffonts\u002Ficons.woff2?url'\n",[236,1224,1225],{"class":238,"line":246},[236,1226,317],{"emptyLinePlaceholder":316},[236,1228,1229,1232],{"class":238,"line":268},[236,1230,1231],{"class":462},"useHead",[236,1233,1234],{"class":264},"({\n",[236,1236,1237,1240,1242],{"class":238,"line":278},[236,1238,1239],{"class":641},"  link",[236,1241,1107],{"class":452},[236,1243,265],{"class":264},[236,1245,1246,1249,1252,1254,1257,1259,1261,1263,1266,1268,1271,1273,1276,1278,1281,1283,1285,1287,1290,1292,1295],{"class":238,"line":286},[236,1247,1248],{"class":264},"    { ",[236,1250,1251],{"class":641},"rel",[236,1253,1107],{"class":452},[236,1255,1256],{"class":271}," 'preload'",[236,1258,681],{"class":264},[236,1260,307],{"class":641},[236,1262,1107],{"class":452},[236,1264,1265],{"class":271}," 'font'",[236,1267,681],{"class":264},[236,1269,1270],{"class":641},"type",[236,1272,1107],{"class":452},[236,1274,1275],{"class":271}," 'font\u002Fwoff2'",[236,1277,681],{"class":264},[236,1279,1280],{"class":641},"href",[236,1282,1107],{"class":452},[236,1284,1216],{"class":622},[236,1286,681],{"class":264},[236,1288,1289],{"class":641},"crossorigin",[236,1291,1107],{"class":452},[236,1293,1294],{"class":271}," ''",[236,1296,1297],{"class":264}," },\n",[236,1299,1300],{"class":238,"line":294},[236,1301,1302],{"class":264},"  ],\n",[236,1304,1305],{"class":238,"line":300},[236,1306,1307],{"class":264},"})\n",[13,1309,1310,1311,1314],{},"Preloading is not always right for fonts. Preload a font a page might not use and you waste bandwidth on it. Here every page draws icons and the file is 15 KB, so fetching it immediately is the right trade. Importing it with ",[51,1312,1313],{},"?url"," means the preload and the CSS point at the same hashed file after the build.",[159,1316,1318],{"id":1317},"_4-stop-the-sideways-shift","4. Stop the sideways shift",[13,1320,1321,1322,1325,1326,1329],{},"Before the font arrives, the invisible ligature name is many ems wide. The first attempt, ",[51,1323,1324],{},"width: 1em",", fixed the shift and broke something else: icons centred by stretching to their parent with ",[51,1327,1328],{},"text-align: center"," moved to the left.",[13,1331,1332,1333,1337,1338,1341,1342,1345],{},"Size containment gives the icon a ",[1334,1335,1336],"em",{},"natural"," width of 1em while ",[51,1339,1340],{},"width"," stays ",[51,1343,1344],{},"auto",", so stretched icons still stretch:",[227,1347,1349],{"className":1110,"code":1348,"language":905,"meta":232,"style":232},"@supports (contain-intrinsic-inline-size: 1em) {\n  .icon {\n    contain: inline-size;\n    contain-intrinsic-inline-size: 1em;\n  }\n}\n",[51,1350,1351,1365,1372,1377,1388,1393],{"__ignoreMap":232},[236,1352,1353,1356,1359,1361,1363],{"class":238,"line":239},[236,1354,1355],{"class":249},"@supports",[236,1357,1358],{"class":264}," (contain-intrinsic-inline-size: ",[236,1360,869],{"class":576},[236,1362,1334],{"class":815},[236,1364,645],{"class":264},[236,1366,1367,1370],{"class":238,"line":246},[236,1368,1369],{"class":576},"  .icon",[236,1371,1120],{"class":264},[236,1373,1374],{"class":238,"line":268},[236,1375,1376],{"class":264},"    contain: inline-size;\n",[236,1378,1379,1382,1384,1386],{"class":238,"line":278},[236,1380,1381],{"class":264},"    contain-intrinsic-inline-size: ",[236,1383,869],{"class":576},[236,1385,1334],{"class":815},[236,1387,1131],{"class":264},[236,1389,1390],{"class":238,"line":286},[236,1391,1392],{"class":264},"  }\n",[236,1394,1395],{"class":238,"line":294},[236,1396,701],{"class":264},[13,1398,1399,1400,1402,1403,1406],{},"The ",[51,1401,1355],{}," wrapper matters: in older Safari (before 17), ",[51,1404,1405],{},"contain: inline-size"," without the intrinsic size collapses icons to zero width, so those browsers keep the old behaviour instead.",[13,1408,1409,1410,1413],{},"One more trap: do not add ",[51,1411,1412],{},"overflow: hidden"," to hide the overflowing name. On an inline-block it moves the baseline, so inline icons sit lower than the text beside them.",[62,1415,1417],{"id":1416},"keeping-it-current","Keeping it current",[13,1419,1420],{},"A subset font is only fast while the manifest matches the code. People will forget, and so will AI coding agents, so the rules live in the codebase rather than in anyone's memory.",[159,1422,1424],{"id":1423},"an-architecture-test","An architecture test",[13,1426,1427],{},"A test in the normal unit suite scans the source for icon names and fails when:",[1429,1430,1431,1444],"table",{},[1432,1433,1434],"thead",{},[1435,1436,1437,1441],"tr",{},[1438,1439,1440],"th",{},"Check",[1438,1442,1443],{},"Catches",[1445,1446,1447,1456,1464,1472,1480,1488],"tbody",{},[1435,1448,1449,1453],{},[1450,1451,1452],"td",{},"Every icon used is in the manifest",[1450,1454,1455],{},"A new icon added without regenerating, which would render as text",[1435,1457,1458,1461],{},[1450,1459,1460],{},"Every manifest icon is still used",[1450,1462,1463],{},"Dead icons bloating the font",[1435,1465,1466,1469],{},[1450,1467,1468],{},"Manifest names match the font JSON",[1450,1470,1471],{},"The list edited but the script not run",[1435,1473,1474,1477],{},[1450,1475,1476],{},"The font's sha256 matches the JSON",[1450,1478,1479],{},"A merge that kept the other branch's binary font while the text files merged",[1435,1481,1482,1485],{},[1450,1483,1484],{},"No raw icon font class in templates",[1450,1486,1487],{},"Icons that bypass the component and the scan",[1435,1489,1490,1493],{},[1450,1491,1492],{},"The scanner finds known samples",[1450,1494,1495],{},"A regex change that quietly stops finding names",[13,1497,1498,1499,1502,1503,1505],{},"Each failure message names the exact fix, such as \"add ",[51,1500,1501],{},"x"," to manifest.ts and run ",[51,1504,384],{},"\".",[13,1507,1508],{},"The sha256 check deserves a mention. Git merges text files line by line but takes one side of a binary file whole, so two branches that each add an icon can merge into a manifest with both names and a font with only one. The hash catches it.",[159,1510,1512],{"id":1511},"scan-only-where-icons-can-appear","Scan only where icons can appear",[13,1514,1515,1516,681,1519,681,1522,1525,1526,1529],{},"The first version of the scan matched every quoted string in the codebase and found about 35 false positives: ",[51,1517,1518],{},"'radio'",[51,1520,1521],{},"'label'",[51,1523,1524],{},"'pending'"," and so on are all valid icon names. An allow-list would have created blind spots, since a later ",[51,1527,1528],{},"icon=\"label\""," would be ignored too.",[13,1531,1532],{},"Instead the scan only looks where an icon name can be:",[20,1534,1535,1544,1551,1557],{},[23,1536,1537,1540,1541,1543],{},[51,1538,1539],{},"icon=\"…\""," and other attributes ending in ",[51,1542,356],{},";",[23,1545,1546,1547,1550],{},"the values of bound ",[51,1548,1549],{},":icon=\"…\""," expressions;",[23,1552,1553,1556],{},[51,1554,1555],{},"icon:"," keys in objects;",[23,1558,1559,1560,528],{},"return values of functions typed ",[51,1561,360],{},[13,1563,1564],{},"The few literals in those positions that are not Material icons are listed per file, so each exception covers one name in one place.",[159,1566,1568],{"id":1567},"guidance-where-people-and-agents-look","Guidance where people and agents look",[13,1570,1571],{},"The project's agent instructions file has one line on rendering icons through the component and running the script. A short doc explains how ligature icons fail, how to add and remove them, where the scan looks and why the CSS is the way it is. The manifest's header comment repeats the three steps at the point people edit.",[13,1573,1574,1575,1577],{},"Adding an icon is now: add the name, run ",[51,1576,384],{},", commit the manifest, font and JSON.",[62,1579,1581],{"id":1580},"proving-it-worked","Proving it worked",[13,1583,1584],{},"Before calling it done, I checked each claim rather than trusting the numbers:",[20,1586,1587,1593,1599],{},[23,1588,1589,1592],{},[16,1590,1591],{},"The build:"," one 15.7 KB icon font in the output, with the preload and the CSS pointing at the same hashed file.",[23,1594,1595,1598],{},[16,1596,1597],{},"Layout:"," measured in Chromium and WebKit, and checked by hand in Firefox. Once loaded, every layout matched the old one; before load, the sideways shift was gone.",[23,1600,1601,1604],{},[16,1602,1603],{},"The guard:"," I broke it on purpose. An unlisted icon, a stale manifest and a swapped font file each made the test fail with the right message.",[13,1606,1607,1608,1610],{},"None of that replaced looking. With AI tools writing more of the code and more of the tests, it is easy to see a green suite and move on. I still loaded the pages on a throttled connection and checked the centred and filled icons by eye, because layout problems like the off-centre icons ",[51,1609,1324],{}," caused are exactly what a test suite misses unless someone already knows to look for them.",[62,1612,1614],{"id":1613},"the-results","The results",[1429,1616,1617,1629],{},[1432,1618,1619],{},[1435,1620,1621,1623,1626],{},[1438,1622],{},[1438,1624,1625],{},"Before",[1438,1627,1628],{},"After",[1445,1630,1631,1645,1656,1667],{},[1435,1632,1633,1636,1639],{},[1450,1634,1635],{},"Icon font",[1450,1637,1638],{},"~625 KB, ~4,300 icons",[1450,1640,1641,1642,1644],{},"15 KB, 102 icons, with the ",[51,1643,210],{}," axis",[1435,1646,1647,1650,1653],{},[1450,1648,1649],{},"Discovery",[1450,1651,1652],{},"after the CSS is parsed",[1450,1654,1655],{},"preloaded with the document",[1435,1657,1658,1661,1664],{},[1450,1659,1660],{},"Layout before load",[1450,1662,1663],{},"box as wide as the hidden name, row shifts on load",[1450,1665,1666],{},"1em natural width, no sideways shift",[1435,1668,1669,1672,1675],{},[1450,1670,1671],{},"Missing or misspelt icon",[1450,1673,1674],{},"renders as text, found by eye",[1450,1676,1677],{},"fails a test or the type check",[13,1679,1680,1681,1684],{},"The work also caught a real bug: an ",[51,1682,1683],{},"icon=\"Asterisk\""," that had been rendering the word \"Asterisk\" beside a required field, because ligatures are case-sensitive. It now draws an asterisk.",[62,1686,1688],{"id":1687},"is-an-icon-font-still-the-right-choice","Is an icon font still the right choice?",[13,1690,1691],{},"Honestly, it depends where you are starting from.",[13,1693,1694],{},"Icon fonts earned their place. One package gives a team a large, consistent icon set, everyone can use it straight away, and adding an icon is as easy as typing its name. For a team moving quickly, that is a reasonable trade, and it is why so many projects still use them.",[13,1696,1697],{},"If I were starting a project today, though, I would be cautious about reaching for one. Most of this article exists because the icons were a font: the blocking load, the hidden text taking up space, the preload, the containment CSS, and icons that fail silently as words. SVG icons avoid all of that by design. They are drawn inline, there is nothing to wait for, a missing icon never turns into a stray word, and they are easier to make accessible, since an SVG can carry its own title and label.",[13,1699,1700,1701,1708],{},"The tooling has also caught up. In Nuxt, ",[187,1702,1706],{"href":1703,"rel":1704},"https:\u002F\u002Fnuxt.com\u002Fmodules\u002Ficon\u002Fbundles",[1705],"nofollow",[51,1707,168],{}," can scan your source and bundle only the icons you use, which is much the same safety net this article builds by hand. Material Symbols is available as an SVG set through Iconify, so you do not even have to change the look.",[13,1710,1711],{},"But a working app is not a new project. Moving this one to SVG would have meant changing around 220 call sites to go from 15 KB to slightly less, and the subset got almost all of the benefit in one pull request with only a handful of small caller changes.",[13,1713,1714],{},"Time mattered too. The subset delivered results within a day. The proper fix would have meant re-auditing every call site, likely taking a week, and any icon rendered somewhere the audit missed, such as a name built at runtime or passed through data, could have reached production broken. The subset's scan and tests surfaced those hidden uses as part of the work, rather than leaving them to be found by users.",[13,1716,1717],{},"That made subsetting the right first step for a codebase that already depends on a font: fix the real problem now, decide the bigger change separately.",[13,1719,1720],{},"There is a twist here. Almost all of the upkeep this process needs, keeping the manifest current, running the script and acting on test failures, is the kind of work AI coding agents now do reliably, especially when every failure message names its own fix. That makes the mitigation cheap to live with. But the same is true of the migration: changing every call site to an SVG component is exactly the repetitive, test-checked change agents handle well. AI-assisted development lowers the cost of maintaining a compromise and the cost of leaving it, so the question becomes less \"can we afford to migrate?\" and more \"is this still worth its upkeep?\"",[13,1722,1723],{},"So my advice is:",[20,1725,1726,1732,1738],{},[23,1727,1728,1731],{},[16,1729,1730],{},"Starting fresh:"," prefer SVG icons, bundled at build time.",[23,1733,1734,1737],{},[16,1735,1736],{},"Already using an icon font:"," subset it now. It is the biggest win for the least change, and it mitigates most of the problems above.",[23,1739,1740,1743,1744,1747],{},[16,1741,1742],{},"Either way:"," hide decorative icons from screen readers (",[51,1745,1746],{},"aria-hidden=\"true\"",") and give meaningful ones a label. A ligature icon that is not hidden can be read aloud by its name.",[13,1749,1750],{},"Subsetting is excellent mitigation. It is not a cure, and it is not always the best starting point.",[62,1752,1754],{"id":1753},"takeaways","Takeaways",[20,1756,1757,1760,1763,1766,1769],{},[23,1758,1759],{},"Identify, understand, fix, prove, guard. A fix you have not proven is a guess, and one you have not guarded will drift.",[23,1761,1762],{},"Ligature fonts cannot be subset by characters. Subset by icon name, and let Google do it if you can.",[23,1764,1765],{},"Validate names yourself. Both the subset API and the browser fail silently.",[23,1767,1768],{},"Put the rules in a test, not a wiki page. A subset that drifts quietly turns icons back into words.",[23,1770,1771],{},"In an existing codebase, fix the real problem within its constraints first, and make the bigger change a separate decision.",[1773,1774,1775],"style",{},"html pre.shiki code .sLKXg, html code.shiki .sLKXg{--shiki-default:#A626A4;--shiki-dark:#C678DD}html pre.shiki code .s5ixo, html code.shiki .s5ixo{--shiki-default:#383A42;--shiki-dark:#ABB2BF}html pre.shiki code .sDhpE, html code.shiki .sDhpE{--shiki-default:#50A14F;--shiki-dark:#98C379}html pre.shiki code .sYebD, html code.shiki .sYebD{--shiki-default:#383A42;--shiki-dark:#D19A66}html pre.shiki code .sAGMh, html code.shiki .sAGMh{--shiki-default:#986801;--shiki-dark:#D19A66}html pre.shiki code .s_Sar, html code.shiki .s_Sar{--shiki-default:#0184BC;--shiki-dark:#56B6C2}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sRZ4U, html code.shiki .sRZ4U{--shiki-default:#986801;--shiki-dark:#E06C75}html pre.shiki code .sW2Sy, html code.shiki .sW2Sy{--shiki-default:#A0A1A7;--shiki-default-font-style:italic;--shiki-dark:#7F848E;--shiki-dark-font-style:italic}html pre.shiki code .sNmU0, html code.shiki .sNmU0{--shiki-default:#986801;--shiki-dark:#E5C07B}html pre.shiki code .sblXP, html code.shiki .sblXP{--shiki-default:#383A42;--shiki-dark:#C678DD}html pre.shiki code .sC09Y, html code.shiki .sC09Y{--shiki-default:#C18401;--shiki-dark:#E5C07B}html pre.shiki code .s7DPa, html code.shiki .s7DPa{--shiki-default:#0184BC;--shiki-dark:#C678DD}html pre.shiki code .sN9Y4, html code.shiki .sN9Y4{--shiki-default:#0184BC;--shiki-dark:#E5C07B}html pre.shiki code .st7oF, html code.shiki .st7oF{--shiki-default:#0184BC;--shiki-dark:#ABB2BF}html pre.shiki code .sAdtL, html code.shiki .sAdtL{--shiki-default:#4078F2;--shiki-dark:#61AFEF}html pre.shiki code .s7GmK, html code.shiki .s7GmK{--shiki-default:#383A42;--shiki-dark:#E5C07B}html pre.shiki code .s8iYz, html code.shiki .s8iYz{--shiki-default:#383A42;--shiki-default-font-style:inherit;--shiki-dark:#E06C75;--shiki-dark-font-style:italic}html pre.shiki code .sz0mV, html code.shiki .sz0mV{--shiki-default:#383A42;--shiki-dark:#E06C75}html pre.shiki code .sJa8x, html code.shiki .sJa8x{--shiki-default:#E45649;--shiki-dark:#E06C75}html pre.shiki code .sAOjX, html code.shiki .sAOjX{--shiki-default:#CA1243;--shiki-dark:#C678DD}html pre.shiki code .sMj0N, html code.shiki .sMj0N{--shiki-default:#50A14F;--shiki-dark:#ABB2BF}html pre.shiki code .sWwuK, html code.shiki .sWwuK{--shiki-default:#CA1243;--shiki-dark:#98C379}html pre.shiki code .sDaw7, html code.shiki .sDaw7{--shiki-default:#0184BC;--shiki-dark:#E06C75}html pre.shiki code .sYoRg, html code.shiki .sYoRg{--shiki-default:#0184BC;--shiki-dark:#D19A66}",{"title":232,"searchDepth":246,"depth":246,"links":1777},[1778,1779,1780,1781,1786,1793,1798,1799,1800,1801],{"id":64,"depth":246,"text":65},{"id":114,"depth":246,"text":115},{"id":149,"depth":246,"text":150},{"id":156,"depth":246,"text":157,"children":1782},[1783,1784,1785],{"id":161,"depth":268,"text":162},{"id":176,"depth":268,"text":177},{"id":199,"depth":268,"text":200},{"id":217,"depth":246,"text":218,"children":1787},[1788,1789,1790,1792],{"id":221,"depth":268,"text":222},{"id":377,"depth":268,"text":378},{"id":1097,"depth":268,"text":1791},"3. Load it early, and keep font-display: block",{"id":1317,"depth":268,"text":1318},{"id":1416,"depth":246,"text":1417,"children":1794},[1795,1796,1797],{"id":1423,"depth":268,"text":1424},{"id":1511,"depth":268,"text":1512},{"id":1567,"depth":268,"text":1568},{"id":1580,"depth":246,"text":1581},{"id":1613,"depth":246,"text":1614},{"id":1687,"depth":246,"text":1688},{"id":1753,"depth":246,"text":1754},"A case study in fixing a real performance problem in an existing codebase: cutting an icon font by 97%, the compromises involved, and whether an icon font is still the right choice.",false,"md",{},"\u002Farticles\u002Fsubsetting-material-symbols-icon-font","2026-11-06",{"title":5,"description":1802},"How I cut an icon font by 97% in a live codebase, the compromises involved, and when SVG icons are the better choice.",null,"articles\u002Fsubsetting-material-symbols-icon-font",[1813,1814,1815,905,1816],"performance","fonts","web-vitals","nuxt","IXCSV05iJKfCvVro_DWnGuqvV5HuVsIN54Lr9XDIpR4",[1819,1825,1830,1835,1840,1845,1850,1855,1859,1863,1864,1868,1872],{"path":1820,"title":1821,"published":1822,"series":1823,"seriesOrder":1824},"\u002Farticles\u002Fembrace-the-framework-custom-collections-for-subscriptions","Embrace the Framework: Put Subscription Business Rules in a Custom Collection","2026-07-28","embrace-the-framework","3",{"path":1826,"title":1827,"published":1828,"series":1823,"seriesOrder":1829},"\u002Farticles\u002Fembrace-the-framework-customer-eligibility-with-custom-collections","Embrace the Framework: Make Eligibility Rules Read Like Business Policy","2026-08-04","6",{"path":1831,"title":1832,"published":1833,"series":1823,"seriesOrder":1834},"\u002Farticles\u002Fembrace-the-framework-operational-reports-with-collections","Embrace the Framework: Build Clear Operational Reports With Collections","2026-08-02","5",{"path":1836,"title":1837,"published":1838,"series":1823,"seriesOrder":1839},"\u002Farticles\u002Fembrace-the-framework-order-fulfilment-with-collections","Embrace the Framework: Make Order Fulfilment Clearer With Collections","2026-07-25","2",{"path":1841,"title":1842,"published":1843,"series":1823,"seriesOrder":1844},"\u002Farticles\u002Fembrace-the-framework-order-processing-pipeline-that-can-grow","Embrace the Framework: Build an Order Processing Pipeline That Can Grow","2026-08-06","7",{"path":1846,"title":1847,"published":1848,"series":1823,"seriesOrder":1849},"\u002Farticles\u002Fembrace-the-framework-order-processing-with-pipelines","Embrace the Framework: Break Order Processing Into Steps With Pipelines","2026-07-31","4",{"path":1851,"title":1852,"published":1853,"series":1823,"seriesOrder":1854},"\u002Farticles\u002Fembrace-the-framework-supplier-imports-with-collections-and-pipelines","Embrace the Framework: Combine Collections and Pipelines for Reliable Supplier Imports","2026-08-08","8",{"path":1856,"title":1857,"published":1858,"series":1823,"seriesOrder":869},"\u002Farticles\u002Fembrace-the-framework-why-working-with-laravel-beats-working-around-it","Embrace the Framework: Why Working With Laravel Beats Working Around It","2026-07-23",{"path":1860,"title":1861,"published":1862,"series":1810,"seriesOrder":1810},"\u002Farticles\u002Fsimple-search-helper","A Simple Laravel Search Helper","2023-08-06",{"path":1806,"title":5,"published":1807,"series":1810,"seriesOrder":1810},{"path":1865,"title":1866,"published":1867,"series":1810,"seriesOrder":1810},"\u002Farticles\u002Fsvg-tidy-local-svg-optimiser","SVG Tidy: Clean and Optimise SVGs Locally","2026-07-21",{"path":1869,"title":1870,"published":1871,"series":1810,"seriesOrder":1810},"\u002Farticles\u002Ftools-i-use","Tools I Use","2024-05-26",{"path":1873,"title":1874,"published":1875,"series":1810,"seriesOrder":1810},"\u002Farticles\u002Fwelcome","Welcome","2023-08-05",1793949650549]