[{"data":1,"prerenderedAt":2171},["ShallowReactive",2],{"project:\u002Fleve-mix:pt-br":3,"project:prev:\u002Fleve-mix:pt-br":2167,"project:next:\u002Fleve-mix:pt-br":2168},{"id":4,"title":5,"body":6,"category":2140,"company":2142,"cover":2143,"date":2144,"description":2145,"extension":2146,"featured":590,"image":2147,"isPublic":590,"links":2148,"meta":2152,"navigation":590,"order":268,"path":2153,"period":2154,"role":2155,"seo":2157,"stack":2158,"status":2160,"stem":2161,"tags":2162,"__hash__":2166},"projects_ptBR\u002Fprojects\u002Fleve-mix.md","Leve Mix",{"type":7,"value":8,"toc":2112},"minimark",[9,22,38,43,145,157,161,164,171,232,235,239,254,514,521,525,536,647,650,681,690,702,724,833,847,851,854,906,929,1216,1231,1235,1246,1460,1471,1475,1481,1533,1540,1552,1562,1697,1711,1715,1733,1737,1769,1773,1780,1791,1795,1808,1812,1821,1824,1829,1836,1840,1851,1856,1866,1876,1880,1889,1915,1919,1929,1932,1936,1945,1948,1978,2006,2010,2019,2033,2040,2044,2047,2074,2078,2081,2105,2108],[10,11,12,13,21],"p",{},"A Leve Mix é uma indústria de terceirização (outsourcing) de produção e envase de alimentos — café, chocolate em pó, colágeno, chá, tempero, farináceo — pra quem quer lançar marca própria sem construir fábrica do zero. Faz parte do Grupo Romanus, com mais de 26 anos de mercado, sediada em Campo Largo, no Paraná. Peguei esse projeto como freela, em parceria com a ",[14,15,20],"a",{"href":16,"rel":17,"target":19},"https:\u002F\u002Fbeacons.ai\u002Fal2.studio",[18],"nofollow","_blank","al2.studio"," — eles fizeram a ponte entre a equipe técnica e o cliente, papel parecido com o de um PO. É o projeto mais completo que já documentei aqui: zero framework, zero dependência de build de terceiro, um CMS que eu inventei do zero em JavaScript puro, e um sistema de theming dinâmico por cor que a maioria de quem visita o site nunca vai perceber que existe.",[23,24,27,35],"figure",{"className":25},[26],"rehype-figure",[28,29],"img",{"alt":30,"height":31,"src":32,"title":33,"width":34},"Home da Leve Mix, com hero em vídeo e seção de números de impacto",675,"\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-home.png","Home",1200,[36,37,30],"figcaption",{},[39,40,42],"h2",{"id":41},"por-baixo-do-capo","Por baixo do capô",[44,45,46,59],"table",{},[47,48,49],"thead",{},[50,51,52,56],"tr",{},[53,54,55],"th",{},"Camada",[53,57,58],{},"Tecnologia",[60,61,62,76,89,108,120,132],"tbody",{},[50,63,64,68],{},[65,66,67],"td",{},"Estrutura",[65,69,70,75],{},[14,71,74],{"href":72,"rel":73,"target":19},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTML",[18],"HTML5"," semântico, uma página estática por rota",[50,77,78,81],{},[65,79,80],{},"Estilo",[65,82,83,88],{},[14,84,87],{"href":85,"rel":86,"target":19},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS",[18],"CSS3"," — neomorfismo, variáveis CSS, Grid\u002FFlexbox",[50,90,91,94],{},[65,92,93],{},"Interatividade",[65,95,96,101,102,107],{},[14,97,100],{"href":98,"rel":99,"target":19},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FJavaScript",[18],"JavaScript Vanilla",", ",[14,103,106],{"href":104,"rel":105,"target":19},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FWeb_Components",[18],"Web Components"," nativos",[50,109,110,113],{},[65,111,112],{},"Carrossel\u002FLightbox",[65,114,115],{},[14,116,119],{"href":117,"rel":118,"target":19},"https:\u002F\u002Fswiperjs.com\u002Fget-started",[18],"Swiper.js",[50,121,122,125],{},[65,123,124],{},"Offline\u002FPWA",[65,126,127],{},[14,128,131],{"href":129,"rel":130,"target":19},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FService_Worker_API",[18],"Service Worker API",[50,133,134,137],{},[65,135,136],{},"Hosting",[65,138,139,144],{},[14,140,143],{"href":141,"rel":142,"target":19},"https:\u002F\u002Fvercel.com\u002Fdocs",[18],"Vercel",", sem build step remoto",[10,146,147,148,152,153,156],{},"Sem Nuxt, sem React, sem Vue, sem bundler. ",[149,150,151],"code",{},"npm run build"," só roda um script Node local (",[149,154,155],{},"build.js",") que gera cache-busting e injeta imagens antes do commit — o deploy em si é puramente estático.",[39,158,160],{"id":159},"de-cms-puro-pra-abordagem-hibrida","De CMS puro pra abordagem híbrida",[10,162,163],{},"A primeira versão do site renderizava as páginas de produto inteiramente via JavaScript: um Web Component lia o slug da URL, buscava o JSON correspondente e montava o DOM em runtime. Funcionava, mas o Google via uma página quase vazia até o JS rodar — ruim pra SEO, ruim pra Core Web Vitals.",[10,165,166,167,170],{},"A decisão que documentei num arquivo interno na época (",[149,168,169],{},"ABORDAGEM-HIBRIDA.md",") foi migrar pra um modelo híbrido: HTML estático com todo o SEO (title, meta tags, Open Graph, canonical) já escrito no arquivo, e o Web Component só preenche o conteúdo dinâmico por cima.",[44,172,173,186],{},[47,174,175],{},[50,176,177,180,183],{},[53,178,179],{},"Aspecto",[53,181,182],{},"CMS puro (antes)",[53,184,185],{},"Abordagem híbrida (depois)",[60,187,188,199,210,221],{},[50,189,190,193,196],{},[65,191,192],{},"SEO",[65,194,195],{},"Meta tags via JS — pior",[65,197,198],{},"Meta tags no HTML — indexação perfeita",[50,200,201,204,207],{},[65,202,203],{},"Performance",[65,205,206],{},"JS necessário pra renderizar",[65,208,209],{},"HTML já visível no primeiro paint",[50,211,212,215,218],{},[65,213,214],{},"Manutenção",[65,216,217],{},"Só editar JSON",[65,219,220],{},"HTML + JSON — um pouco mais de trabalho",[50,222,223,226,229],{},[65,224,225],{},"Escalabilidade",[65,227,228],{},"Trivial",[65,230,231],{},"Precisa de um template HTML por página nova",[10,233,234],{},"Trocar facilidade de manutenção por SEO\u002Fperformance valeu a pena — cada página de produto tem sua própria URL, título e Open Graph reais, sem depender de nenhum framework de SSR pra isso.",[39,236,238],{"id":237},"o-cms-dados-em-json-motor-em-js-vanilla","O CMS: dados em JSON, motor em JS vanilla",[10,240,241,242,245,246,249,250,253],{},"Cada produto e cada embalagem vira um arquivo JSON dentro de ",[149,243,244],{},"CMS\u002Fslugs\u002F{producao|embalagens}\u002F{slug}.json",". Um único Web Component genérico, o ",[149,247,248],{},"SlugPage",", lê esse arquivo pelo slug da URL e preenche o template — o mesmo componente atende produção e embalagens, só muda a ",[149,251,252],{},"category"," detectada na URL. Exemplo real, o JSON do produto \"Chás\":",[255,256,262],"pre",{"className":257,"code":258,"filename":259,"language":260,"meta":261,"style":261},"language-json shiki shiki-themes github-light-high-contrast github-dark-high-contrast","{\n  \"id\": \"chas\",\n  \"slug\": \"chas\",\n  \"category\": \"producao\",\n  \"meta\": {\n    \"title\": \"Chás Solúveis e a Granel | LeveMix\",\n    \"description\": \"Terceirização de produção de chás solúveis e a granel...\",\n    \"keywords\": \"chá solúvel, chá a granel, chá matcha, chás funcionais\",\n    \"ogImage\": \"\u002Fassets\u002Fimages\u002Fprodutos\u002Fchas-01.webp\"\n  },\n  \"title\": \"Chás Solúveis e a Granel\",\n  \"descriptionShort\": \"Soluções para o mercado de produtos naturais e saudáveis\",\n  \"variants\": [\n    { \"title\": \"Chás Solúveis\", \"description\": \"Chás em formato solúvel, práticos e convenientes.\" },\n    { \"title\": \"Chás a Granel\", \"description\": \"Chás em formato a granel para embalagem personalizada.\" }\n  ],\n  \"benefits\": [\n    \"Formatos solúveis e a granel\",\n    \"Chás funcionais disponíveis\",\n    \"Produção em escala\"\n  ]\n}\n","CMS\u002Fslugs\u002Fproducao\u002Fchas.json","json","",[149,263,264,273,290,302,315,324,337,350,363,374,380,393,406,415,442,466,472,480,488,496,502,508],{"__ignoreMap":261},[265,266,269],"span",{"class":267,"line":268},"line",1,[265,270,272],{"class":271},"suds8","{\n",[265,274,276,280,283,287],{"class":267,"line":275},2,[265,277,279],{"class":278},"sne4z","  \"id\"",[265,281,282],{"class":271},": ",[265,284,286],{"class":285},"sT6z2","\"chas\"",[265,288,289],{"class":271},",\n",[265,291,293,296,298,300],{"class":267,"line":292},3,[265,294,295],{"class":278},"  \"slug\"",[265,297,282],{"class":271},[265,299,286],{"class":285},[265,301,289],{"class":271},[265,303,305,308,310,313],{"class":267,"line":304},4,[265,306,307],{"class":278},"  \"category\"",[265,309,282],{"class":271},[265,311,312],{"class":285},"\"producao\"",[265,314,289],{"class":271},[265,316,318,321],{"class":267,"line":317},5,[265,319,320],{"class":278},"  \"meta\"",[265,322,323],{"class":271},": {\n",[265,325,327,330,332,335],{"class":267,"line":326},6,[265,328,329],{"class":278},"    \"title\"",[265,331,282],{"class":271},[265,333,334],{"class":285},"\"Chás Solúveis e a Granel | LeveMix\"",[265,336,289],{"class":271},[265,338,340,343,345,348],{"class":267,"line":339},7,[265,341,342],{"class":278},"    \"description\"",[265,344,282],{"class":271},[265,346,347],{"class":285},"\"Terceirização de produção de chás solúveis e a granel...\"",[265,349,289],{"class":271},[265,351,353,356,358,361],{"class":267,"line":352},8,[265,354,355],{"class":278},"    \"keywords\"",[265,357,282],{"class":271},[265,359,360],{"class":285},"\"chá solúvel, chá a granel, chá matcha, chás funcionais\"",[265,362,289],{"class":271},[265,364,366,369,371],{"class":267,"line":365},9,[265,367,368],{"class":278},"    \"ogImage\"",[265,370,282],{"class":271},[265,372,373],{"class":285},"\"\u002Fassets\u002Fimages\u002Fprodutos\u002Fchas-01.webp\"\n",[265,375,377],{"class":267,"line":376},10,[265,378,379],{"class":271},"  },\n",[265,381,383,386,388,391],{"class":267,"line":382},11,[265,384,385],{"class":278},"  \"title\"",[265,387,282],{"class":271},[265,389,390],{"class":285},"\"Chás Solúveis e a Granel\"",[265,392,289],{"class":271},[265,394,396,399,401,404],{"class":267,"line":395},12,[265,397,398],{"class":278},"  \"descriptionShort\"",[265,400,282],{"class":271},[265,402,403],{"class":285},"\"Soluções para o mercado de produtos naturais e saudáveis\"",[265,405,289],{"class":271},[265,407,409,412],{"class":267,"line":408},13,[265,410,411],{"class":278},"  \"variants\"",[265,413,414],{"class":271},": [\n",[265,416,418,421,424,426,429,431,434,436,439],{"class":267,"line":417},14,[265,419,420],{"class":271},"    { ",[265,422,423],{"class":278},"\"title\"",[265,425,282],{"class":271},[265,427,428],{"class":285},"\"Chás Solúveis\"",[265,430,101],{"class":271},[265,432,433],{"class":278},"\"description\"",[265,435,282],{"class":271},[265,437,438],{"class":285},"\"Chás em formato solúvel, práticos e convenientes.\"",[265,440,441],{"class":271}," },\n",[265,443,445,447,449,451,454,456,458,460,463],{"class":267,"line":444},15,[265,446,420],{"class":271},[265,448,423],{"class":278},[265,450,282],{"class":271},[265,452,453],{"class":285},"\"Chás a Granel\"",[265,455,101],{"class":271},[265,457,433],{"class":278},[265,459,282],{"class":271},[265,461,462],{"class":285},"\"Chás em formato a granel para embalagem personalizada.\"",[265,464,465],{"class":271}," }\n",[265,467,469],{"class":267,"line":468},16,[265,470,471],{"class":271},"  ],\n",[265,473,475,478],{"class":267,"line":474},17,[265,476,477],{"class":278},"  \"benefits\"",[265,479,414],{"class":271},[265,481,483,486],{"class":267,"line":482},18,[265,484,485],{"class":285},"    \"Formatos solúveis e a granel\"",[265,487,289],{"class":271},[265,489,491,494],{"class":267,"line":490},19,[265,492,493],{"class":285},"    \"Chás funcionais disponíveis\"",[265,495,289],{"class":271},[265,497,499],{"class":267,"line":498},20,[265,500,501],{"class":285},"    \"Produção em escala\"\n",[265,503,505],{"class":267,"line":504},21,[265,506,507],{"class":271},"  ]\n",[265,509,511],{"class":267,"line":510},22,[265,512,513],{"class":271},"}\n",[10,515,516,517,520],{},"Foi esse sistema — um CMS caseiro movido a JSON, sem banco de dados, sem painel administrativo — que me deu a ideia de como eu queria estruturar conteúdo dinâmico no meu próprio portfólio, o ao.dev. Lá eu uso o ",[149,518,519],{},"@nuxt\u002Fcontent"," de verdade em vez de reinventar a roda, mas a ideia raiz — conteúdo como dado versionado no repositório, não escondido atrás de um CMS externo — nasceu aqui.",[39,522,524],{"id":523},"cms-loader-cache-busca-em-tempo-real-e-slider-tudo-do-mesmo-json","CMS Loader: cache, busca em tempo real e slider, tudo do mesmo JSON",[10,526,527,528,531,532,535],{},"O ",[149,529,530],{},"cms-loader.js"," é a classe central que qualquer componente usa pra falar com o CMS — nunca um ",[149,533,534],{},"fetch"," solto espalhado pelo código:",[255,537,541],{"className":538,"code":539,"language":540,"meta":261,"style":261},"language-js shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F\u002F Carregar um item específico\nconst item = await window.cmsLoader.loadItem('producao', 'chocolate')\n\n\u002F\u002F Carregar todos os itens de uma categoria (pra busca e sliders)\nconst allItems = await window.cmsLoader.loadAllItems()\n\n\u002F\u002F Buscar por termo\nconst results = await window.cmsLoader.search('chocolate')\n","js",[149,542,543,549,586,592,597,616,620,625],{"__ignoreMap":261},[265,544,545],{"class":267,"line":268},[265,546,548],{"class":547},"sLBg1","\u002F\u002F Carregar um item específico\n",[265,550,551,555,559,562,565,568,572,575,578,580,583],{"class":267,"line":275},[265,552,554],{"class":553},"sHUrx","const",[265,556,558],{"class":557},"s-5SL"," item",[265,560,561],{"class":553}," =",[265,563,564],{"class":553}," await",[265,566,567],{"class":271}," window.cmsLoader.",[265,569,571],{"class":570},"sKwhi","loadItem",[265,573,574],{"class":271},"(",[265,576,577],{"class":285},"'producao'",[265,579,101],{"class":271},[265,581,582],{"class":285},"'chocolate'",[265,584,585],{"class":271},")\n",[265,587,588],{"class":267,"line":292},[265,589,591],{"emptyLinePlaceholder":590},true,"\n",[265,593,594],{"class":267,"line":304},[265,595,596],{"class":547},"\u002F\u002F Carregar todos os itens de uma categoria (pra busca e sliders)\n",[265,598,599,601,604,606,608,610,613],{"class":267,"line":317},[265,600,554],{"class":553},[265,602,603],{"class":557}," allItems",[265,605,561],{"class":553},[265,607,564],{"class":553},[265,609,567],{"class":271},[265,611,612],{"class":570},"loadAllItems",[265,614,615],{"class":271},"()\n",[265,617,618],{"class":267,"line":326},[265,619,591],{"emptyLinePlaceholder":590},[265,621,622],{"class":267,"line":339},[265,623,624],{"class":547},"\u002F\u002F Buscar por termo\n",[265,626,627,629,632,634,636,638,641,643,645],{"class":267,"line":352},[265,628,554],{"class":553},[265,630,631],{"class":557}," results",[265,633,561],{"class":553},[265,635,564],{"class":553},[265,637,567],{"class":271},[265,639,640],{"class":570},"search",[265,642,574],{"class":271},[265,644,582],{"class":285},[265,646,585],{"class":271},[10,648,649],{},"O mesmo JSON de produto alimenta três coisas diferentes, sem duplicar dado em lugar nenhum:",[651,652,653,665,672],"ul",{},[654,655,527,656,660,661,664],"li",{},[657,658,659],"strong",{},"slider da home"," (",[149,662,663],{},"\u003Cswiper-showcase category=\"producao\" exclude-slug=\"chocolate\" color-scheme=\"secondary\">",") — busca todos os itens da categoria, exclui o item atual quando necessário, renderiza os cards",[654,666,667,668,671],{},"A ",[657,669,670],{},"busca em tempo real"," — o loader concatena título, descrição curta, descrição longa, meta description, keywords, benefícios e nome\u002Fdescrição de cada variante num único texto pesquisável, e filtra conforme o usuário digita, sem round-trip nenhum pro servidor",[654,673,667,674,677,678,680],{},[657,675,676],{},"página do slug"," — o ",[149,679,248],{}," component já coberto acima",[10,682,683,684,660,686,689],{},"Uma limitação real que ficou registrada no próprio código: pra um slug novo aparecer na busca e nos sliders, o slug precisa entrar manualmente numa lista conhecida dentro do ",[149,685,530],{},[149,687,688],{},"getKnownSlugs()",") — o carregamento não descobre arquivos novos sozinho, porque não existe listagem de diretório em produção estática. Passo simples, mas é o tipo de coisa que só descobre na prática, não lendo a documentação de longe.",[39,691,693,694,697,698,701],{"id":692},"config-idempotente-de-env-pra-atributo-data-bind-no-html","Config idempotente: de ",[149,695,696],{},".env"," pra atributo ",[149,699,700],{},"data-bind"," no HTML",[10,703,704,705,707,708,711,712,715,716,719,720,723],{},"Dado sensível de contato (telefone, e-mail, redes sociais) não podia ficar hardcoded no HTML nem espalhado em cada Web Component. A solução: um ",[149,706,696],{}," na raiz, processado por ",[149,709,710],{},"build-config.js"," durante o build, que gera ",[149,713,714],{},"js\u002Fapp-conf.js"," (esse sim gitignored) expondo tudo em ",[149,717,718],{},"window.__LEVE_CTX",". Um segundo script, ",[149,721,722],{},"dom-binder.js",", injeta esses valores direto no HTML através de atributos customizados:",[255,725,729],{"className":726,"code":727,"language":728,"meta":261,"style":261},"language-html shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u003C!-- Texto simples -->\n\u003Cspan data-bind=\"CONTACT_PHONE_FORMATTED\">\u003C\u002Fspan>\n\n\u003C!-- Link com template -->\n\u003Ca data-bind-href=\"tel:+55{CONTACT_PHONE}\">Ligar\u003C\u002Fa>\n\n\u003C!-- Até dentro de JSON-LD -->\n\u003Cscript type=\"application\u002Fld+json\" data-bind-json>\n{ \"name\": \"{COMPANY_NAME}\", \"url\": \"{BASE_URL}\" }\n\u003C\u002Fscript>\n","html",[149,730,731,736,760,764,769,790,794,799,819,824],{"__ignoreMap":261},[265,732,733],{"class":267,"line":268},[265,734,735],{"class":547},"\u003C!-- Texto simples -->\n",[265,737,738,741,743,746,749,752,755,757],{"class":267,"line":275},[265,739,740],{"class":271},"\u003C",[265,742,265],{"class":278},[265,744,745],{"class":557}," data-bind",[265,747,748],{"class":271},"=",[265,750,751],{"class":285},"\"CONTACT_PHONE_FORMATTED\"",[265,753,754],{"class":271},">\u003C\u002F",[265,756,265],{"class":278},[265,758,759],{"class":271},">\n",[265,761,762],{"class":267,"line":292},[265,763,591],{"emptyLinePlaceholder":590},[265,765,766],{"class":267,"line":304},[265,767,768],{"class":547},"\u003C!-- Link com template -->\n",[265,770,771,773,775,778,780,783,786,788],{"class":267,"line":317},[265,772,740],{"class":271},[265,774,14],{"class":278},[265,776,777],{"class":557}," data-bind-href",[265,779,748],{"class":271},[265,781,782],{"class":285},"\"tel:+55{CONTACT_PHONE}\"",[265,784,785],{"class":271},">Ligar\u003C\u002F",[265,787,14],{"class":278},[265,789,759],{"class":271},[265,791,792],{"class":267,"line":326},[265,793,591],{"emptyLinePlaceholder":590},[265,795,796],{"class":267,"line":339},[265,797,798],{"class":547},"\u003C!-- Até dentro de JSON-LD -->\n",[265,800,801,803,806,809,811,814,817],{"class":267,"line":352},[265,802,740],{"class":271},[265,804,805],{"class":278},"script",[265,807,808],{"class":557}," type",[265,810,748],{"class":271},[265,812,813],{"class":285},"\"application\u002Fld+json\"",[265,815,816],{"class":557}," data-bind-json",[265,818,759],{"class":271},[265,820,821],{"class":267,"line":365},[265,822,823],{"class":271},"{ \"name\": \"{COMPANY_NAME}\", \"url\": \"{BASE_URL}\" }\n",[265,825,826,829,831],{"class":267,"line":376},[265,827,828],{"class":271},"\u003C\u002F",[265,830,805],{"class":278},[265,832,759],{"class":271},[10,834,835,836,839,840,842,843,846],{},"O processo é ",[657,837,838],{},"idempotente"," de propósito: rodar o build várias vezes seguidas nunca corrompe o ",[149,841,696],{}," fonte nem duplica binding — ",[149,844,845],{},"app-conf.js"," é sempre regenerado do zero a partir da mesma fonte de verdade. Cada Web Component (como o footer, já mostrado abaixo) ainda mantém um fallback hardcoded pros mesmos valores, então nada quebra visualmente se a env var não for injetada por algum motivo.",[39,848,850],{"id":849},"um-sistema-de-cor-por-slug-que-ficou-pronto-e-nunca-foi-usado","Um sistema de cor por slug que ficou pronto e nunca foi usado",[10,852,853],{},"Por padrão, cada categoria de página tem sua própria cor de destaque, com os tokens de design já definidos globalmente:",[255,855,859],{"className":856,"code":857,"language":858,"meta":261,"style":261},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast","--primary-color: #792a47;          \u002F* vinho — cor principal, Embalagens *\u002F\n--primary-color-emphasis: #f5ebe8;\n--secondary-color: #5a7c2e;        \u002F* verde — cor secundária, Produção *\u002F\n--secondary-color-emphasis: #e6f2d3;\n","css",[149,860,861,876,884,898],{"__ignoreMap":261},[265,862,863,866,870,873],{"class":267,"line":268},[265,864,865],{"class":271},"--primary-color: ",[265,867,869],{"class":868},"sh5d0","#792a47",[265,871,872],{"class":271},";          ",[265,874,875],{"class":547},"\u002F* vinho — cor principal, Embalagens *\u002F\n",[265,877,878,881],{"class":267,"line":275},[265,879,880],{"class":271},"--primary-color-emphasis: ",[265,882,883],{"class":868},"#f5ebe8;\n",[265,885,886,889,892,895],{"class":267,"line":292},[265,887,888],{"class":271},"--secondary-color: ",[265,890,891],{"class":868},"#5a7c2e",[265,893,894],{"class":271},";        ",[265,896,897],{"class":547},"\u002F* verde — cor secundária, Produção *\u002F\n",[265,899,900,903],{"class":267,"line":304},[265,901,902],{"class":271},"--secondary-color-emphasis: ",[265,904,905],{"class":868},"#e6f2d3;\n",[10,907,908,909,912,913,916,917,920,921,924,925,928],{},"Isso já resolvia o caso de uso real. Mas construí um passo a mais: qualquer JSON de slug pode declarar um campo ",[149,910,911],{},"color"," com um hex customizado — três formatos aceitos (",[149,914,915],{},"#FF6B35"," de 6 dígitos, ",[149,918,919],{},"#F6B"," de 3 dígitos que expande automaticamente; ",[149,922,923],{},"rgb()"," e hex sem ",[149,926,927],{},"#"," são rejeitados) — e a página inteira se retema em cima dele, sem precisar escrever uma linha de CSS nova. A cor customizada se propaga por oito áreas diferentes: hero, cards de variante, seção de benefícios, CTA, swiper da galeria, footer (só em páginas slug), título da seção \"Outros Produtos\"\u002F\"Outras Embalagens\" e o próprio swiper-showcase.",[255,930,933],{"className":538,"code":931,"filename":932,"language":540,"meta":261,"style":261},"applyCustomColor(color) {\n  if (!\u002F^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$\u002F.test(color.trim())) {\n    console.warn('Invalid color format. Expected hex color (e.g., #792a47)')\n    return\n  }\n\n  const [r, g, b] = hexToRgb(color)\n  \u002F\u002F Versão clara pra hover\u002Fênfase: 95% branco + 5% da cor original\n  const emphasis = blend([r, g, b], [255, 255, 255], 0.95)\n\n  const style = document.createElement('style')\n  style.textContent = `\n    :root {\n      --custom-color: ${color};\n      --custom-color-emphasis: rgb(${emphasis.join(',')});\n    }\n    .slug-hero-custom { background: radial-gradient(var(--custom-color), black) !important; }\n    .slug-variant-card-custom h3 { color: var(--custom-color) !important; }\n    .slug-cta-custom .btn-primary { border-color: var(--custom-color) !important; }\n    \u002F* + uma dezena de seletores de swiper, benefícios e CTA *\u002F\n  `\n  document.head.appendChild(style)\n}\n","js\u002Fcomponents\u002Fslug-page.js",[149,934,935,943,996,1011,1016,1021,1025,1057,1062,1096,1100,1122,1132,1137,1147,1170,1175,1180,1185,1190,1195,1200,1211],{"__ignoreMap":261},[265,936,937,940],{"class":267,"line":268},[265,938,939],{"class":570},"applyCustomColor",[265,941,942],{"class":271},"(color) {\n",[265,944,945,948,950,953,956,959,962,965,968,970,973,976,979,981,984,987,990,993],{"class":267,"line":275},[265,946,947],{"class":553},"  if",[265,949,660],{"class":271},[265,951,952],{"class":553},"!",[265,954,955],{"class":285},"\u002F",[265,957,958],{"class":553},"^",[265,960,961],{"class":285},"#(",[265,963,964],{"class":557},"[A-Fa-f0-9]",[265,966,967],{"class":553},"{6}|",[265,969,964],{"class":557},[265,971,972],{"class":553},"{3}",[265,974,975],{"class":285},")",[265,977,978],{"class":553},"$",[265,980,955],{"class":285},[265,982,983],{"class":271},".",[265,985,986],{"class":570},"test",[265,988,989],{"class":271},"(color.",[265,991,992],{"class":570},"trim",[265,994,995],{"class":271},"())) {\n",[265,997,998,1001,1004,1006,1009],{"class":267,"line":292},[265,999,1000],{"class":271},"    console.",[265,1002,1003],{"class":570},"warn",[265,1005,574],{"class":271},[265,1007,1008],{"class":285},"'Invalid color format. Expected hex color (e.g., #792a47)'",[265,1010,585],{"class":271},[265,1012,1013],{"class":267,"line":304},[265,1014,1015],{"class":553},"    return\n",[265,1017,1018],{"class":267,"line":317},[265,1019,1020],{"class":271},"  }\n",[265,1022,1023],{"class":267,"line":326},[265,1024,591],{"emptyLinePlaceholder":590},[265,1026,1027,1030,1033,1036,1038,1041,1043,1046,1049,1051,1054],{"class":267,"line":339},[265,1028,1029],{"class":553},"  const",[265,1031,1032],{"class":271}," [",[265,1034,1035],{"class":557},"r",[265,1037,101],{"class":271},[265,1039,1040],{"class":557},"g",[265,1042,101],{"class":271},[265,1044,1045],{"class":557},"b",[265,1047,1048],{"class":271},"] ",[265,1050,748],{"class":553},[265,1052,1053],{"class":570}," hexToRgb",[265,1055,1056],{"class":271},"(color)\n",[265,1058,1059],{"class":267,"line":352},[265,1060,1061],{"class":547},"  \u002F\u002F Versão clara pra hover\u002Fênfase: 95% branco + 5% da cor original\n",[265,1063,1064,1066,1069,1071,1074,1077,1080,1082,1084,1086,1088,1091,1094],{"class":267,"line":365},[265,1065,1029],{"class":553},[265,1067,1068],{"class":557}," emphasis",[265,1070,561],{"class":553},[265,1072,1073],{"class":570}," blend",[265,1075,1076],{"class":271},"([r, g, b], [",[265,1078,1079],{"class":557},"255",[265,1081,101],{"class":271},[265,1083,1079],{"class":557},[265,1085,101],{"class":271},[265,1087,1079],{"class":557},[265,1089,1090],{"class":271},"], ",[265,1092,1093],{"class":557},"0.95",[265,1095,585],{"class":271},[265,1097,1098],{"class":267,"line":376},[265,1099,591],{"emptyLinePlaceholder":590},[265,1101,1102,1104,1107,1109,1112,1115,1117,1120],{"class":267,"line":382},[265,1103,1029],{"class":553},[265,1105,1106],{"class":557}," style",[265,1108,561],{"class":553},[265,1110,1111],{"class":271}," document.",[265,1113,1114],{"class":570},"createElement",[265,1116,574],{"class":271},[265,1118,1119],{"class":285},"'style'",[265,1121,585],{"class":271},[265,1123,1124,1127,1129],{"class":267,"line":395},[265,1125,1126],{"class":271},"  style.textContent ",[265,1128,748],{"class":553},[265,1130,1131],{"class":285}," `\n",[265,1133,1134],{"class":267,"line":408},[265,1135,1136],{"class":285},"    :root {\n",[265,1138,1139,1142,1144],{"class":267,"line":417},[265,1140,1141],{"class":285},"      --custom-color: ${",[265,1143,911],{"class":271},[265,1145,1146],{"class":285},"};\n",[265,1148,1149,1152,1155,1157,1160,1162,1165,1167],{"class":267,"line":444},[265,1150,1151],{"class":285},"      --custom-color-emphasis: rgb(${",[265,1153,1154],{"class":271},"emphasis",[265,1156,983],{"class":285},[265,1158,1159],{"class":570},"join",[265,1161,574],{"class":285},[265,1163,1164],{"class":285},"','",[265,1166,975],{"class":285},[265,1168,1169],{"class":285},"});\n",[265,1171,1172],{"class":267,"line":468},[265,1173,1174],{"class":285},"    }\n",[265,1176,1177],{"class":267,"line":474},[265,1178,1179],{"class":285},"    .slug-hero-custom { background: radial-gradient(var(--custom-color), black) !important; }\n",[265,1181,1182],{"class":267,"line":482},[265,1183,1184],{"class":285},"    .slug-variant-card-custom h3 { color: var(--custom-color) !important; }\n",[265,1186,1187],{"class":267,"line":490},[265,1188,1189],{"class":285},"    .slug-cta-custom .btn-primary { border-color: var(--custom-color) !important; }\n",[265,1191,1192],{"class":267,"line":498},[265,1193,1194],{"class":285},"    \u002F* + uma dezena de seletores de swiper, benefícios e CTA *\u002F\n",[265,1196,1197],{"class":267,"line":504},[265,1198,1199],{"class":285},"  `\n",[265,1201,1202,1205,1208],{"class":267,"line":510},[265,1203,1204],{"class":271},"  document.head.",[265,1206,1207],{"class":570},"appendChild",[265,1209,1210],{"class":271},"(style)\n",[265,1212,1214],{"class":267,"line":1213},23,[265,1215,513],{"class":271},[10,1217,1218,1219,1222,1223,1226,1227,1230],{},"A função valida o hex por regex, converte pra RGB, calcula uma versão clara pra usar em hover\u002Fbackgrounds, e injeta um ",[149,1220,1221],{},"\u003Cstyle>"," no ",[149,1224,1225],{},"\u003Chead>"," sobrescrevendo mais de dez seletores — hero, cards de variante, botões do carrossel, paginação, lista de benefícios, CTA — tudo derivado de um único valor. Nenhum produto ou embalagem real usa esse campo hoje: o cliente nunca pediu cor customizada por item, então ficou tudo no padrão verde\u002Fvinho por categoria. Mas o código está lá, testado e funcional, esperando o dia que alguém preencha um ",[149,1228,1229],{},"\"color\": \"#...\""," num JSON novo.",[39,1232,1234],{"id":1233},"web-components-sem-framework","Web Components sem framework",[10,1236,1237,1238,1241,1242,1245],{},"Cada peça reutilizável do site (header, footer, carrossel, slider de depoimentos) é um ",[149,1239,1240],{},"HTMLElement"," nativo registrado via ",[149,1243,1244],{},"customElements.define",", sem Vue nem React por trás. O footer, por exemplo:",[255,1247,1250],{"className":538,"code":1248,"filename":1249,"language":540,"meta":261,"style":261},"class LeveFooter extends HTMLElement {\n  constructor() {\n    super()\n  }\n\n  connectedCallback() {\n    const colorScheme = this.detectColorScheme()\n\n    \u002F\u002F Lê variáveis de ambiente injetadas no build, com fallback hardcoded\n    const E = window.__LEVE_CTX || {}\n    const phone = E.CONTACT_PHONE || '41992694691'\n    const email = E.CONTACT_EMAIL || 'comercial@levemix.ind.br'\n\n    this.innerHTML = `\n      \u003Cfooter class=\"footer ${colorScheme}\">\n        \u003C!-- ... -->\n      \u003C\u002Ffooter>\n    `\n  }\n}\n\ncustomElements.define('leve-footer', LeveFooter)\n","js\u002Fcomponents\u002Fleve-footer.js",[149,1251,1252,1270,1278,1285,1289,1293,1300,1320,1324,1329,1347,1369,1390,1394,1406,1417,1422,1427,1432,1436,1440,1444],{"__ignoreMap":261},[265,1253,1254,1257,1261,1264,1267],{"class":267,"line":268},[265,1255,1256],{"class":553},"class",[265,1258,1260],{"class":1259},"soyes"," LeveFooter",[265,1262,1263],{"class":553}," extends",[265,1265,1266],{"class":557}," HTMLElement",[265,1268,1269],{"class":271}," {\n",[265,1271,1272,1275],{"class":267,"line":275},[265,1273,1274],{"class":553},"  constructor",[265,1276,1277],{"class":271},"() {\n",[265,1279,1280,1283],{"class":267,"line":292},[265,1281,1282],{"class":557},"    super",[265,1284,615],{"class":271},[265,1286,1287],{"class":267,"line":304},[265,1288,1020],{"class":271},[265,1290,1291],{"class":267,"line":317},[265,1292,591],{"emptyLinePlaceholder":590},[265,1294,1295,1298],{"class":267,"line":326},[265,1296,1297],{"class":570},"  connectedCallback",[265,1299,1277],{"class":271},[265,1301,1302,1305,1308,1310,1313,1315,1318],{"class":267,"line":339},[265,1303,1304],{"class":553},"    const",[265,1306,1307],{"class":557}," colorScheme",[265,1309,561],{"class":553},[265,1311,1312],{"class":557}," this",[265,1314,983],{"class":271},[265,1316,1317],{"class":570},"detectColorScheme",[265,1319,615],{"class":271},[265,1321,1322],{"class":267,"line":352},[265,1323,591],{"emptyLinePlaceholder":590},[265,1325,1326],{"class":267,"line":365},[265,1327,1328],{"class":547},"    \u002F\u002F Lê variáveis de ambiente injetadas no build, com fallback hardcoded\n",[265,1330,1331,1333,1336,1338,1341,1344],{"class":267,"line":376},[265,1332,1304],{"class":553},[265,1334,1335],{"class":557}," E",[265,1337,561],{"class":553},[265,1339,1340],{"class":271}," window.__LEVE_CTX ",[265,1342,1343],{"class":553},"||",[265,1345,1346],{"class":271}," {}\n",[265,1348,1349,1351,1354,1356,1358,1360,1363,1366],{"class":267,"line":382},[265,1350,1304],{"class":553},[265,1352,1353],{"class":557}," phone",[265,1355,561],{"class":553},[265,1357,1335],{"class":557},[265,1359,983],{"class":271},[265,1361,1362],{"class":557},"CONTACT_PHONE",[265,1364,1365],{"class":553}," ||",[265,1367,1368],{"class":285}," '41992694691'\n",[265,1370,1371,1373,1376,1378,1380,1382,1385,1387],{"class":267,"line":395},[265,1372,1304],{"class":553},[265,1374,1375],{"class":557}," email",[265,1377,561],{"class":553},[265,1379,1335],{"class":557},[265,1381,983],{"class":271},[265,1383,1384],{"class":557},"CONTACT_EMAIL",[265,1386,1365],{"class":553},[265,1388,1389],{"class":285}," 'comercial@levemix.ind.br'\n",[265,1391,1392],{"class":267,"line":408},[265,1393,591],{"emptyLinePlaceholder":590},[265,1395,1396,1399,1402,1404],{"class":267,"line":417},[265,1397,1398],{"class":557},"    this",[265,1400,1401],{"class":271},".innerHTML ",[265,1403,748],{"class":553},[265,1405,1131],{"class":285},[265,1407,1408,1411,1414],{"class":267,"line":444},[265,1409,1410],{"class":285},"      \u003Cfooter class=\"footer ${",[265,1412,1413],{"class":271},"colorScheme",[265,1415,1416],{"class":285},"}\">\n",[265,1418,1419],{"class":267,"line":468},[265,1420,1421],{"class":285},"        \u003C!-- ... -->\n",[265,1423,1424],{"class":267,"line":474},[265,1425,1426],{"class":285},"      \u003C\u002Ffooter>\n",[265,1428,1429],{"class":267,"line":482},[265,1430,1431],{"class":285},"    `\n",[265,1433,1434],{"class":267,"line":490},[265,1435,1020],{"class":271},[265,1437,1438],{"class":267,"line":498},[265,1439,513],{"class":271},[265,1441,1442],{"class":267,"line":504},[265,1443,591],{"emptyLinePlaceholder":590},[265,1445,1446,1449,1452,1454,1457],{"class":267,"line":510},[265,1447,1448],{"class":271},"customElements.",[265,1450,1451],{"class":570},"define",[265,1453,574],{"class":271},[265,1455,1456],{"class":285},"'leve-footer'",[265,1458,1459],{"class":271},", LeveFooter)\n",[10,1461,1462,1464,1465,1467,1468,1470],{},[149,1463,718],{}," é exatamente o objeto gerado pelo pipeline ",[149,1466,696],{}," → ",[149,1469,845],{}," explicado acima — o fallback hardcoded garante que o footer nunca quebra visualmente mesmo se essa injeção falhar por qualquer motivo.",[39,1472,1474],{"id":1473},"build-cache-busting-via-timestamp-em-paralelo","Build: cache-busting via timestamp, em paralelo",[10,1476,1477,1478,1480],{},"Sem framework, cache-busting de CSS\u002FJS não vem de graça — tive que construir na mão. ",[149,1479,155],{}," orquestra três scripts:",[651,1482,1483,1490,1527],{},[654,1484,1485,1486,1489],{},"Renomeia cada arquivo CSS\u002FJS com um timestamp no nome (ex: ",[149,1487,1488],{},"home-27-02-2026-17-44-10.css",") e atualiza as referências em todos os HTMLs",[654,1491,1492,1495,1496,1499,1500,101,1503,1506,1507,1510,1511,101,1514,101,1517,955,1520,1523,1524],{},[149,1493,1494],{},"build-images.js"," processa ",[149,1497,1498],{},"CMS\u002Fimages.json"," — a fonte única de verdade pra imagem gerenciada por ID (",[149,1501,1502],{},"IMG-01",[149,1504,1505],{},"IMG-02","...) — e injeta ",[149,1508,1509],{},"src"," com ",[149,1512,1513],{},"?v=timestamp",[149,1515,1516],{},"alt",[149,1518,1519],{},"width",[149,1521,1522],{},"height"," em cada ",[149,1525,1526],{},"\u003Cimg data-image-id=\"IMG-XX\">",[654,1528,1529,1532],{},[149,1530,1531],{},"build-images-worker.js"," faz esse processamento em paralelo, uma página por Worker Thread — sem isso, processar todas as páginas HTML sequencialmente ficava perceptivelmente mais lento a cada nova página adicionada",[39,1534,1536,1537],{"id":1535},"o-bug-de-cache-que-exigiu-cirurgia-no-verceljson","O bug de cache que exigiu cirurgia no ",[149,1538,1539],{},"vercel.json",[10,1541,1542,1543,1546,1547,1551],{},"Depois de um dos primeiros deploys, o cliente reportou não ver as mudanças mesmo com o novo código já publicado. Causa raiz: os headers de cache originais aplicavam ",[149,1544,1545],{},"max-age=31536000, immutable"," (um ano, \"nunca muda\") em regras amplas demais — inclusive pegando HTML e os arquivos JS\u002FCSS principais, que ",[1548,1549,1550],"em",{},"mudam"," a cada deploy mesmo sem cache-busting no nome do arquivo raiz.",[10,1553,1554,1555,101,1558,1561],{},"A correção foi separar regras por tipo de arquivo: HTML e os poucos arquivos sem hash no nome recebem revalidação agressiva; assets versionados (",[149,1556,1557],{},"\u002Fassets\u002F*",[149,1559,1560],{},"\u002Ficons\u002F*",", com hash de timestamp) mantêm o cache de um ano com segurança, porque o nome muda a cada build.",[255,1563,1567],{"className":1564,"code":1565,"filename":1539,"language":1566,"meta":261,"style":261},"language-jsonc shiki shiki-themes github-light-high-contrast github-dark-high-contrast","{\n  \"headers\": [\n    \u002F\u002F HTML sempre revalida — nunca fica preso em cache velho\n    {\n      \"source\": \"\u002F(.*)\\\\.(html)\",\n      \"headers\": [{ \"key\": \"Cache-Control\", \"value\": \"public, max-age=0, must-revalidate\" }]\n    },\n    \u002F\u002F Assets com timestamp no nome — cache de 1 ano é seguro\n    {\n      \"source\": \"\u002Fassets\u002F(.*)\",\n      \"headers\": [{ \"key\": \"Cache-Control\", \"value\": \"public, max-age=31536000, immutable\" }]\n    }\n  ]\n}\n","jsonc",[149,1568,1569,1573,1580,1585,1590,1608,1637,1642,1647,1651,1662,1685,1689,1693],{"__ignoreMap":261},[265,1570,1571],{"class":267,"line":268},[265,1572,272],{"class":271},[265,1574,1575,1578],{"class":267,"line":275},[265,1576,1577],{"class":278},"  \"headers\"",[265,1579,414],{"class":271},[265,1581,1582],{"class":267,"line":292},[265,1583,1584],{"class":547},"    \u002F\u002F HTML sempre revalida — nunca fica preso em cache velho\n",[265,1586,1587],{"class":267,"line":304},[265,1588,1589],{"class":271},"    {\n",[265,1591,1592,1595,1597,1600,1603,1606],{"class":267,"line":317},[265,1593,1594],{"class":278},"      \"source\"",[265,1596,282],{"class":271},[265,1598,1599],{"class":285},"\"\u002F(.*)",[265,1601,1602],{"class":553},"\\\\",[265,1604,1605],{"class":285},".(html)\"",[265,1607,289],{"class":271},[265,1609,1610,1613,1616,1619,1621,1624,1626,1629,1631,1634],{"class":267,"line":326},[265,1611,1612],{"class":278},"      \"headers\"",[265,1614,1615],{"class":271},": [{ ",[265,1617,1618],{"class":278},"\"key\"",[265,1620,282],{"class":271},[265,1622,1623],{"class":285},"\"Cache-Control\"",[265,1625,101],{"class":271},[265,1627,1628],{"class":278},"\"value\"",[265,1630,282],{"class":271},[265,1632,1633],{"class":285},"\"public, max-age=0, must-revalidate\"",[265,1635,1636],{"class":271}," }]\n",[265,1638,1639],{"class":267,"line":339},[265,1640,1641],{"class":271},"    },\n",[265,1643,1644],{"class":267,"line":352},[265,1645,1646],{"class":547},"    \u002F\u002F Assets com timestamp no nome — cache de 1 ano é seguro\n",[265,1648,1649],{"class":267,"line":365},[265,1650,1589],{"class":271},[265,1652,1653,1655,1657,1660],{"class":267,"line":376},[265,1654,1594],{"class":278},[265,1656,282],{"class":271},[265,1658,1659],{"class":285},"\"\u002Fassets\u002F(.*)\"",[265,1661,289],{"class":271},[265,1663,1664,1666,1668,1670,1672,1674,1676,1678,1680,1683],{"class":267,"line":382},[265,1665,1612],{"class":278},[265,1667,1615],{"class":271},[265,1669,1618],{"class":278},[265,1671,282],{"class":271},[265,1673,1623],{"class":285},[265,1675,101],{"class":271},[265,1677,1628],{"class":278},[265,1679,282],{"class":271},[265,1681,1682],{"class":285},"\"public, max-age=31536000, immutable\"",[265,1684,1636],{"class":271},[265,1686,1687],{"class":267,"line":395},[265,1688,1174],{"class":271},[265,1690,1691],{"class":267,"line":408},[265,1692,507],{"class":271},[265,1694,1695],{"class":267,"line":417},[265,1696,513],{"class":271},[1698,1699,1700],"blockquote",{},[10,1701,1702,1703,1706,1707,1710],{},"A regra que resolveu tudo: ",[149,1704,1705],{},"max-age=0, must-revalidate"," obriga o navegador a sempre perguntar pro servidor se há versão nova antes de usar o cache local — sem isso, ",[149,1708,1709],{},"immutable"," genérico demais deixa o cliente preso na versão antiga indefinidamente.",[39,1712,1714],{"id":1713},"pwa-de-verdade","PWA de verdade",[10,1716,1717,1720,1721,1724,1725,1728,1729,1732],{},[149,1718,1719],{},"manifest.json"," e ",[149,1722,1723],{},"sw.js"," (Service Worker) estão no ar e respondem 200 — não é enfeite, o site funciona como Progressive Web App instalável, com cache offline dos assets estáticos. Único detalhe técnico chato: Service Worker não registra em ",[149,1726,1727],{},"file:\u002F\u002F",", só via servidor HTTP real — documentei isso num ",[149,1730,1731],{},"TESTE-LOCAL.md"," interno depois de mais de uma vez esquecer e testar direto do disco.",[39,1734,1736],{"id":1735},"seo-e-descoberta-por-ia","SEO e descoberta por IA",[10,1738,1739,1740,1720,1743,1746,1747,1750,1751,1754,1755,1758,1759,1761,1762,1720,1765,1768],{},"Junto do ",[149,1741,1742],{},"sitemap.xml",[149,1744,1745],{},"robots.txt",", o site tem um ",[149,1748,1749],{},"llm.txt"," na raiz — mesma ideia do ",[149,1752,1753],{},"\u002Fllms.txt"," que o próprio ao.dev expõe hoje (via ",[149,1756,1757],{},"nuxt-llms","), só que aqui é o próprio ",[149,1760,155],{}," que regenera os três arquivos a cada build, junto com a lista dinâmica de slugs do CMS Loader. Dado estruturado via JSON-LD cobre ",[149,1763,1764],{},"Organization",[149,1766,1767],{},"LocalBusiness",". Cada página de produto\u002Fembalagem tem título, descrição e Open Graph únicos — consequência direta de ter migrado pra HTML estático real.",[39,1770,1772],{"id":1771},"documentacao-pensada-pro-cliente-nao-pro-dev","Documentação pensada pro cliente, não pro dev",[10,1774,1775,1776,1779],{},"Um dos artefatos que mais me orgulho desse projeto nem é código: é um formulário em Google Docs — ",[149,1777,1778],{},"formulario-adicionar-novos-itens.md"," — que ensina alguém sem nenhum conhecimento técnico a descrever um produto novo (nome, descrição, imagens, características, palavras-chave de SEO) de um jeito que eu consigo transformar direto num JSON novo do CMS. A seção técnica do formulário (slug, categoria, caminho do arquivo) vem preenchida como \"isso aqui é com a gente, você não precisa se preocupar\".",[10,1781,1782,1783,1786,1787,1790],{},"Também existe um documento interno só de guarda-corpo (",[149,1784,1785],{},"produtos-e-servicos-descontinuados.md","): uma lista do que a empresa ",[657,1788,1789],{},"não"," trabalha mais, pra eu nunca reintroduzir por engano algo removido a pedido do cliente numa rodada anterior de revisão — proteção contra regressão de conteúdo, não só de código.",[39,1792,1794],{"id":1793},"deploy","Deploy",[10,1796,1797,1798,1801,1802,1804,1805,1807],{},"O deploy roda 100% na Vercel, sem build remoto — ",[149,1799,1800],{},"Output Directory"," aponta pra raiz do repositório (",[149,1803,983],{},"), porque o HTML já sai pronto do ",[149,1806,155],{}," local. A parte mais chata do projeto não foi técnica: foi a burocracia de acesso a domínio e certificado SSL do lado do cliente, resolvida diretamente com o time de TI deles. Levou mais tempo do que o esperado, mas os dois sites do grupo (Leve Mix e o institucional da Romanus) terminaram no ar, validados e com certificado correto.",[39,1809,1811],{"id":1810},"sobre","Sobre",[23,1813,1815,1819],{"className":1814},[26],[28,1816],{"alt":1817,"height":31,"src":1818,"title":1811,"width":34},"Página Sobre, com Missão, Visão, Valores e o compromisso com segurança alimentar","\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-sobre.png",[36,1820,1817],{},[10,1822,1823],{},"A missão declarada no site resume bem o negócio:",[1698,1825,1826],{},[10,1827,1828],{},"Ser a solução de terceirização completa e confiável para o desenvolvimento, produção e embalagem de marcas próprias no mercado de alimentos, simplificando o caminho do empreendedor para lançar produtos de alta qualidade com agilidade e custos otimizados.",[10,1830,1831,1832,1835],{},"Logo abaixo, a seção \"Compromisso com Segurança e Qualidade Alimentar\" conta que a operação roda em ",[657,1833,1834],{},"duas plantas independentes",", com sistemas de controle de contaminação cruzada — informação que entrou nessa versão a pedido explícito do cliente, numa rodada de revisão específica pra deixar claro que não é produção genérica misturada. A seção de Valores lista itens como \"Excelência: qualidade premium em cada produto\" e \"Parceria: seu sucesso é o nosso sucesso\", cada um com ícone Lucide próprio. Fecha com o carrossel \"O que você vai produzir com a gente\" — a mesma lista de produtos que também vira página própria em Produção.",[39,1837,1839],{"id":1838},"producao","Produção",[10,1841,1842,1843,1846,1847,1850],{},"Diferente de Embalagens, Produção não tem uma página de listagem própria — cada linha de produto é só um slug direto (",[149,1844,1845],{},"\u002Fproducao\u002F{slug}\u002F","), descoberto pelo carrossel \"O que você vai produzir com a gente\" (lá na página Sobre) ou pela busca em tempo real do CMS Loader. No momento em que escrevo isso, o catálogo cobre linhas como cafés e cappuccinos, chocolate em pó, colágenos, chás, farinhas e temperos — mas como cada uma é um JSON isolado, esse catálogo muda com o tempo: itens novos entram, outros podem sair, sem precisar de deploy de código nenhum. \"Líquidos\", por exemplo, foi a última linha adicionada nesse projeto, numa rodada de revisão já perto do fim — subiu primeiro com imagem e texto placeholder (",[1548,1848,1849],{},"lorem ipsum"," mesmo) enquanto o material final não chegava, e só depois foi preenchido com dado real. É o tipo de coisa que só um CMS separado do HTML permite fazer sem gambiarra: a página existe, está no ar, indexável, e o conteúdo evolui por baixo sem precisar de nada novo em código.",[1852,1853,1855],"h3",{"id":1854},"um-slug-de-produto-por-dentro","Um slug de produto por dentro",[23,1857,1859,1864],{"className":1858},[26],[28,1860],{"alt":1861,"height":31,"src":1862,"title":1863,"width":34},"Exemplo de página de produto, com variantes, vantagens e CTA","\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-produto-chocolate.png","Exemplo de página de produto",[36,1865,1861],{},[10,1867,1868,1869,1871,1872,1875],{},"Toda página de produto segue a mesma anatomia, gerada pelo mesmo ",[149,1870,248],{},", nessa ordem: hero (título, foto, subtítulo, verde por ser categoria Produção), parágrafo de descrição completa, cards de variante (quando o item tem mais de uma versão), uma lista de \"Vantagens\" num grid de 2 colunas, CTA \"Fale com um Especialista\", e um swiper \"Outros Produtos\" no fim, puxando os demais slugs da categoria via ",[149,1873,1874],{},"cmsLoader.getItemsForSlider()",". O exemplo no print acima é a página de Chocolate em Pó, capturada num momento específico do catálogo — o conteúdo exato pode mudar, a estrutura por trás, não.",[39,1877,1879],{"id":1878},"embalagens","Embalagens",[23,1881,1883,1887],{"className":1882},[26],[28,1884],{"alt":1885,"height":31,"src":1886,"title":1879,"width":34},"Página de Embalagens com o carrossel de formatos disponíveis","\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-embalagens.png",[36,1888,1885],{},[10,1890,1891,1892,1895,1896,1899,1900,1902,1903,1905,1906,1908,1909,1911,1912,1914],{},"Diferente de Produção, Embalagens tem sua própria página de listagem — ",[149,1893,1894],{},"\u002Fembalagens\u002F",", com carrossel dos formatos disponíveis, presente no menu e no rodapé. Cada formato individual segue a mesma estrutura de slug (",[149,1897,1898],{},"\u002Fembalagens\u002F{slug}\u002F","), mesmo Web Component ",[149,1901,248],{},", mesmo schema de JSON — só a ",[149,1904,252],{}," na URL muda de ",[149,1907,1838],{}," pra ",[149,1910,1878],{},". Reaproveitar o mesmo componente genérico pras duas seções, em vez de duplicar a lógica de \"página de produto\" e \"página de embalagem\" como no início do projeto, foi uma das decisões que mais economizou trabalho de manutenção lá na frente: um bug corrigido ou uma feature nova no ",[149,1913,248],{}," beneficia todas as páginas de uma vez, produto ou embalagem.",[1852,1916,1918],{"id":1917},"um-slug-de-embalagem-por-dentro","Um slug de embalagem por dentro",[23,1920,1922,1927],{"className":1921},[26],[28,1923],{"alt":1924,"height":31,"src":1925,"title":1926,"width":34},"Exemplo de página de embalagem, em vinho, mesma estrutura da página de produto","\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-embalagem-pouches.png","Exemplo de página de embalagem",[36,1928,1924],{},[10,1930,1931],{},"Mesma anatomia da página de produto, ponto por ponto — só que em vinho, a cor padrão de Embalagens. O exemplo acima é a página de Stand Up Pouch, com suas próprias variantes por tamanho e vantagens específicas do formato, seguida do mesmo carrossel de \"Outras Embalagens\" no fim. Ver as duas páginas lado a lado é o jeito mais direto de mostrar por que valeu a pena ter um único componente genérico: zero diferença estrutural entre elas, só o dado (que pode mudar) e a cor mudam.",[39,1933,1935],{"id":1934},"nossos-servicos","Nossos Serviços",[23,1937,1939,1943],{"className":1938},[26],[28,1940],{"alt":1941,"height":31,"src":1942,"title":1935,"width":34},"Página Nossos Serviços, com o accordion das oito etapas do outsourcing","\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-servicos.png",[36,1944,1941],{},[10,1946,1947],{},"Um accordion com oito etapas do processo de outsourcing, cada uma com ícone Lucide próprio:",[1949,1950,1951,1957,1960,1963,1966,1969,1972,1975],"ol",{},[654,1952,1953,1954],{},"Mistura, envase e rotulação — ",[1548,1955,1956],{},"\"Nossa infraestrutura completa garante a mistura precisa das formulações em pó — cafés, achocolatados, temperos e mixes diversos — seguida pelo envase e rotulação eficientes.\"",[654,1958,1959],{},"Desenvolvimento de novos produtos",[654,1961,1962],{},"Desenvolvimento de embalagens",[654,1964,1965],{},"Gestão de fornecedores",[654,1967,1968],{},"Produção convencional",[654,1970,1971],{},"Produção livre de alergênicos",[654,1973,1974],{},"Consultoria de viabilidade",[654,1976,1977],{},"HUB Logístico",[10,1979,1980,1981,101,1984,101,1987,101,1990,101,1993,101,1996,101,1999,101,2002,2005],{},"Os ícones desses oito itens passaram por uma revisão no meio do projeto: começaram como imagens placeholder 80×80, depois viraram ícones Lucide reais (",[149,1982,1983],{},"package",[149,1985,1986],{},"flask-conical",[149,1988,1989],{},"box",[149,1991,1992],{},"users",[149,1994,1995],{},"factory",[149,1997,1998],{},"shield-check",[149,2000,2001],{},"trending-up",[149,2003,2004],{},"truck",") — mais leve, mais nítido em qualquer resolução, e sem depender de nenhuma imagem exportada pelo design.",[39,2007,2009],{"id":2008},"contato","Contato",[23,2011,2013,2017],{"className":2012},[26],[28,2014],{"alt":2015,"height":31,"src":2016,"title":2009,"width":34},"Página de contato, com formulário integrado via Google Apps Script","\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-contato.png",[36,2018,2015],{},[10,2020,2021,2022,2025,2026,101,2029,2032],{},"O formulário pede empresa, nome, e-mail, telefone e uma descrição livre da ideia ou produto — sem excesso de campo pra não afugentar quem só quer mandar uma mensagem rápida. Tem um campo invisível a mais, ",[149,2023,2024],{},"website_url",", fora do fluxo visual (",[149,2027,2028],{},"tabindex=\"-1\"",[149,2030,2031],{},"autocomplete=\"off\"",") — um honeypot clássico anti-spam: bot de formulário costuma preencher todo campo que encontra no DOM, humano nunca vê esse campo pra preencher. Se ele chegar preenchido no submit, a entrada é descartada como spam sem incomodar ninguém.",[10,2034,2035,2036,2039],{},"O envio em si passa por um ",[657,2037,2038],{},"Google Apps Script"," publicado como Web App — sem backend próprio, sem servidor de e-mail pra manter, só um script rodando na conta Google do cliente. A página fecha com um mapa da localização real (Campo Largo, PR) e a seção de depoimentos de clientes.",[39,2041,2043],{"id":2042},"o-que-ficou-pro-aodev","O que ficou pro ao.dev",[10,2045,2046],{},"Esse projeto rodou pouco antes de eu recomeçar o ao.dev do zero, e algumas lições vieram diretas de erro cometido aqui:",[651,2048,2049,2059,2065],{},[654,2050,2051,2052,2055,2056,2058],{},"O bug de ",[149,2053,2054],{},"Cache-Control"," genérico demais neste projeto é o motivo de eu já ter chegado no ao.dev sabendo exatamente que assets com hash no nome usam ",[149,2057,1709],{}," e tudo o resto revalida — documentado desde o início em vez de descoberto depois de um deploy quebrado",[654,2060,2061,2062,2064],{},"O CMS caseiro em JSON, sem painel, sem banco — a ideia raiz de \"conteúdo é dado versionado no repositório\" — é literalmente por que optei pelo ",[149,2063,519],{}," no ao.dev em vez de um CMS headless externo",[654,2066,2067,2068,2070,2071,2073],{},"Ter automatizado a geração de ",[149,2069,1749],{}," aqui — build.js regenerando o arquivo a cada deploy em vez de mantê-lo estático na mão — é a mesma lógica por trás de eu ter ido atrás do ",[149,2072,1757],{}," no ao.dev assim que soube que esse padrão existia como módulo pronto",[39,2075,2077],{"id":2076},"grupo-romanus-e-o-portfolio-completo","Grupo Romanus e o portfólio completo",[10,2079,2080],{},"A Leve Mix oferece terceirização completa — desenvolvimento de fórmula, produção em escala e embalagem final com a marca do cliente — em cinco frentes principais:",[1949,2082,2083,2089,2096,2099,2102],{},[654,2084,2085,2086],{},"Bebidas quentes: cafés e cappuccinos (tradicionais e funcionais, como café verde e erva-mate) e chocolate em pó com cacau certificado ",[657,2087,2088],{},"Rainforest Alliance",[654,2090,2091,2092,2095],{},"Suplementos e colágenos, incluindo a linha ",[657,2093,2094],{},"Verisol"," — peptídeos bioativos de colágeno patenteados",[654,2097,2098],{},"Chás solúveis e a granel",[654,2100,2101],{},"Farináceos, como o mix pra bolo de caneca proteico (sem glúten, sem lactose)",[654,2103,2104],{},"Temperos e condimentos, tradicionais e funcionais",[10,2106,2107],{},"Hoje o site mostra mais de 200 soluções desenvolvidas, mais de 50 clientes ativos, 10 anos da Leve Mix e 26 anos do Grupo Romanus — números que o próprio cliente validou antes da publicação.",[2109,2110,2111],"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 .s-5SL, html code.shiki .s-5SL{--shiki-default:#023B95;--shiki-dark:#91CBFF}html pre.shiki code .suds8, html code.shiki .suds8{--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .sKwhi, html code.shiki .sKwhi{--shiki-default:#622CBC;--shiki-dark:#DBB7FF}html pre.shiki code .sT6z2, html code.shiki .sT6z2{--shiki-default:#032563;--shiki-dark:#ADDCFF}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 .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 .soyes, html code.shiki .soyes{--shiki-default:#702C00;--shiki-dark:#FFB757}",{"title":261,"searchDepth":275,"depth":275,"links":2113},[2114,2115,2116,2117,2118,2120,2121,2122,2123,2125,2126,2127,2128,2129,2130,2133,2136,2137,2138,2139],{"id":41,"depth":275,"text":42},{"id":159,"depth":275,"text":160},{"id":237,"depth":275,"text":238},{"id":523,"depth":275,"text":524},{"id":692,"depth":275,"text":2119},"Config idempotente: de .env pra atributo data-bind no HTML",{"id":849,"depth":275,"text":850},{"id":1233,"depth":275,"text":1234},{"id":1473,"depth":275,"text":1474},{"id":1535,"depth":275,"text":2124},"O bug de cache que exigiu cirurgia no vercel.json",{"id":1713,"depth":275,"text":1714},{"id":1735,"depth":275,"text":1736},{"id":1771,"depth":275,"text":1772},{"id":1793,"depth":275,"text":1794},{"id":1810,"depth":275,"text":1811},{"id":1838,"depth":275,"text":1839,"children":2131},[2132],{"id":1854,"depth":292,"text":1855},{"id":1878,"depth":275,"text":1879,"children":2134},[2135],{"id":1917,"depth":292,"text":1918},{"id":1934,"depth":275,"text":1935},{"id":2008,"depth":275,"text":2009},{"id":2042,"depth":275,"text":2043},{"id":2076,"depth":275,"text":2077},[2141],"institutional","Freela","\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-cover.png","2026-07-12","Site institucional de uma indústria de terceirização de alimentos (private label) do Grupo Romanus — 100% estático, zero framework, CMS próprio em JSON, sistema de theming por cor, Web Components nativos e PWA de verdade.","md","\u002Fimages\u002Fpages\u002Fprojects\u002Fleve-mix\u002Fleve-mix-detail.png",[2149],{"label":2150,"url":2151},"levemix.ind.br","https:\u002F\u002Flevemix.ind.br",{},"\u002Fprojects\u002Fleve-mix","2025 – 2026",[2156],"Frontend Developer",{"title":5,"description":2145},[74,87,100,106,2159,119,143],"Service Worker","completed","projects\u002Fleve-mix",[74,87,2163,106,2164,192,2165],"JavaScript","PWA","CMS Próprio","fUao62lT3t0snJsaMvl5Fq4147BHBs7LwpZIvZ50SKI",null,{"path":2169,"title":2170,"order":275},"\u002Fprojects\u002Fprospecta-geo","Prospecta Geo",1784312151039]