[{"data":1,"prerenderedAt":1830},["ShallowReactive",2],{"project:\u002Fao-dev:en-us":3,"project:prev:\u002Fao-dev:en-us":1826,"project:next:\u002Fao-dev:en-us":1829},{"id":4,"title":5,"body":6,"category":1794,"company":1797,"cover":1798,"date":1799,"description":1800,"extension":1801,"featured":402,"image":1802,"isPublic":402,"links":1803,"meta":1806,"navigation":402,"order":1807,"path":1808,"period":1809,"role":1810,"seo":1813,"stack":1814,"status":1818,"stem":1819,"tags":1820,"__hash__":1825},"projects_enUS\u002Fen-us\u002Fprojects\u002Fao-dev.md","ao.dev",{"type":7,"value":8,"toc":1764},"minimark",[9,22,33,38,208,215,219,230,314,335,340,367,451,458,462,469,472,507,511,533,537,551,555,561,565,574,682,713,719,727,738,766,776,780,791,822,870,873,926,930,933,937,966,1009,1013,1042,1056,1072,1076,1093,1155,1170,1174,1181,1227,1233,1253,1380,1434,1438,1447,1528,1545,1552,1575,1582,1592,1596,1599,1649,1656,1659,1663,1673,1691,1695,1705,1709,1712,1754,1760],[10,11,12,13,16,17,21],"p",{},"This is the only case study in this portfolio that describes the portfolio itself. ",[14,15,5],"code",{}," was born with a dual intent: be my professional showcase and, at the same time, serve as a ",[18,19,20],"strong",{},"reference boilerplate"," for the Nuxt 4 ecosystem — a real project, shipped and never abandoned halfway, that anyone can read end to end and learn something genuine about SSG, i18n, swappable design systems, and performance auditing taken seriously.",[10,23,24,25,32],{},"Every visual component here comes from ",[26,27,29],"a",{"href":28},"\u002Fprojects\u002Ffui-content",[14,30,31],{},"fui-content",", the theming library I built in parallel. Every case study you've already read on this site — Leve Mix, Grupo Clamed, the Grupo 3C modules — was written, reviewed, and published through the exact same Nuxt Content this project uses. It's the project that closes the loop: the tool and the showcase for the tool are the same thing.",[34,35,37],"h2",{"id":36},"under-the-hood","Under the hood",[39,40,41,54],"table",{},[42,43,44],"thead",{},[45,46,47,51],"tr",{},[48,49,50],"th",{},"Layer",[48,52,53],{},"Technology",[55,56,57,81,93,106,130,143,155,167,179,192],"tbody",{},[45,58,59,63],{},[60,61,62],"td",{},"Framework",[60,64,65,72,73,76,77,80],{},[26,66,71],{"href":67,"target":68,"rel":69},"https:\u002F\u002Fnuxt.com\u002F","_blank",[70],"nofollow","Nuxt 4"," (",[14,74,75],{},"compatibilityVersion: 4",", ",[14,78,79],{},"app\u002F"," directory)",[45,82,83,86],{},[60,84,85],{},"Language",[60,87,88],{},[26,89,92],{"href":90,"target":68,"rel":91},"https:\u002F\u002Fwww.typescriptlang.org\u002F",[70],"TypeScript",[45,94,95,98],{},[60,96,97],{},"Content",[60,99,100,105],{},[26,101,104],{"href":102,"target":68,"rel":103},"https:\u002F\u002Fcontent.nuxt.com\u002F",[70],"@nuxt\u002Fcontent"," — Markdown with Zod-typed frontmatter",[45,107,108,111],{},[60,109,110],{},"i18n",[60,112,113,118,119,122,123,76,126,129],{},[26,114,117],{"href":115,"target":68,"rel":116},"https:\u002F\u002Fi18n.nuxtjs.org\u002F",[70],"@nuxtjs\u002Fi18n"," — ",[14,120,121],{},"pt-br"," (default) and ",[14,124,125],{},"en-us",[14,127,128],{},"prefix_except_default"," strategy",[45,131,132,135],{},[60,133,134],{},"SEO",[60,136,137,142],{},[26,138,141],{"href":139,"target":68,"rel":140},"https:\u002F\u002Fnuxtseo.com\u002F",[70],"@nuxtjs\u002Fseo"," — schema.org, OG image, sitemap, robots",[45,144,145,148],{},[60,146,147],{},"PWA",[60,149,150],{},[26,151,154],{"href":152,"target":68,"rel":153},"https:\u002F\u002Fvite-pwa-org.netlify.app\u002Fframeworks\u002Fnuxt.html",[70],"@vite-pwa\u002Fnuxt",[45,156,157,160],{},[60,158,159],{},"State",[60,161,162],{},[26,163,166],{"href":164,"target":68,"rel":165},"https:\u002F\u002Fpinia.vuejs.org\u002F",[70],"Pinia",[45,168,169,172],{},[60,170,171],{},"Styling",[60,173,174,178],{},[26,175,176],{"href":28,"target":68},[14,177,31],{}," — SCSS with 5 swappable morphisms",[45,180,181,184],{},[60,182,183],{},"Dev infra",[60,185,186,187],{},"Docker + Traefik, local HTTPS via ",[26,188,191],{"href":189,"target":68,"rel":190},"https:\u002F\u002Fgithub.com\u002FFiloSottile\u002Fmkcert",[70],"mkcert",[45,193,194,197],{},[60,195,196],{},"AI discoverability",[60,198,199,118,204,207],{},[26,200,203],{"href":201,"target":68,"rel":202},"https:\u002F\u002Fgithub.com\u002Fnuxtlabs\u002Fnuxt-llms",[70],"nuxt-llms",[14,205,206],{},"\u002Fllms.txt"," generated from real content",[10,209,210,211,214],{},"The project runs on ",[14,212,213],{},"nuxt generate"," (SSG) — every page's HTML is generated exactly once, at build time. That architectural decision, made early on, is the root of a good chunk of the project's most interesting technical decisions: anything that would normally be a \"runtime decision\" (breakpoint, theme, locale) has to be resolved without a server re-rendering behind it on every request.",[34,216,218],{"id":217},"i18n-two-fields-two-formats-one-convention","i18n — two fields, two formats, one convention",[10,220,221,222,225,226,229],{},"The easiest thing to get wrong in multi-locale i18n is conflating two concepts that look like the same thing: the ",[18,223,224],{},"URL prefix"," and the ",[18,227,228],{},"HTML language tag",". The project separates them explicitly:",[39,231,232,245],{},[42,233,234],{},[45,235,236,239,242],{},[48,237,238],{},"Field",[48,240,241],{},"Value",[48,243,244],{},"Responsibility",[55,246,247,269,296],{},[45,248,249,253,260],{},[60,250,251],{},[14,252,14],{},[60,254,255,257,258],{},[14,256,121],{}," \u002F ",[14,259,125],{},[60,261,262,263,268],{},"URL prefix — always lowercase (HTTP convention, ",[26,264,267],{"href":265,"target":68,"rel":266},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc3986",[70],"RFC 3986",")",[45,270,271,276,284],{},[60,272,273],{},[14,274,275],{},"language",[60,277,278,257,281],{},[14,279,280],{},"pt-BR",[14,282,283],{},"en-US",[60,285,286,291,292,295],{},[26,287,290],{"href":288,"target":68,"rel":289},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Fbcp\u002Fbcp47.txt",[70],"BCP 47"," — HTML ",[14,293,294],{},"lang",", hreflang, meta tags",[45,297,298,303,311],{},[60,299,300],{},[14,301,302],{},"file",[60,304,305,257,308],{},[14,306,307],{},"pt-BR.json",[14,309,310],{},"en-US.json",[60,312,313],{},"Filesystem path — not a URL, stays uppercase",[10,315,316,317,320,321,323,324,327,328,330,331,334],{},"The rule exists because ",[14,318,319],{},"nuxt-link-checker"," (part of ",[14,322,141],{},") reports ",[14,325,326],{},"no-uppercase-chars"," for any path containing an uppercase letter — switching ",[14,329,14],{}," to lowercase kills the warning at the root, no ",[14,332,333],{},"skipInspections"," hack needed.",[336,337,339],"h3",{"id":338},"collection-names-derived-algorithmically-never-by-ternary","Collection names — derived algorithmically, never by ternary",[10,341,342,343,76,346,76,349,76,352,355,356,359,360,363,364,366],{},"With four collections per content type (",[14,344,345],{},"blog_ptBR",[14,347,348],{},"blog_enUS",[14,350,351],{},"projects_ptBR",[14,353,354],{},"projects_enUS","...), the naive pattern — ",[14,357,358],{},"locale.value === 'en-us' ? queryCollection('blog_enUS') : queryCollection('blog_ptBR')"," — doesn't scale: add a third language and it becomes an ",[14,361,362],{},"if\u002Felse"," chain scattered across dozens of files. The fix was a composable that derives the suffix algorithmically from ",[14,365,14],{},":",[368,369,374],"pre",{"className":370,"code":371,"language":372,"meta":373,"style":373},"language-ts shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F\u002F app\u002Fcomposables\u002FuseLocaleCollection.ts\n\u002F\u002F pt-br → ptBR · en-us → enUS · es-mx → esMX\n\u002F\u002F (split on '-', parts after the hyphen become UPPERCASE, join with no separator)\n\nconst { blogCol, pathPrefix } = useLocaleCollection()\nqueryCollection(blogCol.value).all()\n","ts","",[14,375,376,385,391,397,404,437],{"__ignoreMap":373},[377,378,381],"span",{"class":379,"line":380},"line",1,[377,382,384],{"class":383},"sLBg1","\u002F\u002F app\u002Fcomposables\u002FuseLocaleCollection.ts\n",[377,386,388],{"class":379,"line":387},2,[377,389,390],{"class":383},"\u002F\u002F pt-br → ptBR · en-us → enUS · es-mx → esMX\n",[377,392,394],{"class":379,"line":393},3,[377,395,396],{"class":383},"\u002F\u002F (split on '-', parts after the hyphen become UPPERCASE, join with no separator)\n",[377,398,400],{"class":379,"line":399},4,[377,401,403],{"emptyLinePlaceholder":402},true,"\n",[377,405,407,411,415,419,421,424,427,430,434],{"class":379,"line":406},5,[377,408,410],{"class":409},"sHUrx","const",[377,412,414],{"class":413},"suds8"," { ",[377,416,418],{"class":417},"s-5SL","blogCol",[377,420,76],{"class":413},[377,422,423],{"class":417},"pathPrefix",[377,425,426],{"class":413}," } ",[377,428,429],{"class":409},"=",[377,431,433],{"class":432},"sKwhi"," useLocaleCollection",[377,435,436],{"class":413},"()\n",[377,438,440,443,446,449],{"class":379,"line":439},6,[377,441,442],{"class":432},"queryCollection",[377,444,445],{"class":413},"(blogCol.value).",[377,447,448],{"class":432},"all",[377,450,436],{"class":413},[10,452,453,454,457],{},"Adding a fifth locale becomes: extend a type union in a single file, define the collections in ",[14,455,456],{},"content.config.ts",", create the content folder. Zero changes to listing composables or pages — no new ternary is ever born.",[34,459,461],{"id":460},"ssg-first-no-layout-decisions-in-javascript","SSG-first — no layout decisions in JavaScript",[10,463,464,465,468],{},"Since HTML is generated exactly once, ",[14,466,467],{},"useViewport()"," for visual layout decisions is banned in this project: during SSR\u002FSSG the hook uses a fallback value, not the visitor's real viewport — hydration then discovers the real viewport and swaps the layout out from under the user, guaranteeing CLS.",[10,470,471],{},"The practical rule:",[473,474,475,490,497,504],"ul",{},[476,477,478,479,482,483,76,486,489],"li",{},"Responsiveness ",[18,480,481],{},"always"," via CSS media query (",[14,484,485],{},"fui-col-md-6",[14,487,488],{},"fui-col-lg-4",") — the browser applies it after it already has the real viewport, no mismatch.",[476,491,492,493,496],{},"Item counts, sizes, and breakpoint-conditioned visibility ",[18,494,495],{},"never"," in a listing component's JS.",[476,498,499,500,503],{},"Fixed ",[14,501,502],{},"perPage"," (31, currently) instead of varying per viewport — anyone who wants fewer items per page uses the select, not automatic detection.",[476,505,506],{},"The one exception: purely client-side interaction (hover, focus, animation) can use JS normally — it doesn't affect the already-painted HTML.",[34,508,510],{"id":509},"morphisms-the-fouc-that-ssg-creates-on-its-own","Morphisms — the FOUC that SSG creates on its own",[10,512,513,514,516,517,520,521,524,525,528,529,532],{},"The ",[14,515,31],{}," library was designed to install ",[18,518,519],{},"one"," morphism per project — the selectors it emits are flat, unprefixed (",[14,522,523],{},".fui-btn {}",", not ",[14,526,527],{},".fui-glass .fui-btn {}","). ao.dev deliberately breaks that premise: it loads all five themes as separate CSS files and swaps via a ",[14,530,531],{},"\u003Clink id=\"fui-theme\">",", to give visitors the real-time morphism switcher. That creates two problems the library, on its own, would never need to solve.",[336,534,536],{"id":535},"problem-1-ssg-fouc","Problem 1 — SSG FOUC",[10,538,539,542,543,546,547,550],{},[14,540,541],{},"useCookie"," has no request context during prerender: the generated HTML ships with no morphism class and no theme ",[14,544,545],{},"\u003Clink>",". Vue hydrates, ",[14,548,549],{},"useMorphism"," reads the visitor's cookie, adds the class and the link — and in that window the user sees a flash of completely unstyled theme.",[336,552,554],{"id":553},"problem-2-bleed-between-themes","Problem 2 — bleed between themes",[10,556,557,558,560],{},"Without scoping selectors, ",[14,559,523],{}," from one loaded theme leaks into the whole document, even when a different morphism is active.",[336,562,564],{"id":563},"the-fix-postcss-scoping-a-blocking-script","The fix — PostCSS scoping + a blocking script",[10,566,567,570,571,366],{},[14,568,569],{},"scripts\u002Fbuild-themes.ts"," runs a PostCSS pass after every SCSS compile, prefixing every selector with ",[14,572,573],{},"html.{theme}",[368,575,579],{"className":576,"code":577,"language":578,"meta":373,"style":373},"language-scss shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F* before (emitted by the library, designed for single-theme use) *\u002F\n:root { --fui-color-primary: #7c3aed; }\n.fui-btn { padding: var(--fui-space-3); }\n\n\u002F* after PostCSS scoping, per theme *\u002F\nhtml.skeu { --fui-color-primary: #7c3aed; }\nhtml.skeu .fui-btn { padding: var(--fui-space-3); }\n","scss",[14,580,581,586,606,630,634,639,658],{"__ignoreMap":373},[377,582,583],{"class":379,"line":380},[377,584,585],{"class":383},"\u002F* before (emitted by the library, designed for single-theme use) *\u002F\n",[377,587,588,591,593,597,600,603],{"class":379,"line":387},[377,589,590],{"class":417},":root",[377,592,414],{"class":413},[377,594,596],{"class":595},"soyes","--fui-color-primary",[377,598,599],{"class":413},": ",[377,601,602],{"class":417},"#7c3aed",[377,604,605],{"class":413},"; }\n",[377,607,608,611,613,616,618,621,624,627],{"class":379,"line":393},[377,609,610],{"class":417},".fui-btn",[377,612,414],{"class":413},[377,614,615],{"class":417},"padding",[377,617,599],{"class":413},[377,619,620],{"class":417},"var",[377,622,623],{"class":413},"(",[377,625,626],{"class":595},"--fui-space-3",[377,628,629],{"class":413},"); }\n",[377,631,632],{"class":379,"line":399},[377,633,403],{"emptyLinePlaceholder":402},[377,635,636],{"class":379,"line":406},[377,637,638],{"class":383},"\u002F* after PostCSS scoping, per theme *\u002F\n",[377,640,641,645,648,650,652,654,656],{"class":379,"line":439},[377,642,644],{"class":643},"sne4z","html",[377,646,647],{"class":417},".skeu",[377,649,414],{"class":413},[377,651,596],{"class":595},[377,653,599],{"class":413},[377,655,602],{"class":417},[377,657,605],{"class":413},[377,659,661,663,665,668,670,672,674,676,678,680],{"class":379,"line":660},7,[377,662,644],{"class":643},[377,664,647],{"class":417},[377,666,667],{"class":417}," .fui-btn",[377,669,414],{"class":413},[377,671,615],{"class":417},[377,673,599],{"class":413},[377,675,620],{"class":417},[377,677,623],{"class":413},[377,679,626],{"class":595},[377,681,629],{"class":413},[10,683,684,685,688,689,692,693,696,697,700,701,704,705,708,709,712],{},"And in the ",[14,686,687],{},"\u003Chead>",", an inline script with ",[14,690,691],{},"tagPriority: 'critical'"," runs before any paint: it reads the cookie via regex, validates it against the known morphism list, adds ",[14,694,695],{},"html.{morphism}"," via ",[14,698,699],{},"classList.add"," (idempotent — on SSR the class is already there), and injects a ",[14,702,703],{},"\u003Clink rel=\"preload\" as=\"style\">"," that converts to ",[14,706,707],{},"stylesheet"," on ",[14,710,711],{},"onload",". On SSR the server already renders the correct class — zero FOUC, automatically. On SSG, with no cookie in the HTML, the script takes over on the client, before the first paint.",[714,715,716],"blockquote",{},[10,717,718],{},"Lighthouse tests with no cookies — the script is a no-op, no theme CSS loads, zero impact on Performance.",[336,720,722,723,726],{"id":721},"the-real-bug-layer-order-on-a-cold-reload","The real bug — ",[14,724,725],{},"@layer"," order on a cold reload",[10,728,729,730,733,734,737],{},"This was the hardest bug to diagnose in the entire project. Switching morphism worked perfectly client-side — ",[14,731,732],{},"setMorphism()",", no reload. But an actual ",[18,735,736],{},"reload"," with the cookie already set caused visual bleed: border-radius, padding, gap, font-size, and box-shadow swapping values inconsistently, element by element.",[10,739,740,741,744,745,257,748,751,752,755,756,758,759,761,762,765],{},"Root cause: each generated theme file (",[14,742,743],{},"public\u002Fthemes\u002F{skeu,glass,neu,clay}.css",") only declared loose ",[14,746,747],{},"@layer fui.components {}",[14,749,750],{},"@layer fui.utilities {}"," blocks — only the base bundle declared the full canonical order (",[14,753,754],{},"@layer fui.layout, fui.base, fui.components, fui.utilities;","). In a real network race (the inline script injects the morphism's ",[14,757,545],{}," very early), if the browser parsed the theme ",[14,760,545],{}," ",[18,763,764],{},"before"," the base bundle, the global layer registration order came out wrong — and the base's raw reset started winning over the morphism's utilities and components.",[10,767,768,769,772,773,775],{},"The fix: prefix every generated theme file with the ",[18,770,771],{},"same"," order declaration as the base, before any selector. It no longer matters which file the browser parses first — the correct order is established up front by whichever one loads, and the second identical declaration is a no-op. It eliminates the network race structurally, with no dependency on ",[14,774,545],{}," timing.",[34,777,779],{"id":778},"hydration-when-the-seo-module-itself-caused-the-mismatch","Hydration — when the SEO module itself caused the mismatch",[10,781,782,783,786,787,790],{},"Another bug that only ever showed up in SSG, never in dev: ",[14,784,785],{},"Hydration completed but contains mismatches.",", always in the same spot — a text node inside a ",[14,788,789],{},"\u003Cstyle>"," in the body.",[10,792,793,794,797,798,800,801,804,805,808,809,811,812,814,815,817,818,821],{},"The culprit was ",[14,795,796],{},"nuxt-seo-utils"," (bundled via ",[14,799,141],{},"), which has ",[14,802,803],{},"minify: true"," by default. The Nitro HTML-minification plugin compresses ",[18,806,807],{},"every"," inline ",[14,810,789],{}," during prerender — including the ",[14,813,789],{}," Shiki\u002FMDC injects into the body carrying syntax-highlighting CSS vars. Result: the server's HTML had minified CSS, the Nuxt payload had unminified CSS (coming straight from ",[14,816,104],{},"'s SQLite), and the client re-rendered ",[14,819,820],{},"MDCRenderer"," using the payload — different CSS, guaranteed mismatch.",[368,823,825],{"className":370,"code":824,"language":372,"meta":373,"style":373},"\u002F\u002F nuxt.config.ts\nseo: {\n  minify: { build: false }, \u002F\u002F only disables body minification on prerender —\n},                          \u002F\u002F runtime (head tags via unhead) stays on and doesn't cause mismatches\n",[14,826,827,832,840,862],{"__ignoreMap":373},[377,828,829],{"class":379,"line":380},[377,830,831],{"class":383},"\u002F\u002F nuxt.config.ts\n",[377,833,834,837],{"class":379,"line":387},[377,835,836],{"class":595},"seo",[377,838,839],{"class":413},": {\n",[377,841,842,845,848,851,853,856,859],{"class":379,"line":393},[377,843,844],{"class":595},"  minify",[377,846,847],{"class":413},": { ",[377,849,850],{"class":595},"build",[377,852,599],{"class":413},[377,854,855],{"class":417},"false",[377,857,858],{"class":413}," }, ",[377,860,861],{"class":383},"\u002F\u002F only disables body minification on prerender —\n",[377,863,864,867],{"class":379,"line":399},[377,865,866],{"class":413},"},                          ",[377,868,869],{"class":383},"\u002F\u002F runtime (head tags via unhead) stays on and doesn't cause mismatches\n",[10,871,872],{},"Debugging this class of mismatch in production (SSG) has a little-known Vue 3.4+ flag that enables detailed messages even in a production build:",[368,874,876],{"className":370,"code":875,"language":372,"meta":373,"style":373},"vite: {\n  define: {\n    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(true),\n  },\n}\n",[14,877,878,885,892,916,921],{"__ignoreMap":373},[377,879,880,883],{"class":379,"line":380},[377,881,882],{"class":595},"vite",[377,884,839],{"class":413},[377,886,887,890],{"class":379,"line":387},[377,888,889],{"class":595},"  define",[377,891,839],{"class":413},[377,893,894,897,899,902,905,908,910,913],{"class":379,"line":393},[377,895,896],{"class":595},"    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__",[377,898,599],{"class":413},[377,900,901],{"class":417},"JSON",[377,903,904],{"class":413},".",[377,906,907],{"class":432},"stringify",[377,909,623],{"class":413},[377,911,912],{"class":417},"true",[377,914,915],{"class":413},"),\n",[377,917,918],{"class":379,"line":399},[377,919,920],{"class":413},"  },\n",[377,922,923],{"class":379,"line":406},[377,924,925],{"class":413},"}\n",[34,927,929],{"id":928},"performance-audit-real-numbers-not-guesswork","Performance audit — real numbers, not guesswork",[10,931,932],{},"A pre-deploy audit on the home page, running Lighthouse seriously (not just once, accepting whatever number came back), surfaced a series of systemic issues — things that apply to any new page, not just the one that was tested.",[336,934,936],{"id":935},"compression-the-production-preset-didnt-do-on-its-own","Compression the production preset didn't do on its own",[10,938,939,942,943,945,946,949,950,953,954,957,958,961,962,965],{},[14,940,941],{},"compressPublicAssets"," is ",[14,944,855],{}," by default in Nitro. Without it, the ",[14,947,948],{},"node-server"," preset (what runs in SSR production) serves ",[14,951,952],{},"_nuxt\u002F*.css","\u002F",[14,955,956],{},"*.js","\u002Ffonts ",[18,959,960],{},"with no gzip or brotli"," — we measured the exact same CSS file ",[18,963,964],{},"15× larger"," in SSR (1.28 MB) than on a generic static server (83 KB) serving the same build.",[368,967,969],{"className":370,"code":968,"language":372,"meta":373,"style":373},"nitro: {\n  compressPublicAssets: { gzip: true, brotli: true },\n},\n",[14,970,971,978,1004],{"__ignoreMap":373},[377,972,973,976],{"class":379,"line":380},[377,974,975],{"class":595},"nitro",[377,977,839],{"class":413},[377,979,980,983,985,988,990,992,994,997,999,1001],{"class":379,"line":387},[377,981,982],{"class":595},"  compressPublicAssets",[377,984,847],{"class":413},[377,986,987],{"class":595},"gzip",[377,989,599],{"class":413},[377,991,912],{"class":417},[377,993,76],{"class":413},[377,995,996],{"class":595},"brotli",[377,998,599],{"class":413},[377,1000,912],{"class":417},[377,1002,1003],{"class":413}," },\n",[377,1005,1006],{"class":379,"line":393},[377,1007,1008],{"class":413},"},\n",[336,1010,1012],{"id":1011},"global-component-fetches-only-on-interaction-never-on-mount","Global component fetches only on interaction, never on mount",[10,1014,1015,1018,1019,708,1022,1025,1026,1029,1030,1033,1034,1038,1039,1041],{},[14,1016,1017],{},"CommandPaletteComponent"," (⌘K search) stays mounted in ",[14,1020,1021],{},"app.vue",[18,1023,1024],{},"every page",", even closed. It called ",[14,1027,1028],{},"useAsyncData(..., { server: false, lazy: true })"," — but ",[14,1031,1032],{},"lazy: true"," only defers the ",[1035,1036,1037],"em",{},"render",", the fetch still fires immediately after hydration regardless. That instantiated ",[14,1040,104],{},"'s search engine (client-side SQLite-WASM) on every page, even when a visitor never opened search: we measured 1–2.4s of unattributed scripting at bootup and 239–404 KiB of unused JS.",[10,1043,1044,1045,708,1048,1051,1052,1055],{},"The fix: ",[14,1046,1047],{},"immediate: false",[14,1049,1050],{},"useAsyncData",", with ",[14,1053,1054],{},"execute()"," fired manually only when the user actually opens search.",[1057,1058,1061,1069],"figure",{"className":1059},[1060],"rehype-figure",[1062,1063],"img",{"alt":1064,"height":1065,"src":1066,"title":1067,"width":1068},"Command Palette (⌘K) open, with project, post, and tag suggestions pulled from real content before you even start typing",675,"\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-command-palette.png","Command Palette",1200,[1070,1071,1064],"figcaption",{},[336,1073,1075],{"id":1074},"purging-unused-css","Purging unused CSS",[10,1077,513,1078,1080,1081,1084,1085,1088,1089,1092],{},[14,1079,31],{}," library is utility-first — it generates a fixed set of classes at compile time, with no notion of which ",[14,1082,1083],{},".vue"," components in this project actually use what. ",[14,1086,1087],{},"@fullhuman\u002Fpostcss-purgecss",", configured in ",[14,1090,1091],{},"nuxt.config.ts"," (never in the library itself), fixes that as a build optimization equivalent to JS tree-shaking:",[39,1094,1095,1111],{},[42,1096,1097],{},[45,1098,1099,1102,1105,1108],{},[48,1100,1101],{},"Metric",[48,1103,1104],{},"Before",[48,1106,1107],{},"After",[48,1109,1110],{},"Reduction",[55,1112,1113,1127,1141],{},[45,1114,1115,1118,1121,1124],{},[60,1116,1117],{},"Raw size",[60,1119,1120],{},"1.28 MB",[60,1122,1123],{},"143 KB",[60,1125,1126],{},"−89%",[45,1128,1129,1132,1135,1138],{},[60,1130,1131],{},"Gzip size",[60,1133,1134],{},"~113 KB",[60,1136,1137],{},"~21.6 KB",[60,1139,1140],{},"−81%",[45,1142,1143,1146,1149,1152],{},[60,1144,1145],{},"CSS classes",[60,1147,1148],{},"14,508",[60,1150,1151],{},"464",[60,1153,1154],{},"—",[10,1156,1157,1158,1161,1162,1165,1166,1169],{},"The real risk of automated purging is a dynamically-assembled class — ",[14,1159,1160],{},"`fui-tag-${tech.color}`"," never appears as a complete string in the source, so the scanner would miss it and strip the class. Before enabling it, a sweep for template-literal interpolation prefixed with ",[14,1163,1164],{},"fui-"," identified the few real cases, which went into a regex safelist (",[14,1167,1168],{},"\u002F^fui-tag-\u002F","). The result was validated with a rebuild + screenshot + computed-styles check across three pages before calling it done — never just \"the build passed with no errors.\"",[336,1171,1173],{"id":1172},"lcp-priority-per-page","LCP priority per page",[10,1175,1176,1177,1180],{},"The LCP element changes per page — on the home it's the first morphism showcase card, elsewhere it might be a cover image. Only the actual LCP element gets ",[14,1178,1179],{},"fetchpriority=\"high\""," plus preload; applying it to several images dilutes priority and helps nothing.",[368,1182,1185],{"className":1183,"code":1184,"language":644,"meta":373,"style":373},"language-html shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u003CNuxtImg\n  :preload=\"isLcp ? { fetchPriority: 'high' } : undefined\"\n  :fetchpriority=\"isLcp ? 'high' : undefined\"\n  ...\n\u002F>\n",[14,1186,1187,1196,1207,1217,1222],{"__ignoreMap":373},[377,1188,1189,1192],{"class":379,"line":380},[377,1190,1191],{"class":413},"\u003C",[377,1193,1195],{"class":1194},"sh5d0","NuxtImg\n",[377,1197,1198,1201,1203],{"class":379,"line":387},[377,1199,1200],{"class":417},"  :preload",[377,1202,429],{"class":413},[377,1204,1206],{"class":1205},"sT6z2","\"isLcp ? { fetchPriority: 'high' } : undefined\"\n",[377,1208,1209,1212,1214],{"class":379,"line":393},[377,1210,1211],{"class":417},"  :fetchpriority",[377,1213,429],{"class":413},[377,1215,1216],{"class":1205},"\"isLcp ? 'high' : undefined\"\n",[377,1218,1219],{"class":379,"line":399},[377,1220,1221],{"class":417},"  ...\n",[377,1223,1224],{"class":379,"line":406},[377,1225,1226],{"class":413},"\u002F>\n",[34,1228,1230,1232],{"id":1229},"querycollection-payload-is-a-budget-not-a-detail",[14,1231,442],{}," — payload is a budget, not a detail",[10,1234,513,1235,1238,1239,1242,1243,1246,1247,1249,1250,1252],{},[14,1236,1237],{},"body"," field of a ",[14,1240,1241],{},"type: 'page'"," collection is the full Markdown AST — 30 to 200 KB per document. A listing query with no ",[14,1244,1245],{},".select()"," includes ",[18,1248,807],{}," post's ",[14,1251,1237],{},", multiplied per SSG route.",[368,1254,1256],{"className":370,"code":1255,"language":372,"meta":373,"style":373},"\u002F\u002F WRONG — every post's body (~770 KB per route)\nqueryCollection(blogCol.value).order('date', 'DESC').all()\n\n\u002F\u002F RIGHT — frontmatter only (~5–20 KB)\nqueryCollection(blogCol.value)\n  .select('path', 'title', 'shortTitle', 'description', 'date', 'readTime', 'tags', 'cover')\n  .order('date', 'DESC')\n  .all()\n",[14,1257,1258,1263,1289,1293,1298,1305,1355,1371],{"__ignoreMap":373},[377,1259,1260],{"class":379,"line":380},[377,1261,1262],{"class":383},"\u002F\u002F WRONG — every post's body (~770 KB per route)\n",[377,1264,1265,1267,1269,1272,1274,1277,1279,1282,1285,1287],{"class":379,"line":387},[377,1266,442],{"class":432},[377,1268,445],{"class":413},[377,1270,1271],{"class":432},"order",[377,1273,623],{"class":413},[377,1275,1276],{"class":1205},"'date'",[377,1278,76],{"class":413},[377,1280,1281],{"class":1205},"'DESC'",[377,1283,1284],{"class":413},").",[377,1286,448],{"class":432},[377,1288,436],{"class":413},[377,1290,1291],{"class":379,"line":393},[377,1292,403],{"emptyLinePlaceholder":402},[377,1294,1295],{"class":379,"line":399},[377,1296,1297],{"class":383},"\u002F\u002F RIGHT — frontmatter only (~5–20 KB)\n",[377,1299,1300,1302],{"class":379,"line":406},[377,1301,442],{"class":432},[377,1303,1304],{"class":413},"(blogCol.value)\n",[377,1306,1307,1310,1313,1315,1318,1320,1323,1325,1328,1330,1333,1335,1337,1339,1342,1344,1347,1349,1352],{"class":379,"line":439},[377,1308,1309],{"class":413},"  .",[377,1311,1312],{"class":432},"select",[377,1314,623],{"class":413},[377,1316,1317],{"class":1205},"'path'",[377,1319,76],{"class":413},[377,1321,1322],{"class":1205},"'title'",[377,1324,76],{"class":413},[377,1326,1327],{"class":1205},"'shortTitle'",[377,1329,76],{"class":413},[377,1331,1332],{"class":1205},"'description'",[377,1334,76],{"class":413},[377,1336,1276],{"class":1205},[377,1338,76],{"class":413},[377,1340,1341],{"class":1205},"'readTime'",[377,1343,76],{"class":413},[377,1345,1346],{"class":1205},"'tags'",[377,1348,76],{"class":413},[377,1350,1351],{"class":1205},"'cover'",[377,1353,1354],{"class":413},")\n",[377,1356,1357,1359,1361,1363,1365,1367,1369],{"class":379,"line":660},[377,1358,1309],{"class":413},[377,1360,1271],{"class":432},[377,1362,623],{"class":413},[377,1364,1276],{"class":1205},[377,1366,76],{"class":413},[377,1368,1281],{"class":1205},[377,1370,1354],{"class":413},[377,1372,1374,1376,1378],{"class":379,"line":1373},8,[377,1375,1309],{"class":413},[377,1377,448],{"class":432},[377,1379,436],{"class":413},[10,1381,1382,1383,1386,1387,1389,1390,953,1393,1396,1397,1399,1400,1402,1403,1405,1406,1409,1410,1412,1413,1416,1417,1420,1421,1424,1425,1428,1429,1431,1432,904],{},"Two rules followed from this: ",[18,1384,1385],{},"indexes"," in ",[14,1388,456],{}," for every column used in ",[14,1391,1392],{},".where()",[14,1394,1395],{},".order()"," (mandatory on databases like Cloudflare D1, where every row read is billed); and ",[14,1398,1050],{}," keys ",[18,1401,481],{}," delimited with ",[14,1404,366],{},", never ",[14,1407,1408],{},"-",", whenever they combine a literal prefix with a dynamic slug — ",[14,1411,1050],{},"'s cache is global to the app, and ",[14,1414,1415],{},"blog-${slug}"," collides structurally between a post ",[14,1418,1419],{},"\"prev-foo\""," and another post ",[14,1422,1423],{},"\"foo\""," whose own key also becomes ",[14,1426,1427],{},"blog-prev-foo-...",". With ",[14,1430,366],{}," as the delimiter, the collision is impossible, because content slugs never contain ",[14,1433,366],{},[34,1435,1437],{"id":1436},"accessibility-what-ibm-equal-access-taught","Accessibility — what IBM Equal Access taught",[10,1439,1440,1441,1446],{},"Real audit cycles with the ",[26,1442,1445],{"href":1443,"target":68,"rel":1444},"https:\u002F\u002Fwww.ibm.com\u002Fable\u002Ftoolkit\u002Ftools\u002F",[70],"IBM Equal Access Checker"," corrected patterns that looked right until they were actually tested:",[473,1448,1449,1478,1499,1516],{},[476,1450,1451,118,1454,1457,1458,1461,1462,1465,1466,1469,1470,1473,1474,1477],{},[18,1452,1453],{},"ARIA combobox is ARIA 1.2",[14,1455,1456],{},"role=\"combobox\""," belongs on the ",[14,1459,1460],{},"\u003Cinput>"," itself, never on a wrapping ",[14,1463,1464],{},"\u003Cdiv>","; the input must be ",[14,1467,1468],{},"type=\"text\"",", since ",[14,1471,1472],{},"type=\"search\""," already carries an implicit ",[14,1475,1476],{},"searchbox"," role and rejects the override.",[476,1479,1480,1486,1487,1490,1491,1494,1495,1498],{},[18,1481,1482,1485],{},[14,1483,1484],{},"\u003Cp>"," with bold text inside a link triggers a false-heading heuristic"," — IBM interprets ",[14,1488,1489],{},"\u003Cp class=\"fui-fw-bold\">"," inside an ",[14,1492,1493],{},"\u003Ca>"," as a possibly malformed heading. Swapping in ",[14,1496,1497],{},"\u003Cspan class=\"fui-d-block\">"," keeps the exact same look without tripping the rule, because a span is inline.",[476,1500,1501,1507,1508,1511,1512,1515],{},[18,1502,1503,1506],{},[14,1504,1505],{},"aria-labelledby"," never points at a conditionally-absent element"," — if the target is ",[14,1509,1510],{},"v-if","'d, the value falls back to ",[14,1513,1514],{},"undefined"," when that element isn't in the DOM.",[476,1517,1518,1524,1525,1527],{},[18,1519,1520,1523],{},[14,1521,1522],{},"new Date()"," as a template fallback is a guaranteed hydration mismatch"," — the server's timestamp never matches the client's; the fix is ",[14,1526,1510],{}," to omit the element, never generating a date \"however.\"",[714,1529,1530],{},[10,1531,1532,1533,1536,1537,1540,1541,1544],{},"Not every IBM finding is a real bug. ",[14,1534,1535],{},"aria_keyboard_handler_exists"," fires because the tool can't see Vue's ",[14,1538,1539],{},"@keydown"," (only plain HTML ",[14,1542,1543],{},"onkeydown",") — a documented false positive, not something to \"fix\" blindly.",[34,1546,1548,1549,1551],{"id":1547},"ai-discoverability-llmstxt-generated-never-hardcoded","AI discoverability — ",[14,1550,206],{},", generated, never hardcoded",[10,1553,1554,1555,1557,1558,72,1561,1566,1567,1569,1570,76,1572,1574],{},"The site exposes ",[14,1556,206],{}," and ",[14,1559,1560],{},"\u002Fllms-full.txt",[26,1562,1565],{"href":1563,"target":68,"rel":1564},"https:\u002F\u002Fllmstxt.org",[70],"a community standard",") via ",[14,1568,203],{}," — the free alternative to Nuxt SEO's paid PRO equivalent. Every section of the file is generated from a real collection (",[14,1571,345],{},[14,1573,354],{},"...), never from a static template — the file can never go stale because there's no hand-written version to grow old.",[10,1576,1577,1578,1581],{},"The same library exposes the raw Markdown of every content page via ",[14,1579,1580],{},"\u002Fraw\u002F\u003Cpath>.md"," — that's the \"View as Markdown\" link at the top of every blog post and case study, including this very one you're reading now.",[1057,1583,1585,1590],{"className":1584},[1060],[1062,1586],{"alt":1587,"height":1065,"src":1588,"title":1589,"width":1068},"Case study header with the \"View as Markdown\" link visible, exposing the real content via \u002Fraw\u002F\u003Cpath>.md","\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-ai-actions.png","View as Markdown",[1070,1591,1587],{},[34,1593,1595],{"id":1594},"the-finalization-checklist-five-phases-always-in-this-order","The finalization checklist — five phases, always in this order",[10,1597,1598],{},"No new page (or significant refactor) is considered done without going through, in order:",[1600,1601,1602,1616,1624,1637,1643],"ol",{},[476,1603,1604,118,1607,76,1610,76,1613],{},[18,1605,1606],{},"Lint + typecheck",[14,1608,1609],{},"lint:fix",[14,1611,1612],{},"lint:css:fix",[14,1614,1615],{},"typecheck",[476,1617,1618,118,1621,1623],{},[18,1619,1620],{},"SSG audit",[14,1622,213],{}," + static preview, WAVE (visual) + IBM Equal Access (exported report) across pt-BR\u002Fen-US × light\u002Fdark",[476,1625,1626,118,1629,1632,1633,1636],{},[18,1627,1628],{},"SSR audit",[14,1630,1631],{},"nuxt build"," + ",[14,1634,1635],{},"nuxi preview",", same toolset, extra attention to hydration and Schema.org in the initial HTML",[476,1638,1639,1642],{},[18,1640,1641],{},"Build logs"," — payloads over 50 KB get investigated, not ignored",[476,1644,1645,1648],{},[18,1646,1647],{},"Lighthouse"," — the best possible score across Performance, Accessibility, Best Practices, and SEO, with LCP, INP, and CLS inside the Core Web Vitals recommended thresholds, across both dimensions (desktop\u002Fmobile) × both themes (light\u002Fdark)",[10,1650,1651,1652,1655],{},"Lighthouse's default ",[14,1653,1654],{},"throttlingMethod: simulate"," has some run-to-run variance — the exact same page, with no code change at all, can come out slightly different from one run to the next, depending on machine load at test time. Every page gets pushed for the highest score achievable; the real acceptance bar is always whatever the most recent run under those conditions comes back with.",[10,1657,1658],{},"Only once all five steps pass does a page get committed. The rule exists because on this project, Core Web Vitals aren't a bonus — they're a requirement, at the same level as lint or typecheck.",[34,1660,1662],{"id":1661},"the-projects-listing-the-design-system-at-scale","The projects listing — the design system at scale",[1057,1664,1666,1671],{"className":1665},[1060],[1062,1667],{"alt":1668,"height":1065,"src":1669,"title":1670,"width":1068},"Grid project listing, with category filter and several cards using the same card component","\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-projects-listing.png","Projects listing",[1070,1672,1668],{},[10,1674,1675,1676,1678,1679,1682,1683,1686,1687,1690],{},"This is the most honest page to see ",[14,1677,31],{}," working at scale: dozens of cards, all the exact same component (",[14,1680,1681],{},"ProjectCardGridComponent","), each pulling ",[14,1684,1685],{},"cover"," from every project's real frontmatter, with search, category filtering, and live counts — all listing logic lives in composables (",[14,1688,1689],{},"useProjectsListing","), zero duplication between the grid and list views.",[34,1692,1694],{"id":1693},"mobile-nav-and-global-search","Mobile nav and global search",[10,1696,1697,1698,1700,1701,1704],{},"Off desktop, primary navigation becomes fixed bottom tabs (Home, Projects, Search, Blog, Contact) plus a hamburger menu for the rest of the links — never decided by ",[14,1699,467],{}," (same SSG-first rule already described), always plain CSS reacting to the device's real viewport. That's a lot easier to feel live than in a resized desktop screenshot — if you're reading this on a laptop, it's worth opening ",[14,1702,1703],{},"ao.dev.br"," straight on your phone to see the real tab bar, on a real device.",[34,1706,1708],{"id":1707},"whats-still-missing","What's still missing",[10,1710,1711],{},"Two fronts remain deliberately unresolved:",[473,1713,1714,1734],{},[476,1715,1716,118,1728,1730,1731,1733],{},[18,1717,1718,1721,1722,1727],{},[14,1719,1720],{},"@nuxtjs\u002Fmdc"," → ",[26,1723,1726],{"href":1724,"target":68,"rel":1725},"https:\u002F\u002Fcomark.dev",[70],"Comark"," migration",[14,1729,1720],{}," is in confirmed deprecation by its own maintaining team, in favor of Comark (\"faster, AI-friendly, and no longer bound to Vue or Nuxt\"). ",[14,1732,104],{}," still depends on the old version; the real migration only arrives via a dependency update, with an official breaking-changes guide — installing Comark manually before that lands makes no sense.",[476,1735,1736,1741,1742,1745,1746,1749,1750,1753],{},[18,1737,1738],{},[14,1739,1740],{},"@nuxt\u002Fa11y"," — removed from ",[14,1743,1744],{},"modules:"," over real documented bugs (a version conflict with ",[14,1747,1748],{},"@nuxt\u002Fdevtools-kit",", silenced RPC errors within the module itself), but kept in ",[14,1751,1752],{},"package.json"," — re-enabling it is its own dedicated investigation cycle, not a last-minute tweak right before deploy.",[10,1755,1756,1757,1759],{},"This case study closes an odd loop to write: it's the only project in this portfolio whose \"client\" is myself, whose \"deploy\" hasn't happened yet at the moment I'm writing this, and whose quality audit applies to the very text you're reading. The first blog post and the deploy to ",[14,1758,1703],{}," come right after this commit.",[1761,1762,1763],"style",{},"html pre.shiki code .sLBg1, html code.shiki .sLBg1{--shiki-default:#66707B;--shiki-dark:#BDC4CC}html pre.shiki code .sHUrx, html code.shiki .sHUrx{--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .suds8, html code.shiki .suds8{--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .s-5SL, html code.shiki .s-5SL{--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .sKwhi, html code.shiki .sKwhi{--shiki-default:#622CBC;--shiki-dark:#DBB7FF}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 .soyes, html code.shiki .soyes{--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .sne4z, html code.shiki .sne4z{--shiki-default:#024C1A;--shiki-dark:#72F088}html pre.shiki code .sh5d0, html code.shiki .sh5d0{--shiki-default:#6E011A;--shiki-default-font-style:italic;--shiki-dark:#FFB1AF;--shiki-dark-font-style:italic}html pre.shiki code .sT6z2, html code.shiki .sT6z2{--shiki-default:#032563;--shiki-dark:#ADDCFF}",{"title":373,"searchDepth":387,"depth":387,"links":1765},[1766,1767,1770,1771,1778,1779,1785,1787,1788,1790,1791,1792,1793],{"id":36,"depth":387,"text":37},{"id":217,"depth":387,"text":218,"children":1768},[1769],{"id":338,"depth":393,"text":339},{"id":460,"depth":387,"text":461},{"id":509,"depth":387,"text":510,"children":1772},[1773,1774,1775,1776],{"id":535,"depth":393,"text":536},{"id":553,"depth":393,"text":554},{"id":563,"depth":393,"text":564},{"id":721,"depth":393,"text":1777},"The real bug — @layer order on a cold reload",{"id":778,"depth":387,"text":779},{"id":928,"depth":387,"text":929,"children":1780},[1781,1782,1783,1784],{"id":935,"depth":393,"text":936},{"id":1011,"depth":393,"text":1012},{"id":1074,"depth":393,"text":1075},{"id":1172,"depth":393,"text":1173},{"id":1229,"depth":387,"text":1786},"queryCollection — payload is a budget, not a detail",{"id":1436,"depth":387,"text":1437},{"id":1547,"depth":387,"text":1789},"AI discoverability — \u002Fllms.txt, generated, never hardcoded",{"id":1594,"depth":387,"text":1595},{"id":1661,"depth":387,"text":1662},{"id":1693,"depth":387,"text":1694},{"id":1707,"depth":387,"text":1708},[1795,1796],"portfolio","open-source","Personal","\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-cover.png","2026-07-12","The portfolio you're browsing right now — a reference boilerplate for the Nuxt 4 ecosystem, SSG-first, five design morphisms switchable in real time, full i18n, and a performance\u002Faccessibility audit documented step by step.","md","\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-detail.png",[1804],{"label":1703,"url":1805},"https:\u002F\u002Fao.dev.br",{},21,"\u002Fen-us\u002Fprojects\u002Fao-dev","2026 – present",[1811,1812],"Creator","Developer",{"title":5,"description":1800},[71,1815,92,104,117,141,166,1816,1817],"Vue 3","fui-content (SCSS)","Docker + Traefik","in-development","en-us\u002Fprojects\u002Fao-dev",[71,92,1821,110,1822,1823,1824],"Nuxt Content","SSG","Performance","Accessibility","imwm6B-V_J8uD3ryhdNzTUefPb-tmxsxblutslZTUc8",{"path":1827,"title":31,"order":1828},"\u002Fen-us\u002Fprojects\u002Ffui-content",20,null,1784312184519]