[{"data":1,"prerenderedAt":1829},["ShallowReactive",2],{"project:\u002Fao-dev:pt-br":3,"project:prev:\u002Fao-dev:pt-br":1826,"project:next:\u002Fao-dev:pt-br":1828},{"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_ptBR\u002Fprojects\u002Fao-dev.md","ao.dev",{"type":7,"value":8,"toc":1764},"minimark",[9,22,33,38,208,215,219,230,313,334,339,367,451,458,462,469,472,506,510,532,536,550,554,560,564,573,681,712,718,726,737,765,775,779,790,821,869,872,924,928,931,935,964,1007,1011,1042,1056,1072,1076,1093,1155,1170,1174,1181,1227,1233,1254,1381,1434,1438,1447,1528,1545,1552,1575,1582,1592,1596,1599,1649,1655,1658,1662,1672,1690,1694,1704,1708,1711,1754,1760],[10,11,12,13,16,17,21],"p",{},"Este é o único case study do portfólio que descreve o próprio portfólio. ",[14,15,5],"code",{}," nasceu com uma intenção dupla: ser minha vitrine profissional e, ao mesmo tempo, servir como ",[18,19,20],"strong",{},"boilerplate de referência"," do ecossistema Nuxt 4 — um projeto real, gerado e não abandonado no meio, que qualquer pessoa possa ler de ponta a ponta e aprender algo genuíno sobre SSG, i18n, design systems trocáveis e auditoria de performance levada a sério.",[10,23,24,25,32],{},"Todo componente visual daqui vem do ",[26,27,29],"a",{"href":28},"\u002Fprojects\u002Ffui-content",[14,30,31],{},"fui-content",", a lib de temas que construí em paralelo. Todo case study que você já leu neste site — Leve Mix, Grupo Clamed, os módulos do Grupo 3C — foi escrito, revisado e publicado através do próprio Nuxt Content que este projeto usa. É o projeto que fecha o círculo: a ferramenta e a vitrine da ferramenta são a mesma coisa.",[34,35,37],"h2",{"id":36},"por-baixo-do-capo","Por baixo do capô",[39,40,41,54],"table",{},[42,43,44],"thead",{},[45,46,47,51],"tr",{},[48,49,50],"th",{},"Camada",[48,52,53],{},"Tecnologia",[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",", diretório ",[14,78,79],{},"app\u002F",")",[45,82,83,86],{},[60,84,85],{},"Linguagem",[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],{},"Conteúdo",[60,99,100,105],{},[26,101,104],{"href":102,"target":68,"rel":103},"https:\u002F\u002Fcontent.nuxt.com\u002F",[70],"@nuxt\u002Fcontent"," — Markdown com frontmatter tipado via Zod",[45,107,108,111],{},[60,109,110],{},"i18n",[60,112,113,118,119,122,123,126,127],{},[26,114,117],{"href":115,"target":68,"rel":116},"https:\u002F\u002Fi18n.nuxtjs.org\u002F",[70],"@nuxtjs\u002Fi18n"," — ",[14,120,121],{},"pt-br"," (default) e ",[14,124,125],{},"en-us",", estratégia ",[14,128,129],{},"prefix_except_default",[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],{},"Estado",[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],{},"Estilos",[60,173,174,178],{},[26,175,176],{"href":28,"target":68},[14,177,31],{}," — SCSS com 5 morfismos trocáveis",[45,180,181,184],{},[60,182,183],{},"Infra dev",[60,185,186,187],{},"Docker + Traefik, HTTPS local 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"," gerado do conteúdo real",[10,209,210,211,214],{},"O projeto roda em cima de ",[14,212,213],{},"nuxt generate"," (SSG) — o HTML de cada página é gerado uma única vez, em build time. Essa decisão de arquitetura, tomada logo no início, é a raiz de boa parte das decisões técnicas mais interessantes do projeto: tudo que normalmente seria \"decisão em runtime\" (breakpoint, tema, idioma) precisa ser resolvido sem um servidor por trás re-renderizando a cada request.",[34,216,218],{"id":217},"i18n-dois-campos-dois-formatos-uma-convencao","i18n — dois campos, dois formatos, uma convenção",[10,220,221,222,225,226,229],{},"O detalhe mais fácil de errar em i18n multi-locale é misturar dois conceitos que parecem a mesma coisa: o ",[18,223,224],{},"prefixo de URL"," e a ",[18,227,228],{},"tag de idioma HTML",". O projeto separa os dois explicitamente:",[39,231,232,245],{},[42,233,234],{},[45,235,236,239,242],{},[48,237,238],{},"Campo",[48,240,241],{},"Valor",[48,243,244],{},"Responsabilidade",[55,246,247,268,295],{},[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,80],{},"Prefixo de URL — sempre lowercase (convenção HTTP, ",[26,264,267],{"href":265,"target":68,"rel":266},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc3986",[70],"RFC 3986",[45,269,270,275,283],{},[60,271,272],{},[14,273,274],{},"language",[60,276,277,257,280],{},[14,278,279],{},"pt-BR",[14,281,282],{},"en-US",[60,284,285,290,291,294],{},[26,286,289],{"href":287,"target":68,"rel":288},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Fbcp\u002Fbcp47.txt",[70],"BCP 47"," — HTML ",[14,292,293],{},"lang",", hreflang, meta tags",[45,296,297,302,310],{},[60,298,299],{},[14,300,301],{},"file",[60,303,304,257,307],{},[14,305,306],{},"pt-BR.json",[14,308,309],{},"en-US.json",[60,311,312],{},"Caminho de filesystem — não é URL, mantém uppercase",[10,314,315,316,319,320,322,323,326,327,329,330,333],{},"A regra existe porque ",[14,317,318],{},"nuxt-link-checker"," (parte do ",[14,321,141],{},") reporta ",[14,324,325],{},"no-uppercase-chars"," para qualquer path com letra maiúscula — trocar o ",[14,328,14],{}," pra lowercase elimina o warning na raiz, sem precisar de ",[14,331,332],{},"skipInspections"," como gambiarra.",[335,336,338],"h3",{"id":337},"nomes-de-colecao-derivados-algoritmicamente-nunca-por-ternario","Nomes de coleção — derivados algoritmicamente, nunca por ternário",[10,340,341,342,345,346,345,349,345,352,355,356,359,360,363,364,366],{},"Com quatro coleções por tipo de conteúdo (",[14,343,344],{},"blog_ptBR",", ",[14,347,348],{},"blog_enUS",[14,350,351],{},"projects_ptBR",[14,353,354],{},"projects_enUS","...), o padrão ingênuo — ",[14,357,358],{},"locale.value === 'en-us' ? queryCollection('blog_enUS') : queryCollection('blog_ptBR')"," — não escala: com um terceiro idioma vira uma cadeia de ",[14,361,362],{},"if\u002Felse"," espalhada por dezenas de arquivos. A solução foi um composable que deriva o sufixo algoritmicamente a partir do ",[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 '-', partes após o hífen viram UPPERCASE, join sem separador)\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 '-', partes após o hífen viram UPPERCASE, join sem separador)\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,345],{"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],{},"Adicionar um quinto idioma vira: estender uma type union num único arquivo, criar as coleções em ",[14,455,456],{},"content.config.ts",", criar a pasta de conteúdo. Zero mudança nos composables de listagem ou nas páginas — nenhum novo ternário nasce.",[34,459,461],{"id":460},"ssg-first-nenhuma-decisao-de-layout-em-javascript","SSG-first — nenhuma decisão de layout em JavaScript",[10,463,464,465,468],{},"Como o HTML é gerado uma única vez, ",[14,466,467],{},"useViewport()"," para decisão de layout visual é proibido no projeto: no SSR\u002FSSG, o hook usa um valor de fallback, não o viewport real do visitante — a hidratação então descobre o viewport de verdade e troca o layout debaixo do usuário, gerando CLS garantido.",[10,470,471],{},"A regra prática:",[473,474,475,490,497,503],"ul",{},[476,477,478,479,482,483,345,486,489],"li",{},"Responsividade ",[18,480,481],{},"sempre"," via media query CSS (",[14,484,485],{},"fui-col-md-6",[14,487,488],{},"fui-col-lg-4",") — o browser aplica depois de já ter o viewport real, sem mismatch.",[476,491,492,493,496],{},"Contagem de itens, tamanhos e visibilidade condicionados a breakpoint ",[18,494,495],{},"nunca"," em JS de componente de listagem.",[476,498,499,502],{},[14,500,501],{},"perPage"," fixo (31, hoje) em vez de variar por viewport — quem precisa de menos itens por página troca via select, não via detecção automática.",[476,504,505],{},"Única exceção: interação puramente client-side (hover, foco, animação) pode usar JS normalmente — não afeta o HTML já pintado.",[34,507,509],{"id":508},"morfismos-o-fouc-que-o-ssg-cria-sozinho","Morfismos — o FOUC que o SSG cria sozinho",[10,511,512,513,515,516,519,520,523,524,527,528,531],{},"A lib ",[14,514,31],{}," foi desenhada pra instalar ",[18,517,518],{},"um"," morfismo por projeto — os seletores que ela emite são flat, sem prefixo (",[14,521,522],{},".fui-btn {}",", não ",[14,525,526],{},".fui-glass .fui-btn {}","). O ao.dev quebra essa premissa de propósito: carrega os cinco temas como arquivos CSS separados e troca via ",[14,529,530],{},"\u003Clink id=\"fui-theme\">",", pra dar ao visitante o seletor de morfismo em tempo real. Isso cria dois problemas que a lib, sozinha, nunca precisaria resolver.",[335,533,535],{"id":534},"problema-1-fouc-em-ssg","Problema 1 — FOUC em SSG",[10,537,538,541,542,545,546,549],{},[14,539,540],{},"useCookie"," não tem contexto de request durante o prerender: o HTML gerado sai sem classe de morfismo e sem ",[14,543,544],{},"\u003Clink>"," de tema. O Vue hidrata, ",[14,547,548],{},"useMorphism"," lê o cookie do visitante, adiciona a classe e o link — e nesse intervalo o usuário vê um flash de estilo sem tema nenhum.",[335,551,553],{"id":552},"problema-2-bleed-entre-temas","Problema 2 — bleed entre temas",[10,555,556,557,559],{},"Sem escopar os seletores, ",[14,558,522],{}," de um tema carregado vaza pro documento inteiro, mesmo que o morfismo ativo seja outro.",[335,561,563],{"id":562},"a-solucao-postcss-scoping-script-bloqueante","A solução — PostCSS scoping + script bloqueante",[10,565,566,569,570,366],{},[14,567,568],{},"scripts\u002Fbuild-themes.ts"," roda um passo de PostCSS depois de cada compilação SCSS, prefixando todo seletor com ",[14,571,572],{},"html.{tema}",[368,574,578],{"className":575,"code":576,"language":577,"meta":373,"style":373},"language-scss shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F* antes (emitido pela lib, pensada pra single-theme) *\u002F\n:root { --fui-color-primary: #7c3aed; }\n.fui-btn { padding: var(--fui-space-3); }\n\n\u002F* depois do PostCSS scoping, por tema *\u002F\nhtml.skeu { --fui-color-primary: #7c3aed; }\nhtml.skeu .fui-btn { padding: var(--fui-space-3); }\n","scss",[14,579,580,585,605,629,633,638,657],{"__ignoreMap":373},[377,581,582],{"class":379,"line":380},[377,583,584],{"class":383},"\u002F* antes (emitido pela lib, pensada pra single-theme) *\u002F\n",[377,586,587,590,592,596,599,602],{"class":379,"line":387},[377,588,589],{"class":417},":root",[377,591,414],{"class":413},[377,593,595],{"class":594},"soyes","--fui-color-primary",[377,597,598],{"class":413},": ",[377,600,601],{"class":417},"#7c3aed",[377,603,604],{"class":413},"; }\n",[377,606,607,610,612,615,617,620,623,626],{"class":379,"line":393},[377,608,609],{"class":417},".fui-btn",[377,611,414],{"class":413},[377,613,614],{"class":417},"padding",[377,616,598],{"class":413},[377,618,619],{"class":417},"var",[377,621,622],{"class":413},"(",[377,624,625],{"class":594},"--fui-space-3",[377,627,628],{"class":413},"); }\n",[377,630,631],{"class":379,"line":399},[377,632,403],{"emptyLinePlaceholder":402},[377,634,635],{"class":379,"line":406},[377,636,637],{"class":383},"\u002F* depois do PostCSS scoping, por tema *\u002F\n",[377,639,640,644,647,649,651,653,655],{"class":379,"line":439},[377,641,643],{"class":642},"sne4z","html",[377,645,646],{"class":417},".skeu",[377,648,414],{"class":413},[377,650,595],{"class":594},[377,652,598],{"class":413},[377,654,601],{"class":417},[377,656,604],{"class":413},[377,658,660,662,664,667,669,671,673,675,677,679],{"class":379,"line":659},7,[377,661,643],{"class":642},[377,663,646],{"class":417},[377,665,666],{"class":417}," .fui-btn",[377,668,414],{"class":413},[377,670,614],{"class":417},[377,672,598],{"class":413},[377,674,619],{"class":417},[377,676,622],{"class":413},[377,678,625],{"class":594},[377,680,628],{"class":413},[10,682,683,684,687,688,691,692,695,696,699,700,703,704,707,708,711],{},"E no ",[14,685,686],{},"\u003Chead>",", um script inline com ",[14,689,690],{},"tagPriority: 'critical'"," roda antes de qualquer paint: lê o cookie via regex, valida contra a lista de morfismos conhecidos, adiciona ",[14,693,694],{},"html.{morfismo}"," via ",[14,697,698],{},"classList.add"," (idempotente — no SSR a classe já está lá) e injeta um ",[14,701,702],{},"\u003Clink rel=\"preload\" as=\"style\">"," que vira ",[14,705,706],{},"stylesheet"," no ",[14,709,710],{},"onload",". No SSR o servidor já renderiza a classe certa — zero FOUC automaticamente. No SSG, sem cookie no HTML, o script assume o trabalho no client, antes do primeiro paint.",[713,714,715],"blockquote",{},[10,716,717],{},"Lighthouse testa sem cookies — o script é no-op, nenhum CSS de tema carrega, zero impacto em Performance.",[335,719,721,722,725],{"id":720},"o-bug-real-ordem-de-layer-num-reload-frio","O bug real — ordem de ",[14,723,724],{},"@layer"," num reload frio",[10,727,728,729,732,733,736],{},"Esse foi o bug mais difícil de diagnosticar do projeto inteiro. Trocar de morfismo funcionava perfeitamente client-side — ",[14,730,731],{},"setMorphism()",", sem reload. Mas um ",[18,734,735],{},"reload real"," com o cookie já setado causava bleed visual: border-radius, padding, gap, font-size e box-shadow trocando de valor de forma inconsistente, elemento por elemento.",[10,738,739,740,743,744,257,747,750,751,754,755,757,758,760,761,764],{},"A causa raiz: cada arquivo de tema gerado (",[14,741,742],{},"public\u002Fthemes\u002F{skeu,glass,neu,clay}.css",") só declarava blocos soltos de ",[14,745,746],{},"@layer fui.components {}",[14,748,749],{},"@layer fui.utilities {}"," — só o bundle base declarava a ordem canônica completa (",[14,752,753],{},"@layer fui.layout, fui.base, fui.components, fui.utilities;","). Numa corrida de rede real (o script inline injeta o ",[14,756,544],{}," do morfismo muito cedo), se o browser parseasse o ",[14,759,544],{}," do tema ",[18,762,763],{},"antes"," do bundle base, a ordem global de registro dos layers saía errada — e o reset cru do base passava a vencer utilitários e componentes do morfismo.",[10,766,767,768,771,772,774],{},"O fix: prefixar cada arquivo de tema gerado com a ",[18,769,770],{},"mesma"," declaração de ordem do base, antes de qualquer seletor. Não importa qual arquivo o browser parseia primeiro — a ordem correta é estabelecida de cara por qualquer um dos dois, e a segunda declaração idêntica é um no-op. Elimina a corrida de rede estruturalmente, sem depender de timing de ",[14,773,544],{},".",[34,776,778],{"id":777},"hidratacao-quando-o-proprio-modulo-de-seo-causava-o-mismatch","Hidratação — quando o próprio módulo de SEO causava o mismatch",[10,780,781,782,785,786,789],{},"Outro bug que só aparecia em SSG, nunca em dev: ",[14,783,784],{},"Hydration completed but contains mismatches.",", sempre no mesmo lugar — um nó de texto dentro de um ",[14,787,788],{},"\u003Cstyle>"," do body.",[10,791,792,793,796,797,799,800,803,804,807,808,810,811,813,814,816,817,820],{},"O culpado era ",[14,794,795],{},"nuxt-seo-utils"," (incluído via ",[14,798,141],{},"), que tem ",[14,801,802],{},"minify: true"," por padrão. O plugin Nitro de minificação de HTML comprime ",[18,805,806],{},"todos"," os ",[14,809,788],{}," inline durante o prerender — incluindo o ",[14,812,788],{}," que o Shiki\u002FMDC injeta no body com as CSS vars de syntax highlighting. Resultado: o HTML do servidor tinha CSS minificado, o payload Nuxt tinha CSS não-minificado (vindo direto do SQLite do ",[14,815,104],{},"), e o cliente re-renderizava o ",[14,818,819],{},"MDCRenderer"," usando o payload — CSS diferente, mismatch garantido.",[368,822,824],{"className":370,"code":823,"language":372,"meta":373,"style":373},"\u002F\u002F nuxt.config.ts\nseo: {\n  minify: { build: false }, \u002F\u002F só desativa a minificação de body no prerender —\n},                          \u002F\u002F runtime (head tags via unhead) continua ativo e não causa mismatch\n",[14,825,826,831,839,861],{"__ignoreMap":373},[377,827,828],{"class":379,"line":380},[377,829,830],{"class":383},"\u002F\u002F nuxt.config.ts\n",[377,832,833,836],{"class":379,"line":387},[377,834,835],{"class":594},"seo",[377,837,838],{"class":413},": {\n",[377,840,841,844,847,850,852,855,858],{"class":379,"line":393},[377,842,843],{"class":594},"  minify",[377,845,846],{"class":413},": { ",[377,848,849],{"class":594},"build",[377,851,598],{"class":413},[377,853,854],{"class":417},"false",[377,856,857],{"class":413}," }, ",[377,859,860],{"class":383},"\u002F\u002F só desativa a minificação de body no prerender —\n",[377,862,863,866],{"class":379,"line":399},[377,864,865],{"class":413},"},                          ",[377,867,868],{"class":383},"\u002F\u002F runtime (head tags via unhead) continua ativo e não causa mismatch\n",[10,870,871],{},"Debugar esse tipo de mismatch em produção (SSG) tem uma flag pouco conhecida do Vue 3.4+ que ativa mensagens detalhadas mesmo em build de produção:",[368,873,875],{"className":370,"code":874,"language":372,"meta":373,"style":373},"vite: {\n  define: {\n    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(true),\n  },\n}\n",[14,876,877,884,891,914,919],{"__ignoreMap":373},[377,878,879,882],{"class":379,"line":380},[377,880,881],{"class":594},"vite",[377,883,838],{"class":413},[377,885,886,889],{"class":379,"line":387},[377,887,888],{"class":594},"  define",[377,890,838],{"class":413},[377,892,893,896,898,901,903,906,908,911],{"class":379,"line":393},[377,894,895],{"class":594},"    __VUE_PROD_HYDRATION_MISMATCH_DETAILS__",[377,897,598],{"class":413},[377,899,900],{"class":417},"JSON",[377,902,774],{"class":413},[377,904,905],{"class":432},"stringify",[377,907,622],{"class":413},[377,909,910],{"class":417},"true",[377,912,913],{"class":413},"),\n",[377,915,916],{"class":379,"line":399},[377,917,918],{"class":413},"  },\n",[377,920,921],{"class":379,"line":406},[377,922,923],{"class":413},"}\n",[34,925,927],{"id":926},"auditoria-de-performance-numeros-reais-nao-estimativa","Auditoria de performance — números reais, não estimativa",[10,929,930],{},"Uma auditoria pré-deploy na home, usando Lighthouse a sério (não só rodando uma vez e aceitando o número), revelou uma série de problemas de sistema — coisas que valem pra qualquer página nova, não só a que foi testada.",[335,932,934],{"id":933},"compressao-que-o-preset-de-producao-nao-fazia-sozinho","Compressão que o preset de produção não fazia sozinho",[10,936,937,940,941,943,944,947,948,951,952,955,956,959,960,963],{},[14,938,939],{},"compressPublicAssets"," é ",[14,942,854],{}," por padrão no Nitro. Sem essa config, o preset ",[14,945,946],{},"node-server"," (o que roda em produção SSR) serve ",[14,949,950],{},"_nuxt\u002F*.css","\u002F",[14,953,954],{},"*.js","\u002Ffontes ",[18,957,958],{},"sem gzip nem brotli"," — chegamos a medir o mesmo arquivo CSS ",[18,961,962],{},"15× maior"," em SSR (1,28 MB) do que num servidor estático genérico (83 KB) servindo o mesmo build.",[368,965,967],{"className":370,"code":966,"language":372,"meta":373,"style":373},"nitro: {\n  compressPublicAssets: { gzip: true, brotli: true },\n},\n",[14,968,969,976,1002],{"__ignoreMap":373},[377,970,971,974],{"class":379,"line":380},[377,972,973],{"class":594},"nitro",[377,975,838],{"class":413},[377,977,978,981,983,986,988,990,992,995,997,999],{"class":379,"line":387},[377,979,980],{"class":594},"  compressPublicAssets",[377,982,846],{"class":413},[377,984,985],{"class":594},"gzip",[377,987,598],{"class":413},[377,989,910],{"class":417},[377,991,345],{"class":413},[377,993,994],{"class":594},"brotli",[377,996,598],{"class":413},[377,998,910],{"class":417},[377,1000,1001],{"class":413}," },\n",[377,1003,1004],{"class":379,"line":393},[377,1005,1006],{"class":413},"},\n",[335,1008,1010],{"id":1009},"fetch-de-componente-global-so-na-interacao-nunca-no-mount","Fetch de componente global só na interação, nunca no mount",[10,1012,1013,1014,1017,1018,1021,1022,1025,1026,1029,1030,1033,1034,1038,1039,1041],{},"O ",[14,1015,1016],{},"CommandPaletteComponent"," (busca via ⌘K) fica montado em ",[14,1019,1020],{},"app.vue"," em ",[18,1023,1024],{},"toda página",", mesmo fechado. Ele chamava ",[14,1027,1028],{},"useAsyncData(..., { server: false, lazy: true })"," — mas ",[14,1031,1032],{},"lazy: true"," só adia o ",[1035,1036,1037],"em",{},"render",", o fetch dispara imediatamente após a hidratação de qualquer jeito. Isso instanciava o motor de busca do ",[14,1040,104],{}," (SQLite-WASM client-side) em toda página, mesmo que o visitante nunca abrisse a busca: medimos 1–2,4s de scripting não-atribuído no bootup e 239–404 KiB de JS não usado.",[10,1043,1044,1045,707,1048,1051,1052,1055],{},"A correção: ",[14,1046,1047],{},"immediate: false",[14,1049,1050],{},"useAsyncData",", com ",[14,1053,1054],{},"execute()"," disparado manualmente só quando o usuário efetivamente abre a busca.",[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) aberta, com sugestões de projetos, posts e tags puxadas do conteúdo real antes mesmo de digitar",675,"\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-command-palette.png","Command Palette",1200,[1070,1071,1064],"figcaption",{},[335,1073,1075],{"id":1074},"purge-de-css-nao-usado","Purge de CSS não usado",[10,1077,512,1078,1080,1081,1084,1085,1088,1089,1092],{},[14,1079,31],{}," é utility-first — gera um conjunto fixo de classes em compile-time, sem noção de quais componentes ",[14,1082,1083],{},".vue"," deste projeto realmente usam o quê. ",[14,1086,1087],{},"@fullhuman\u002Fpostcss-purgecss",", configurado no ",[14,1090,1091],{},"nuxt.config.ts"," (nunca na lib), resolve isso como uma otimização de build equivalente ao tree-shaking de JS:",[39,1094,1095,1111],{},[42,1096,1097],{},[45,1098,1099,1102,1105,1108],{},[48,1100,1101],{},"Métrica",[48,1103,1104],{},"Antes",[48,1106,1107],{},"Depois",[48,1109,1110],{},"Redução",[55,1112,1113,1127,1141],{},[45,1114,1115,1118,1121,1124],{},[60,1116,1117],{},"Tamanho bruto",[60,1119,1120],{},"1,28 MB",[60,1122,1123],{},"143 KB",[60,1125,1126],{},"−89%",[45,1128,1129,1132,1135,1138],{},[60,1130,1131],{},"Tamanho gzip",[60,1133,1134],{},"~113 KB",[60,1136,1137],{},"~21,6 KB",[60,1139,1140],{},"−81%",[45,1142,1143,1146,1149,1152],{},[60,1144,1145],{},"Classes CSS",[60,1147,1148],{},"14.508",[60,1150,1151],{},"464",[60,1153,1154],{},"—",[10,1156,1157,1158,1161,1162,1165,1166,1169],{},"O risco real de um purge automático é classe montada dinamicamente — ",[14,1159,1160],{},"`fui-tag-${tech.color}`"," nunca aparece como string completa no código-fonte, então o scanner erraria e apagaria a classe. Antes de ativar, uma varredura por interpolação de template literal com prefixo ",[14,1163,1164],{},"fui-"," identificou os poucos casos reais, que entraram numa safelist por regex (",[14,1167,1168],{},"\u002F^fui-tag-\u002F","). O resultado foi validado com rebuild + screenshot + computed styles em três páginas antes de considerar fechado — nunca só \"o build passou sem erro\".",[335,1171,1173],{"id":1172},"prioridade-de-lcp-por-pagina","Prioridade de LCP por página",[10,1175,1176,1177,1180],{},"O elemento de LCP muda por página — na home é o primeiro card de morfismo, em outra pode ser uma imagem de capa. Só o elemento real de LCP recebe ",[14,1178,1179],{},"fetchpriority=\"high\""," + preload; aplicar em várias imagens dilui a prioridade e não ajuda em nada.",[368,1182,1185],{"className":1183,"code":1184,"language":643,"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-e-orcamento-nao-detalhe",[14,1231,442],{}," — payload é orçamento, não detalhe",[10,1234,1235,1236,1239,1240,1243,1244,1247,1248,1250,1251,1253],{},"O campo ",[14,1237,1238],{},"body"," de uma coleção ",[14,1241,1242],{},"type: 'page'"," é o AST completo do Markdown — 30 a 200 KB por documento. Uma query de listagem sem ",[14,1245,1246],{},".select()"," inclui o ",[14,1249,1238],{}," de ",[18,1252,806],{}," os posts, multiplicado por rota SSG.",[368,1255,1257],{"className":370,"code":1256,"language":372,"meta":373,"style":373},"\u002F\u002F ERRADO — body de todos os posts (~770 KB por rota)\nqueryCollection(blogCol.value).order('date', 'DESC').all()\n\n\u002F\u002F CORRETO — só frontmatter (~5–20 KB)\nqueryCollection(blogCol.value)\n  .select('path', 'title', 'shortTitle', 'description', 'date', 'readTime', 'tags', 'cover')\n  .order('date', 'DESC')\n  .all()\n",[14,1258,1259,1264,1290,1294,1299,1306,1356,1372],{"__ignoreMap":373},[377,1260,1261],{"class":379,"line":380},[377,1262,1263],{"class":383},"\u002F\u002F ERRADO — body de todos os posts (~770 KB por rota)\n",[377,1265,1266,1268,1270,1273,1275,1278,1280,1283,1286,1288],{"class":379,"line":387},[377,1267,442],{"class":432},[377,1269,445],{"class":413},[377,1271,1272],{"class":432},"order",[377,1274,622],{"class":413},[377,1276,1277],{"class":1205},"'date'",[377,1279,345],{"class":413},[377,1281,1282],{"class":1205},"'DESC'",[377,1284,1285],{"class":413},").",[377,1287,448],{"class":432},[377,1289,436],{"class":413},[377,1291,1292],{"class":379,"line":393},[377,1293,403],{"emptyLinePlaceholder":402},[377,1295,1296],{"class":379,"line":399},[377,1297,1298],{"class":383},"\u002F\u002F CORRETO — só frontmatter (~5–20 KB)\n",[377,1300,1301,1303],{"class":379,"line":406},[377,1302,442],{"class":432},[377,1304,1305],{"class":413},"(blogCol.value)\n",[377,1307,1308,1311,1314,1316,1319,1321,1324,1326,1329,1331,1334,1336,1338,1340,1343,1345,1348,1350,1353],{"class":379,"line":439},[377,1309,1310],{"class":413},"  .",[377,1312,1313],{"class":432},"select",[377,1315,622],{"class":413},[377,1317,1318],{"class":1205},"'path'",[377,1320,345],{"class":413},[377,1322,1323],{"class":1205},"'title'",[377,1325,345],{"class":413},[377,1327,1328],{"class":1205},"'shortTitle'",[377,1330,345],{"class":413},[377,1332,1333],{"class":1205},"'description'",[377,1335,345],{"class":413},[377,1337,1277],{"class":1205},[377,1339,345],{"class":413},[377,1341,1342],{"class":1205},"'readTime'",[377,1344,345],{"class":413},[377,1346,1347],{"class":1205},"'tags'",[377,1349,345],{"class":413},[377,1351,1352],{"class":1205},"'cover'",[377,1354,1355],{"class":413},")\n",[377,1357,1358,1360,1362,1364,1366,1368,1370],{"class":379,"line":659},[377,1359,1310],{"class":413},[377,1361,1272],{"class":432},[377,1363,622],{"class":413},[377,1365,1277],{"class":1205},[377,1367,345],{"class":413},[377,1369,1282],{"class":1205},[377,1371,1355],{"class":413},[377,1373,1375,1377,1379],{"class":379,"line":1374},8,[377,1376,1310],{"class":413},[377,1378,448],{"class":432},[377,1380,436],{"class":413},[10,1382,1383,1384,1021,1387,1389,1390,951,1393,1396,1397,1399,1400,1402,1403,1405,1406,1409,1410,1412,1413,1416,1417,1420,1421,1424,1425,1428,1429,1431,1432,774],{},"Duas regras derivadas: ",[18,1385,1386],{},"indexes",[14,1388,456],{}," para toda coluna usada em ",[14,1391,1392],{},".where()",[14,1394,1395],{},".order()"," (obrigatório em bancos como Cloudflare D1, onde cada linha lida é billing); e chaves de ",[14,1398,1050],{}," ",[18,1401,481],{}," delimitadas por ",[14,1404,366],{},", nunca ",[14,1407,1408],{},"-",", quando combinam prefixo literal com slug dinâmico — o cache de ",[14,1411,1050],{}," é global no app, e ",[14,1414,1415],{},"blog-${slug}"," colide estruturalmente entre um post ",[14,1418,1419],{},"\"prev-foo\""," e outro post ",[14,1422,1423],{},"\"foo\""," cuja chave também vira ",[14,1426,1427],{},"blog-prev-foo-...",". Com ",[14,1430,366],{}," como delimitador, a colisão é impossível, porque slug de conteúdo nunca contém ",[14,1433,366],{},[34,1435,1437],{"id":1436},"acessibilidade-o-que-o-ibm-equal-access-ensinou","Acessibilidade — o que o IBM Equal Access ensinou",[10,1439,1440,1441,1446],{},"Ciclos reais de auditoria com o ",[26,1442,1445],{"href":1443,"target":68,"rel":1444},"https:\u002F\u002Fwww.ibm.com\u002Fable\u002Ftoolkit\u002Ftools\u002F",[70],"IBM Equal Access Checker"," corrigiram padrões que pareciam corretos até serem testados de verdade:",[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 é ARIA 1.2",[14,1455,1456],{},"role=\"combobox\""," vai no ",[14,1459,1460],{},"\u003Cinput>",", nunca num ",[14,1463,1464],{},"\u003Cdiv>"," wrapper; o input precisa ser ",[14,1467,1468],{},"type=\"text\"",", porque ",[14,1471,1472],{},"type=\"search\""," já carrega role implícito ",[14,1475,1476],{},"searchbox"," e não aceita override.",[476,1479,1480,1486,1487,1490,1491,1494,1495,1498],{},[18,1481,1482,1485],{},[14,1483,1484],{},"\u003Cp>"," com bold dentro de link dispara heurística de heading falso"," — IBM interpreta ",[14,1488,1489],{},"\u003Cp class=\"fui-fw-bold\">"," dentro de ",[14,1492,1493],{},"\u003Ca>"," como possível heading mal-formado. Trocar por ",[14,1496,1497],{},"\u003Cspan class=\"fui-d-block\">"," preserva o visual sem acionar a regra, porque span é inline.",[476,1500,1501,1507,1508,1511,1512,1515],{},[18,1502,1503,1506],{},[14,1504,1505],{},"aria-labelledby"," nunca aponta pra elemento condicional ausente"," — se o alvo é ",[14,1509,1510],{},"v-if",", o valor cai pra ",[14,1513,1514],{},"undefined"," quando esse elemento não existe no DOM.",[476,1517,1518,1524,1525,1527],{},[18,1519,1520,1523],{},[14,1521,1522],{},"new Date()"," como fallback em template é hydration mismatch garantido"," — o timestamp do server nunca bate com o do client; a correção é ",[14,1526,1510],{}," pra omitir o elemento, nunca gerar uma data \"de qualquer jeito\".",[713,1529,1530],{},[10,1531,1532,1533,1536,1537,1540,1541,1544],{},"Nem todo achado do IBM é bug real. ",[14,1534,1535],{},"aria_keyboard_handler_exists"," dispara porque a ferramenta não enxerga ",[14,1538,1539],{},"@keydown"," do Vue (só ",[14,1542,1543],{},"onkeydown"," HTML puro) — falso positivo documentado, não \"corrigido\" às cegas.",[34,1546,1548,1549,1551],{"id":1547},"descoberta-por-ia-llmstxt-gerado-nunca-hardcoded","Descoberta por IA — ",[14,1550,206],{}," gerado, nunca hardcoded",[10,1553,1554,1555,1557,1558,72,1561,1566,1567,1569,1570,345,1572,1574],{},"O site expõe ",[14,1556,206],{}," e ",[14,1559,1560],{},"\u002Fllms-full.txt",[26,1562,1565],{"href":1563,"target":68,"rel":1564},"https:\u002F\u002Fllmstxt.org",[70],"padrão da comunidade",") via ",[14,1568,203],{}," — alternativa gratuita ao equivalente pago do Nuxt SEO PRO. Cada seção do arquivo é gerada a partir de uma coleção real (",[14,1571,344],{},[14,1573,354],{},"...), nunca de um template estático — o arquivo nunca fica desatualizado porque não existe versão \"escrita à mão\" pra ficar velha.",[10,1576,1577,1578,1581],{},"A mesma lib expõe o Markdown cru de cada página de conteúdo via ",[14,1579,1580],{},"\u002Fraw\u002F\u003Cpath>.md"," — é o link \"Ver como Markdown\" que aparece no topo de cada post de blog e case study, inclusive neste que você está lendo agora.",[1057,1583,1585,1590],{"className":1584},[1060],[1062,1586],{"alt":1587,"height":1065,"src":1588,"title":1589,"width":1068},"Cabeçalho de um case study com o link \"Ver como Markdown\" visível, expondo o conteúdo real via \u002Fraw\u002F\u003Cpath>.md","\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-ai-actions.png","Ver como Markdown",[1070,1591,1587],{},[34,1593,1595],{"id":1594},"o-checklist-de-finalizacao-cinco-fases-sempre-nessa-ordem","O checklist de finalização — cinco fases, sempre nessa ordem",[10,1597,1598],{},"Nenhuma página nova (ou refatoração significativa) é considerada pronta sem passar, em ordem, por:",[1600,1601,1602,1616,1624,1637,1643],"ol",{},[476,1603,1604,118,1607,345,1610,345,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],{},"Auditoria SSG",[14,1622,213],{}," + preview estático, WAVE (visual) + IBM Equal Access (relatório exportado) em PT-BR\u002FEN-US × light\u002Fdark",[476,1625,1626,118,1629,1632,1633,1636],{},[18,1627,1628],{},"Auditoria SSR",[14,1630,1631],{},"nuxt build"," + ",[14,1634,1635],{},"nuxi preview",", mesma dupla de ferramentas, mais atenção a hidratação e Schema.org no HTML inicial",[476,1638,1639,1642],{},[18,1640,1641],{},"Logs de build"," — payloads acima de 50 KB são investigados, não ignorados",[476,1644,1645,1648],{},[18,1646,1647],{},"Lighthouse"," — a melhor nota possível em Performance, Acessibilidade, Boas Práticas e SEO, com LCP, INP e CLS dentro dos limites recomendados pelo Core Web Vitals, nas duas dimensões (desktop\u002Fmobile) × dois temas (light\u002Fdark)",[10,1650,1013,1651,1654],{},[14,1652,1653],{},"throttlingMethod: simulate"," padrão do Lighthouse tem variância entre execuções — a mesma página, sem nenhuma mudança de código, pode sair um pouco diferente de uma rodada pra outra, dependendo da carga da máquina no momento do teste. Persigo a nota mais alta possível em toda página; a régua real de aceite é sempre o resultado mais recente rodado nas condições descritas acima.",[10,1656,1657],{},"Só depois de todos os cinco passos passarem uma página entra em commit. A regra existe porque Core Web Vitals, neste projeto, não é bônus — é requisito, no mesmo nível que lint ou typecheck.",[34,1659,1661],{"id":1660},"a-listagem-de-projetos-o-design-system-em-producao","A listagem de projetos — o design system em produção",[1057,1663,1665,1670],{"className":1664},[1060],[1062,1666],{"alt":1667,"height":1065,"src":1668,"title":1669,"width":1068},"Listagem de projetos em grid, com filtro por categoria e vários cards usando o mesmo componente de card","\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-projects-listing.png","Listagem de projetos",[1070,1671,1667],{},[10,1673,1674,1675,1677,1678,1681,1682,1685,1686,1689],{},"Essa é a página mais honesta pra ver o ",[14,1676,31],{}," funcionando em escala: dezenas de cards, todos o mesmo componente (",[14,1679,1680],{},"ProjectCardGridComponent","), cada um puxando ",[14,1683,1684],{},"cover"," do frontmatter real de cada projeto, com busca, filtro por categoria e contagem dinâmica — toda a lógica de listagem vive nos composables (",[14,1687,1688],{},"useProjectsListing","), zero duplicação entre a versão grid e a versão lista.",[34,1691,1693],{"id":1692},"nav-mobile-e-busca-global","Nav mobile e busca global",[10,1695,1696,1697,1699,1700,1703],{},"Fora do desktop, a navegação principal vira bottom tabs fixas (Início, Projetos, Buscar, Blog, Contato) mais um menu hambúrguer pro resto dos links — nunca decidido por ",[14,1698,467],{}," (pela mesma regra SSG-first já descrita), sempre por CSS puro reagindo ao viewport real do dispositivo. Isso é bem mais fácil de sentir ao vivo do que numa captura de tela redimensionada no desktop — se você está lendo isto num notebook, vale abrir ",[14,1701,1702],{},"ao.dev.br"," direto no celular pra ver a barra de tabs de verdade, no dispositivo de verdade.",[34,1705,1707],{"id":1706},"o-que-ainda-falta","O que ainda falta",[10,1709,1710],{},"Duas frentes seguem abertas, deliberadamente não resolvidas ainda:",[473,1712,1713,1734],{},[476,1714,1715,1727,1728,1730,1731,1733],{},[18,1716,1717,1718,1721,1722],{},"Migração ",[14,1719,1720],{},"@nuxtjs\u002Fmdc"," → ",[26,1723,1726],{"href":1724,"target":68,"rel":1725},"https:\u002F\u002Fcomark.dev",[70],"Comark"," — o ",[14,1729,1720],{}," está em deprecação confirmada pelo próprio time que o mantém, em favor do Comark (\"faster, AI-friendly, and no longer bound to Vue or Nuxt\"). O ",[14,1732,104],{}," ainda depende da versão antiga; a migração real só chega via update de dependência, com guia oficial de breaking changes — não faz sentido instalar o Comark manualmente antes disso.",[476,1735,1736,1741,1742,1745,1746,1749,1750,1753],{},[18,1737,1738],{},[14,1739,1740],{},"@nuxt\u002Fa11y"," — removido de ",[14,1743,1744],{},"modules:"," por bugs reais documentados (conflito de versão com ",[14,1747,1748],{},"@nuxt\u002Fdevtools-kit",", erros de RPC silenciados no próprio módulo), mas mantido no ",[14,1751,1752],{},"package.json"," — reativação é um ciclo de investigação dedicado, não um ajuste de última hora perto de deploy.",[10,1755,1756,1757,1759],{},"Este case study fecha um ciclo estranho de escrever: é o único projeto deste portfólio cujo \"cliente\" sou eu mesmo, cujo \"deploy\" ainda não aconteceu no momento em que escrevo isto, e cuja auditoria de qualidade se aplica ao próprio texto que você está lendo. O primeiro post do blog e o deploy em ",[14,1758,1702],{}," vêm logo depois deste 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":337,"depth":393,"text":338},{"id":460,"depth":387,"text":461},{"id":508,"depth":387,"text":509,"children":1772},[1773,1774,1775,1776],{"id":534,"depth":393,"text":535},{"id":552,"depth":393,"text":553},{"id":562,"depth":393,"text":563},{"id":720,"depth":393,"text":1777},"O bug real — ordem de @layer num reload frio",{"id":777,"depth":387,"text":778},{"id":926,"depth":387,"text":927,"children":1780},[1781,1782,1783,1784],{"id":933,"depth":393,"text":934},{"id":1009,"depth":393,"text":1010},{"id":1074,"depth":393,"text":1075},{"id":1172,"depth":393,"text":1173},{"id":1229,"depth":387,"text":1786},"queryCollection — payload é orçamento, não detalhe",{"id":1436,"depth":387,"text":1437},{"id":1547,"depth":387,"text":1789},"Descoberta por IA — \u002Fllms.txt gerado, nunca hardcoded",{"id":1594,"depth":387,"text":1595},{"id":1660,"depth":387,"text":1661},{"id":1692,"depth":387,"text":1693},{"id":1706,"depth":387,"text":1707},[1795,1796],"portfolio","open-source","Pessoal","\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-cover.png","2026-07-12","O portfólio que você está navegando agora — boilerplate de referência do ecossistema Nuxt 4, SSG-first, cinco morfismos de design trocáveis em tempo real, i18n completo e auditoria de performance\u002Facessibilidade documentada passo a passo.","md","\u002Fimages\u002Fpages\u002Fprojects\u002Fao-dev\u002Fao-dev-detail.png",[1804],{"label":1702,"url":1805},"https:\u002F\u002Fao.dev.br",{},21,"\u002Fprojects\u002Fao-dev","2026 – atual",[1811,1812],"Criador","Desenvolvedor",{"title":5,"description":1800},[71,1815,92,104,117,141,166,1816,1817],"Vue 3","fui-content (SCSS)","Docker + Traefik","in-development","projects\u002Fao-dev",[71,92,1821,110,1822,1823,1824],"Nuxt Content","SSG","Performance","Acessibilidade","eO0n_tPJN195UJ-tySPidf64IxzpJUuaDFHLBSXDPG8",{"path":28,"title":31,"order":1827},20,null,1784312150102]