[{"data":1,"prerenderedAt":2420},["ShallowReactive",2],{"post-\u002Fposts\u002Fwant-to-contribute-to-open-source-youre-doing-it-wrong":3},{"id":4,"title":5,"body":6,"contentId":2406,"date":2407,"description":2408,"extension":2409,"meta":2410,"navigation":209,"path":2411,"rawbody":2412,"robots":2413,"schemaOrg":2413,"seo":2414,"sitemap":2415,"stem":2418,"__hash__":2419},"posts\u002Fposts\u002Fwant-to-contribute-to-open-source-youre-doing-it-wrong.md","Want to Contribute to Open Source? You're Doing It Wrong",{"type":7,"value":8,"toc":2400},"minimark",[9,13,16,19,22,25,28,43,51,54,81,84,87,99,104,107,116,119,122,125,302,313,320,473,476,479,496,499,502,509,525,532,1303,1306,1315,1322,1337,1341,1344,1347,1350,1613,1620,1627,1630,1633,1754,1761,1764,1771,1774,1777,1783,1786,1790,1793,1796,1799,1806,1809,2119,2122,2125,2131,2137,2150,2239,2246,2253,2397],[10,11,12],"p",{},"I have been actively contributing to open-source since 2020. Since then, I've spoken with many developers who want to contribute but don't know where to start, often asking for an issue to work on.",[10,14,15],{},"I'm deeply convinced that open-source is more than that and, more importantly, doesn't work like that.",[10,17,18],{},"To me, open-source, at its core, is a mindset. It's about the freedom to explore, understand, modify, and improve the source code of the software you want. Of course, this model encourages collaboration and sharing by allowing anyone to contribute, but it doesn't require you to.",[10,20,21],{},"Open-source shouldn't, and should never, be a goal in itself. It's part of the journey, but it's not the destination. Otherwise, you'll end up feeling frustrated and unfulfilled by never being able to contribute.",[10,23,24],{},"Let me tell you a story to illustrate this point.",[26,27],"hr",{},[10,29,30,31,38,39],{},"I'm building a new website called ",[32,33,37],"a",{"href":34,"rel":35},"https:\u002F\u002Finfra.soubiran.dev",[36],"nofollow","infra.soubiran.dev"," to document my own infrastructure. It's a personal wiki but open to everyone. ",[40,41,42],"em",{},"I won't go into details; a dedicated post will come later.",[10,44,45,46,50],{},"To build it, I needed to find the right tools. Until now, I was using ",[32,47,49],{"href":48},".\u002Fvitepress-for-a-personal-website-is-that-possible","VitePress with a custom theme for my personal blog",", the one you're reading right now. It works great, but I've nearly reached its limits, and with all the ideas I have, I know it could be time to move on.",[10,52,53],{},"So, I started looking into alternatives. Honestly, there are two options:",[55,56,57,72],"ol",{},[58,59,60,61,66,67],"li",{},"Using ",[32,62,65],{"href":63,"rel":64},"https:\u002F\u002Fnuxt.com",[36],"Nuxt"," with ",[32,68,71],{"href":69,"rel":70},"https:\u002F\u002Fcontent.nuxt.com",[36],"Nuxt Content",[58,73,74,75,80],{},"Using Vite with Vue and some ",[32,76,79],{"href":77,"rel":78},"https:\u002F\u002Funplugin.unjs.io\u002F",[36],"unplugins"," to handle markdown files.",[10,82,83],{},"I went for the second option. Funny enough, before moving to VitePress, I had the same dilemma: Nuxt Content, VitePress, or Vite with Vue. At that time, I chose VitePress.",[10,85,86],{},"Anyway, I started building the website.",[10,88,89,90,95,96],{},"For the past couple of months, there's one tech I can't live without: ",[32,91,94],{"href":92,"rel":93},"https:\u002F\u002Fui.nuxt.com",[36],"Nuxt UI",". It's an incredible component library made by the Nuxt team. All my projects that use Vue or Nuxt use Nuxt UI, and I regularly push it to its limits. ",[40,97,98],{},"This is also a reason why I decided to move away from VitePress, which is not compatible with Nuxt UI.",[100,101,103],"h2",{"id":102},"building-with-open-source-software","Building with open-source software",[10,105,106],{},"The website I'm building is content-driven. This means that the content is primarily text-based and, in my case, all the pages are written in Markdown. To update the website, I edit the Markdown files and then rebuild the site.",[10,108,109,110,115],{},"The pages are generated and pre-rendered at build time. There is no need to generate them at runtime, as every user will receive the same pre-rendered content. To make it work, I use ",[32,111,114],{"href":112,"rel":113},"https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Fvite-ssg",[36],"Vite SSG",".",[10,117,118],{},"It's a drop-in replacement for Vite when building the website. It automatically builds the client and the server, then uses the server to pre-render each page, as if a user were requesting it.",[10,120,121],{},"After installing it, everything worked as expected.",[10,123,124],{},"So I continued to build the website. I installed Nuxt UI and started configuring it. To use Nuxt UI within a Vue application, you have to use a plugin.",[126,127,133],"pre",{"className":128,"code":129,"filename":130,"language":131,"meta":132,"style":132},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight has-highlighted","import ui from '@nuxt\u002Fui\u002Fvue-plugin'\nimport { ViteSSG } from 'vite-ssg'\nimport App from '.\u002FApp.vue'\n\nexport const createApp = ViteSSG(\n  App,\n  {},\n  ({ app }) => {\n    app.use(ui) \u002F\u002F [!code highlight]\n  },\n)\n","src\u002Fmain.ts","ts","",[134,135,136,163,187,204,211,233,242,248,267,290,296],"code",{"__ignoreMap":132},[137,138,141,145,149,152,156,160],"span",{"class":139,"line":140},"line",1,[137,142,144],{"class":143},"s7zQu","import",[137,146,148],{"class":147},"sTEyZ"," ui ",[137,150,151],{"class":143},"from",[137,153,155],{"class":154},"sMK4o"," '",[137,157,159],{"class":158},"sfazB","@nuxt\u002Fui\u002Fvue-plugin",[137,161,162],{"class":154},"'\n",[137,164,166,168,171,174,177,180,182,185],{"class":139,"line":165},2,[137,167,144],{"class":143},[137,169,170],{"class":154}," {",[137,172,173],{"class":147}," ViteSSG",[137,175,176],{"class":154}," }",[137,178,179],{"class":143}," from",[137,181,155],{"class":154},[137,183,184],{"class":158},"vite-ssg",[137,186,162],{"class":154},[137,188,190,192,195,197,199,202],{"class":139,"line":189},3,[137,191,144],{"class":143},[137,193,194],{"class":147}," App ",[137,196,151],{"class":143},[137,198,155],{"class":154},[137,200,201],{"class":158},".\u002FApp.vue",[137,203,162],{"class":154},[137,205,207],{"class":139,"line":206},4,[137,208,210],{"emptyLinePlaceholder":209},true,"\n",[137,212,214,217,221,224,227,230],{"class":139,"line":213},5,[137,215,216],{"class":143},"export",[137,218,220],{"class":219},"spNyl"," const",[137,222,223],{"class":147}," createApp ",[137,225,226],{"class":154},"=",[137,228,173],{"class":229},"s2Zo4",[137,231,232],{"class":147},"(\n",[137,234,236,239],{"class":139,"line":235},6,[137,237,238],{"class":147},"  App",[137,240,241],{"class":154},",\n",[137,243,245],{"class":139,"line":244},7,[137,246,247],{"class":154},"  {},\n",[137,249,251,254,258,261,264],{"class":139,"line":250},8,[137,252,253],{"class":154},"  ({",[137,255,257],{"class":256},"sHdIc"," app",[137,259,260],{"class":154}," })",[137,262,263],{"class":219}," =>",[137,265,266],{"class":154}," {\n",[137,268,272,275,277,280,284,287],{"class":269,"line":271},[139,270],"highlighted",9,[137,273,274],{"class":147},"    app",[137,276,115],{"class":154},[137,278,279],{"class":229},"use",[137,281,283],{"class":282},"swJcz","(",[137,285,286],{"class":147},"ui",[137,288,289],{"class":282},") ",[137,291,293],{"class":139,"line":292},10,[137,294,295],{"class":154},"  },\n",[137,297,299],{"class":139,"line":298},11,[137,300,301],{"class":147},")\n",[10,303,304,305,308,309,312],{},"And then, everything broke. During the pre-rendering, something tried to access ",[134,306,307],{},"document"," to create a style: ",[134,310,311],{},"const style = document.createElement('style')",". Really strange.",[10,314,315,316,319],{},"When pre-rendering, Vite SSG injects ",[134,317,318],{},"import.meta.env.SSR"," to detect if it's running in SSR mode. It also allows the bundler to tree-shake unused code effectively. So, to fix the bug, I wrapped the plugin usage in a conditional check for SSR mode.",[126,321,323],{"className":128,"code":322,"filename":130,"language":131,"meta":132,"style":132},"import ui from '@nuxt\u002Fui\u002Fvue-plugin'\nimport { ViteSSG } from 'vite-ssg'\nimport App from '.\u002FApp.vue'\n\nexport const createApp = ViteSSG(\n  App,\n  {},\n  ({ app }) => {\n    if (import.meta.env.SSR) { \u002F\u002F [!code highlight]\n      app.use(ui)\n    } \u002F\u002F [!code highlight]\n  },\n)\n",[134,324,325,339,357,371,375,389,395,399,411,442,457,463,468],{"__ignoreMap":132},[137,326,327,329,331,333,335,337],{"class":139,"line":140},[137,328,144],{"class":143},[137,330,148],{"class":147},[137,332,151],{"class":143},[137,334,155],{"class":154},[137,336,159],{"class":158},[137,338,162],{"class":154},[137,340,341,343,345,347,349,351,353,355],{"class":139,"line":165},[137,342,144],{"class":143},[137,344,170],{"class":154},[137,346,173],{"class":147},[137,348,176],{"class":154},[137,350,179],{"class":143},[137,352,155],{"class":154},[137,354,184],{"class":158},[137,356,162],{"class":154},[137,358,359,361,363,365,367,369],{"class":139,"line":189},[137,360,144],{"class":143},[137,362,194],{"class":147},[137,364,151],{"class":143},[137,366,155],{"class":154},[137,368,201],{"class":158},[137,370,162],{"class":154},[137,372,373],{"class":139,"line":206},[137,374,210],{"emptyLinePlaceholder":209},[137,376,377,379,381,383,385,387],{"class":139,"line":213},[137,378,216],{"class":143},[137,380,220],{"class":219},[137,382,223],{"class":147},[137,384,226],{"class":154},[137,386,173],{"class":229},[137,388,232],{"class":147},[137,390,391,393],{"class":139,"line":235},[137,392,238],{"class":147},[137,394,241],{"class":154},[137,396,397],{"class":139,"line":244},[137,398,247],{"class":154},[137,400,401,403,405,407,409],{"class":139,"line":250},[137,402,253],{"class":154},[137,404,257],{"class":256},[137,406,260],{"class":154},[137,408,263],{"class":219},[137,410,266],{"class":154},[137,412,414,417,420,422,424,427,429,432,434,437,439],{"class":413,"line":271},[139,270],[137,415,416],{"class":143},"    if",[137,418,419],{"class":282}," (",[137,421,144],{"class":143},[137,423,115],{"class":154},[137,425,426],{"class":147},"meta",[137,428,115],{"class":154},[137,430,431],{"class":147},"env",[137,433,115],{"class":154},[137,435,436],{"class":147},"SSR",[137,438,289],{"class":282},[137,440,441],{"class":154},"{",[137,443,444,447,449,451,453,455],{"class":139,"line":292},[137,445,446],{"class":147},"      app",[137,448,115],{"class":154},[137,450,279],{"class":229},[137,452,283],{"class":282},[137,454,286],{"class":147},[137,456,301],{"class":282},[137,458,460],{"class":459,"line":298},[139,270],[137,461,462],{"class":154},"    }",[137,464,466],{"class":139,"line":465},12,[137,467,295],{"class":154},[137,469,471],{"class":139,"line":470},13,[137,472,301],{"class":147},[10,474,475],{},"Now, pre-rendering worked as expected, or so I thought.",[10,477,478],{},"I wrote some pages, created components, and pushed everything to production. It built successfully, so I opened the website and saw the following:",[480,481,482,483,482,492],"figure",{},"\n  ",[484,485,486,487,482],"video",{"autoPlay":209,"loop":209,"muted":209,"playsInline":209},"\n    ",[488,489],"source",{"src":490,"type":491},"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Fwhant-to-contribute-to-open-source-youre-doing-it-wrong\u002Finfra-soubiran-dev-fouc.mp4","video\u002Fmp4",[493,494,495],"figcaption",{},"Loading of my new website",[10,497,498],{},"Can you see it?",[10,500,501],{},"Before going further, and if you noticed it, would you have fixed it? You know it will take time, and you won't be able to continue writing content. That's a real dilemma. Taking time for something you might consider a detail, or continuing to work on your content.",[10,503,504,508],{},[505,506,507],"strong",{},"Stunning websites are made of details",". For me, details are very important, and it's essential to pay attention to them. They can transform the user experience, improve user comfort, and increase loyalty. Never underestimate them.",[10,510,511,512,517,518,521,522,524],{},"For those who don't see it, that's called a ",[32,513,516],{"href":514,"rel":515},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FFlash_of_unstyled_content",[36],"FOUC",", a flash of unstyled content. The content appears before all the style is available. It usually happens when the script needs to add a ",[134,519,520],{},"dark"," class to the HTML element. The website loads with the default theme (light mode), appears, and then the JavaScript is parsed and executed. The script adds the ",[134,523,520],{}," class and the theme switches to dark mode. The light mode lasts a few hundred milliseconds, but it's enough to be noticed. This is all related to the way a browser renders a page.",[10,526,527,528,531],{},"It can be very tricky to fix. Usually, it happens when tags in your ",[134,529,530],{},"head"," aren't correctly ordered or when the script is not executed at the right time. So, I jumped into the head of the generated page:",[126,533,538],{"className":534,"code":535,"filename":536,"language":537,"meta":132,"style":132},"language-html shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Chtml lang=\"en\">\n  \u003Chead>\n    \u003Clink rel=\"preconnect\" href=\"https:\u002F\u002Ffonts.gstatic.com\u002F\" crossorigin=\"anonymous\">\n    \u003Clink rel=\"preload\" as=\"style\" onload=\"this.rel='stylesheet'\" href=\"https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=DM Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&amp;family=DM Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&amp;family=Sofia Sans:ital,wght@0,1..1000;1,1..1000&amp;display=swap\">\n    \u003Cmeta charset=\"utf-8\">\n    \u003Cmeta name=\"author\" content=\"Estéban Soubiran\">\n    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    \u003Ctitle>Estéban's Infra · Estéban Soubiran\u003C\u002Ftitle>\n    \u003Cmeta name=\"msapplication-TileColor\" content=\"#ffffff\">\n    \u003Cmeta name=\"theme-color\" content=\"#ffffff\">\n    \u003Cscript>\n      ;(function () {\n        const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches\n        const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'\n        if (setting === 'dark' || (prefersDark && setting !== 'light'))\n          document.documentElement.classList.toggle('dark', true)\n      })()\n    \u003C\u002Fscript>\n    \u003Cscript type=\"module\" crossorigin=\"\" src=\"\u002Fassets\u002Fapp-IQH-kuhC.js\">\u003C\u002Fscript>\n    \u003Clink rel=\"stylesheet\" crossorigin=\"\" href=\"\u002Fassets\u002Fapp-B27WEjTb.css\">\n    \u003Clink rel=\"modulepreload\" crossorigin=\"\" href=\"\u002Fassets\u002Fpages-DeF1Ma75.js\">\n    \u003Clink rel=\"modulepreload\" crossorigin=\"\" href=\"\u002Fassets\u002FWrapperContent-BVl8okbX.js\">\n    \u003Cmeta property=\"og:title\" content=\"Estéban's Infra\">\n    \u003Cmeta name=\"twitter:title\" content=\"Estéban's Infra\">\n  \u003C\u002Fhead>\n\u003C\u002Fhtml>\n","dist\u002Findex.html","html",[134,539,540,563,572,618,696,716,748,778,798,828,857,866,881,927,966,1011,1049,1058,1068,1113,1149,1186,1222,1254,1284,1294],{"__ignoreMap":132},[137,541,542,545,547,550,552,555,558,560],{"class":139,"line":140},[137,543,544],{"class":154},"\u003C",[137,546,537],{"class":282},[137,548,549],{"class":219}," lang",[137,551,226],{"class":154},[137,553,554],{"class":154},"\"",[137,556,557],{"class":158},"en",[137,559,554],{"class":154},[137,561,562],{"class":154},">\n",[137,564,565,568,570],{"class":139,"line":165},[137,566,567],{"class":154},"  \u003C",[137,569,530],{"class":282},[137,571,562],{"class":154},[137,573,574,577,580,583,585,587,590,592,595,597,599,602,604,607,609,611,614,616],{"class":139,"line":189},[137,575,576],{"class":154},"    \u003C",[137,578,579],{"class":282},"link",[137,581,582],{"class":219}," rel",[137,584,226],{"class":154},[137,586,554],{"class":154},[137,588,589],{"class":158},"preconnect",[137,591,554],{"class":154},[137,593,594],{"class":219}," href",[137,596,226],{"class":154},[137,598,554],{"class":154},[137,600,601],{"class":158},"https:\u002F\u002Ffonts.gstatic.com\u002F",[137,603,554],{"class":154},[137,605,606],{"class":219}," crossorigin",[137,608,226],{"class":154},[137,610,554],{"class":154},[137,612,613],{"class":158},"anonymous",[137,615,554],{"class":154},[137,617,562],{"class":154},[137,619,620,622,624,626,628,630,633,635,638,640,642,645,647,650,652,655,658,661,664,667,669,671,673,676,679,682,684,687,689,692,694],{"class":139,"line":206},[137,621,576],{"class":154},[137,623,579],{"class":282},[137,625,582],{"class":219},[137,627,226],{"class":154},[137,629,554],{"class":154},[137,631,632],{"class":158},"preload",[137,634,554],{"class":154},[137,636,637],{"class":219}," as",[137,639,226],{"class":154},[137,641,554],{"class":154},[137,643,644],{"class":158},"style",[137,646,554],{"class":154},[137,648,649],{"class":219}," onload",[137,651,226],{"class":154},[137,653,654],{"class":154},"\"this.",[137,656,657],{"class":147},"rel",[137,659,660],{"class":154},"='",[137,662,663],{"class":158},"stylesheet",[137,665,666],{"class":154},"'\"",[137,668,594],{"class":219},[137,670,226],{"class":154},[137,672,554],{"class":154},[137,674,675],{"class":158},"https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=DM Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000",[137,677,678],{"class":147},"&amp;",[137,680,681],{"class":158},"family=DM Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500",[137,683,678],{"class":147},[137,685,686],{"class":158},"family=Sofia Sans:ital,wght@0,1..1000;1,1..1000",[137,688,678],{"class":147},[137,690,691],{"class":158},"display=swap",[137,693,554],{"class":154},[137,695,562],{"class":154},[137,697,698,700,702,705,707,709,712,714],{"class":139,"line":213},[137,699,576],{"class":154},[137,701,426],{"class":282},[137,703,704],{"class":219}," charset",[137,706,226],{"class":154},[137,708,554],{"class":154},[137,710,711],{"class":158},"utf-8",[137,713,554],{"class":154},[137,715,562],{"class":154},[137,717,718,720,722,725,727,729,732,734,737,739,741,744,746],{"class":139,"line":235},[137,719,576],{"class":154},[137,721,426],{"class":282},[137,723,724],{"class":219}," name",[137,726,226],{"class":154},[137,728,554],{"class":154},[137,730,731],{"class":158},"author",[137,733,554],{"class":154},[137,735,736],{"class":219}," content",[137,738,226],{"class":154},[137,740,554],{"class":154},[137,742,743],{"class":158},"Estéban Soubiran",[137,745,554],{"class":154},[137,747,562],{"class":154},[137,749,750,752,754,756,758,760,763,765,767,769,771,774,776],{"class":139,"line":244},[137,751,576],{"class":154},[137,753,426],{"class":282},[137,755,724],{"class":219},[137,757,226],{"class":154},[137,759,554],{"class":154},[137,761,762],{"class":158},"viewport",[137,764,554],{"class":154},[137,766,736],{"class":219},[137,768,226],{"class":154},[137,770,554],{"class":154},[137,772,773],{"class":158},"width=device-width, initial-scale=1",[137,775,554],{"class":154},[137,777,562],{"class":154},[137,779,780,782,785,788,791,794,796],{"class":139,"line":250},[137,781,576],{"class":154},[137,783,784],{"class":282},"title",[137,786,787],{"class":154},">",[137,789,790],{"class":147},"Estéban's Infra · Estéban Soubiran",[137,792,793],{"class":154},"\u003C\u002F",[137,795,784],{"class":282},[137,797,562],{"class":154},[137,799,800,802,804,806,808,810,813,815,817,819,821,824,826],{"class":139,"line":271},[137,801,576],{"class":154},[137,803,426],{"class":282},[137,805,724],{"class":219},[137,807,226],{"class":154},[137,809,554],{"class":154},[137,811,812],{"class":158},"msapplication-TileColor",[137,814,554],{"class":154},[137,816,736],{"class":219},[137,818,226],{"class":154},[137,820,554],{"class":154},[137,822,823],{"class":158},"#ffffff",[137,825,554],{"class":154},[137,827,562],{"class":154},[137,829,830,832,834,836,838,840,843,845,847,849,851,853,855],{"class":139,"line":292},[137,831,576],{"class":154},[137,833,426],{"class":282},[137,835,724],{"class":219},[137,837,226],{"class":154},[137,839,554],{"class":154},[137,841,842],{"class":158},"theme-color",[137,844,554],{"class":154},[137,846,736],{"class":219},[137,848,226],{"class":154},[137,850,554],{"class":154},[137,852,823],{"class":158},[137,854,554],{"class":154},[137,856,562],{"class":154},[137,858,859,861,864],{"class":139,"line":298},[137,860,576],{"class":154},[137,862,863],{"class":282},"script",[137,865,562],{"class":154},[137,867,868,871,873,876,879],{"class":139,"line":465},[137,869,870],{"class":154},"      ;",[137,872,283],{"class":147},[137,874,875],{"class":219},"function",[137,877,878],{"class":154}," ()",[137,880,266],{"class":154},[137,882,883,886,889,892,895,897,900,903,905,907,909,911,914,917,919,922,924],{"class":139,"line":470},[137,884,885],{"class":219},"        const",[137,887,888],{"class":147}," prefersDark",[137,890,891],{"class":154}," =",[137,893,894],{"class":147}," window",[137,896,115],{"class":154},[137,898,899],{"class":147},"matchMedia",[137,901,902],{"class":154}," &&",[137,904,894],{"class":147},[137,906,115],{"class":154},[137,908,899],{"class":229},[137,910,283],{"class":282},[137,912,913],{"class":154},"'",[137,915,916],{"class":158},"(prefers-color-scheme: dark)",[137,918,913],{"class":154},[137,920,921],{"class":282},")",[137,923,115],{"class":154},[137,925,926],{"class":147},"matches\n",[137,928,930,932,935,937,940,942,945,947,949,952,954,956,959,961,964],{"class":139,"line":929},14,[137,931,885],{"class":219},[137,933,934],{"class":147}," setting",[137,936,891],{"class":154},[137,938,939],{"class":147}," localStorage",[137,941,115],{"class":154},[137,943,944],{"class":229},"getItem",[137,946,283],{"class":282},[137,948,913],{"class":154},[137,950,951],{"class":158},"vueuse-color-scheme",[137,953,913],{"class":154},[137,955,289],{"class":282},[137,957,958],{"class":154},"||",[137,960,155],{"class":154},[137,962,963],{"class":158},"auto",[137,965,162],{"class":154},[137,967,969,972,974,977,980,982,984,986,989,991,994,996,998,1001,1003,1006,1008],{"class":139,"line":968},15,[137,970,971],{"class":143},"        if",[137,973,419],{"class":282},[137,975,976],{"class":147},"setting",[137,978,979],{"class":154}," ===",[137,981,155],{"class":154},[137,983,520],{"class":158},[137,985,913],{"class":154},[137,987,988],{"class":154}," ||",[137,990,419],{"class":282},[137,992,993],{"class":147},"prefersDark",[137,995,902],{"class":154},[137,997,934],{"class":147},[137,999,1000],{"class":154}," !==",[137,1002,155],{"class":154},[137,1004,1005],{"class":158},"light",[137,1007,913],{"class":154},[137,1009,1010],{"class":282},"))\n",[137,1012,1014,1017,1019,1022,1024,1027,1029,1032,1034,1036,1038,1040,1043,1047],{"class":139,"line":1013},16,[137,1015,1016],{"class":147},"          document",[137,1018,115],{"class":154},[137,1020,1021],{"class":147},"documentElement",[137,1023,115],{"class":154},[137,1025,1026],{"class":147},"classList",[137,1028,115],{"class":154},[137,1030,1031],{"class":229},"toggle",[137,1033,283],{"class":282},[137,1035,913],{"class":154},[137,1037,520],{"class":158},[137,1039,913],{"class":154},[137,1041,1042],{"class":154},",",[137,1044,1046],{"class":1045},"sfNiH"," true",[137,1048,301],{"class":282},[137,1050,1052,1055],{"class":139,"line":1051},17,[137,1053,1054],{"class":154},"      }",[137,1056,1057],{"class":147},")()\n",[137,1059,1061,1064,1066],{"class":139,"line":1060},18,[137,1062,1063],{"class":154},"    \u003C\u002F",[137,1065,863],{"class":282},[137,1067,562],{"class":154},[137,1069,1071,1073,1075,1078,1080,1082,1085,1087,1089,1091,1094,1097,1099,1101,1104,1106,1109,1111],{"class":139,"line":1070},19,[137,1072,576],{"class":154},[137,1074,863],{"class":282},[137,1076,1077],{"class":219}," type",[137,1079,226],{"class":154},[137,1081,554],{"class":154},[137,1083,1084],{"class":158},"module",[137,1086,554],{"class":154},[137,1088,606],{"class":219},[137,1090,226],{"class":154},[137,1092,1093],{"class":154},"\"\"",[137,1095,1096],{"class":219}," src",[137,1098,226],{"class":154},[137,1100,554],{"class":154},[137,1102,1103],{"class":158},"\u002Fassets\u002Fapp-IQH-kuhC.js",[137,1105,554],{"class":154},[137,1107,1108],{"class":154},">\u003C\u002F",[137,1110,863],{"class":282},[137,1112,562],{"class":154},[137,1114,1116,1118,1120,1122,1124,1126,1128,1130,1132,1134,1136,1138,1140,1142,1145,1147],{"class":139,"line":1115},20,[137,1117,576],{"class":154},[137,1119,579],{"class":282},[137,1121,582],{"class":219},[137,1123,226],{"class":154},[137,1125,554],{"class":154},[137,1127,663],{"class":158},[137,1129,554],{"class":154},[137,1131,606],{"class":219},[137,1133,226],{"class":154},[137,1135,1093],{"class":154},[137,1137,594],{"class":219},[137,1139,226],{"class":154},[137,1141,554],{"class":154},[137,1143,1144],{"class":158},"\u002Fassets\u002Fapp-B27WEjTb.css",[137,1146,554],{"class":154},[137,1148,562],{"class":154},[137,1150,1152,1154,1156,1158,1160,1162,1165,1167,1169,1171,1173,1175,1177,1179,1182,1184],{"class":139,"line":1151},21,[137,1153,576],{"class":154},[137,1155,579],{"class":282},[137,1157,582],{"class":219},[137,1159,226],{"class":154},[137,1161,554],{"class":154},[137,1163,1164],{"class":158},"modulepreload",[137,1166,554],{"class":154},[137,1168,606],{"class":219},[137,1170,226],{"class":154},[137,1172,1093],{"class":154},[137,1174,594],{"class":219},[137,1176,226],{"class":154},[137,1178,554],{"class":154},[137,1180,1181],{"class":158},"\u002Fassets\u002Fpages-DeF1Ma75.js",[137,1183,554],{"class":154},[137,1185,562],{"class":154},[137,1187,1189,1191,1193,1195,1197,1199,1201,1203,1205,1207,1209,1211,1213,1215,1218,1220],{"class":139,"line":1188},22,[137,1190,576],{"class":154},[137,1192,579],{"class":282},[137,1194,582],{"class":219},[137,1196,226],{"class":154},[137,1198,554],{"class":154},[137,1200,1164],{"class":158},[137,1202,554],{"class":154},[137,1204,606],{"class":219},[137,1206,226],{"class":154},[137,1208,1093],{"class":154},[137,1210,594],{"class":219},[137,1212,226],{"class":154},[137,1214,554],{"class":154},[137,1216,1217],{"class":158},"\u002Fassets\u002FWrapperContent-BVl8okbX.js",[137,1219,554],{"class":154},[137,1221,562],{"class":154},[137,1223,1225,1227,1229,1232,1234,1236,1239,1241,1243,1245,1247,1250,1252],{"class":139,"line":1224},23,[137,1226,576],{"class":154},[137,1228,426],{"class":282},[137,1230,1231],{"class":219}," property",[137,1233,226],{"class":154},[137,1235,554],{"class":154},[137,1237,1238],{"class":158},"og:title",[137,1240,554],{"class":154},[137,1242,736],{"class":219},[137,1244,226],{"class":154},[137,1246,554],{"class":154},[137,1248,1249],{"class":158},"Estéban's Infra",[137,1251,554],{"class":154},[137,1253,562],{"class":154},[137,1255,1257,1259,1261,1263,1265,1267,1270,1272,1274,1276,1278,1280,1282],{"class":139,"line":1256},24,[137,1258,576],{"class":154},[137,1260,426],{"class":282},[137,1262,724],{"class":219},[137,1264,226],{"class":154},[137,1266,554],{"class":154},[137,1268,1269],{"class":158},"twitter:title",[137,1271,554],{"class":154},[137,1273,736],{"class":219},[137,1275,226],{"class":154},[137,1277,554],{"class":154},[137,1279,1249],{"class":158},[137,1281,554],{"class":154},[137,1283,562],{"class":154},[137,1285,1287,1290,1292],{"class":139,"line":1286},25,[137,1288,1289],{"class":154},"  \u003C\u002F",[137,1291,530],{"class":282},[137,1293,562],{"class":154},[137,1295,1297,1299,1301],{"class":139,"line":1296},26,[137,1298,793],{"class":154},[137,1300,537],{"class":282},[137,1302,562],{"class":154},[10,1304,1305],{},"And I tried to understand what was happening.",[10,1307,1308,1309,1314],{},"To know if the head is correctly sorted, you can use ",[32,1310,1313],{"href":1311,"rel":1312},"https:\u002F\u002Frviscomi.github.io\u002Fcapo.js\u002F",[36],"capo.js",". After a quick inspection, I had the following result:",[10,1316,1317],{},[1318,1319],"img",{"alt":1320,"src":1321},"capo.js result showing head issues","\u002Fposts\u002Fwhant-to-contribute-to-open-source-youre-doing-it-wrong\u002Fcapo-js-result.webp",[10,1323,1324,1325,1330,1331,1336],{},"In the Vite SSG documentation, it says to use ",[32,1326,1329],{"href":1327,"rel":1328},"https:\u002F\u002Funhead.unjs.io",[36],"Unhead",", so I read all the documentation about it. I discovered that Unhead automatically ",[32,1332,1335],{"href":1333,"rel":1334},"https:\u002F\u002Funhead.unjs.io\u002Fdocs\u002Fvue\u002Fhead\u002Fguides\u002Fcore-concepts\u002Fpositions#sort-order",[36],"sorts the head"," elements, but if it's already used, why isn't the head correctly sorted?",[100,1338,1340],{"id":1339},"everything-didnt-go-as-planned","Everything didn't go as planned",[10,1342,1343],{},"If Unhead is supposed to sort the head elements, could the problem be in Vite SSG?",[10,1345,1346],{},"Only one way to find out: debugging.",[10,1348,1349],{},"After reading the whole source code of Vite SSG, I found an interesting part:",[126,1351,1354],{"className":128,"code":1352,"filename":1353,"language":131,"meta":132,"style":132},"\u002F\u002F create jsdom from renderedHTML\nconst jsdom = new JSDOM(renderedHTML)\n\n\u002F\u002F render current page's preloadLinks\nrenderPreloadLinks(jsdom.window.document, ctx.modules || new Set\u003Cstring>(), ssrManifest)\n\n\u002F\u002F render head\nif (head) \u002F\u002F [!code highlight]\n  await renderDOMHead(head, { document: jsdom.window.document }) \u002F\u002F [!code highlight]\n\nconst html = jsdom.serialize()\nlet transformed = (await onPageRendered?.(route, html, appCtx)) || html\nif (beasties)\n  transformed = await beasties.process(transformed)\n\nconst formatted = await formatHtml(transformed, formatting)\n","src\u002Fnode\u002Fbuild.ts",[134,1355,1356,1362,1381,1385,1390,1440,1444,1449,1458,1497,1501,1520,1559,1566,1587,1591],{"__ignoreMap":132},[137,1357,1358],{"class":139,"line":140},[137,1359,1361],{"class":1360},"sHwdD","\u002F\u002F create jsdom from renderedHTML\n",[137,1363,1364,1367,1370,1372,1375,1378],{"class":139,"line":165},[137,1365,1366],{"class":219},"const",[137,1368,1369],{"class":147}," jsdom ",[137,1371,226],{"class":154},[137,1373,1374],{"class":154}," new",[137,1376,1377],{"class":229}," JSDOM",[137,1379,1380],{"class":147},"(renderedHTML)\n",[137,1382,1383],{"class":139,"line":189},[137,1384,210],{"emptyLinePlaceholder":209},[137,1386,1387],{"class":139,"line":206},[137,1388,1389],{"class":1360},"\u002F\u002F render current page's preloadLinks\n",[137,1391,1392,1395,1398,1400,1403,1405,1407,1409,1412,1414,1417,1419,1421,1424,1426,1430,1432,1435,1437],{"class":139,"line":213},[137,1393,1394],{"class":229},"renderPreloadLinks",[137,1396,1397],{"class":147},"(jsdom",[137,1399,115],{"class":154},[137,1401,1402],{"class":147},"window",[137,1404,115],{"class":154},[137,1406,307],{"class":147},[137,1408,1042],{"class":154},[137,1410,1411],{"class":147}," ctx",[137,1413,115],{"class":154},[137,1415,1416],{"class":147},"modules ",[137,1418,958],{"class":154},[137,1420,1374],{"class":154},[137,1422,1423],{"class":229}," Set",[137,1425,544],{"class":154},[137,1427,1429],{"class":1428},"sBMFI","string",[137,1431,787],{"class":154},[137,1433,1434],{"class":147},"()",[137,1436,1042],{"class":154},[137,1438,1439],{"class":147}," ssrManifest)\n",[137,1441,1442],{"class":139,"line":235},[137,1443,210],{"emptyLinePlaceholder":209},[137,1445,1446],{"class":139,"line":244},[137,1447,1448],{"class":1360},"\u002F\u002F render head\n",[137,1450,1452,1455],{"class":1451,"line":250},[139,270],[137,1453,1454],{"class":143},"if",[137,1456,1457],{"class":147}," (head) ",[137,1459,1461,1464,1467,1470,1472,1474,1477,1480,1483,1485,1487,1489,1492,1495],{"class":1460,"line":271},[139,270],[137,1462,1463],{"class":143},"  await",[137,1465,1466],{"class":229}," renderDOMHead",[137,1468,1469],{"class":147},"(head",[137,1471,1042],{"class":154},[137,1473,170],{"class":154},[137,1475,1476],{"class":282}," document",[137,1478,1479],{"class":154},":",[137,1481,1482],{"class":147}," jsdom",[137,1484,115],{"class":154},[137,1486,1402],{"class":147},[137,1488,115],{"class":154},[137,1490,1491],{"class":147},"document ",[137,1493,1494],{"class":154},"}",[137,1496,289],{"class":147},[137,1498,1499],{"class":139,"line":292},[137,1500,210],{"emptyLinePlaceholder":209},[137,1502,1503,1505,1508,1510,1512,1514,1517],{"class":139,"line":298},[137,1504,1366],{"class":219},[137,1506,1507],{"class":147}," html ",[137,1509,226],{"class":154},[137,1511,1482],{"class":147},[137,1513,115],{"class":154},[137,1515,1516],{"class":229},"serialize",[137,1518,1519],{"class":147},"()\n",[137,1521,1522,1525,1528,1530,1532,1535,1538,1541,1544,1546,1549,1551,1554,1556],{"class":139,"line":465},[137,1523,1524],{"class":219},"let",[137,1526,1527],{"class":147}," transformed ",[137,1529,226],{"class":154},[137,1531,419],{"class":147},[137,1533,1534],{"class":143},"await",[137,1536,1537],{"class":229}," onPageRendered",[137,1539,1540],{"class":154},"?.",[137,1542,1543],{"class":147},"(route",[137,1545,1042],{"class":154},[137,1547,1548],{"class":147}," html",[137,1550,1042],{"class":154},[137,1552,1553],{"class":147}," appCtx)) ",[137,1555,958],{"class":154},[137,1557,1558],{"class":147}," html\n",[137,1560,1561,1563],{"class":139,"line":470},[137,1562,1454],{"class":143},[137,1564,1565],{"class":147}," (beasties)\n",[137,1567,1568,1571,1573,1576,1579,1581,1584],{"class":139,"line":929},[137,1569,1570],{"class":147},"  transformed ",[137,1572,226],{"class":154},[137,1574,1575],{"class":143}," await",[137,1577,1578],{"class":147}," beasties",[137,1580,115],{"class":154},[137,1582,1583],{"class":229},"process",[137,1585,1586],{"class":147},"(transformed)\n",[137,1588,1589],{"class":139,"line":968},[137,1590,210],{"emptyLinePlaceholder":209},[137,1592,1593,1595,1598,1600,1602,1605,1608,1610],{"class":139,"line":1013},[137,1594,1366],{"class":219},[137,1596,1597],{"class":147}," formatted ",[137,1599,226],{"class":154},[137,1601,1575],{"class":143},[137,1603,1604],{"class":229}," formatHtml",[137,1606,1607],{"class":147},"(transformed",[137,1609,1042],{"class":154},[137,1611,1612],{"class":147}," formatting)\n",[10,1614,1615,1616,1619],{},"This part is responsible for generating the HTML before being saved. The interesting line is ",[134,1617,1618],{},"await renderDOMHead(head, { document: jsdom.window.document })",". This is the only part where the head is used and rendered inside the HTML.",[10,1621,1622,1623,1626],{},"More interestingly, the function comes from ",[134,1624,1625],{},"@unhead\u002Fdom",". Strange, because we're using SSR, so having a front-end function on the server looks suspicious.",[10,1628,1629],{},"And I was right. This function doesn't optimize the head. It's supposed to change the head elements once the page is loaded, on the client. Bringing the whole sorting logic to the client side is completely pointless.",[10,1631,1632],{},"While reading the Unhead documentation, I discovered the recommended way to use it with SSR.",[126,1634,1637],{"className":128,"code":1635,"filename":1636,"language":131,"meta":132,"style":132},"import { transformHtmlTemplate } from '@unhead\u002Fvue\u002Fserver' \u002F\u002F [!code highlight]\n\nconst rendered = await render(url)\n\nconst html = await transformHtmlTemplate( \u002F\u002F [!code highlight]\n  rendered.head,\n  template.replace(`\u003C!--app-html-->`, rendered.html ?? '')\n)\n","server.ts",[134,1638,1639,1660,1664,1681,1685,1701,1712,1750],{"__ignoreMap":132},[137,1640,1642,1644,1646,1649,1651,1653,1655,1658],{"class":1641,"line":140},[139,270],[137,1643,144],{"class":143},[137,1645,170],{"class":154},[137,1647,1648],{"class":147}," transformHtmlTemplate",[137,1650,176],{"class":154},[137,1652,179],{"class":143},[137,1654,155],{"class":154},[137,1656,1657],{"class":158},"@unhead\u002Fvue\u002Fserver",[137,1659,913],{"class":154},[137,1661,1662],{"class":139,"line":165},[137,1663,210],{"emptyLinePlaceholder":209},[137,1665,1666,1668,1671,1673,1675,1678],{"class":139,"line":189},[137,1667,1366],{"class":219},[137,1669,1670],{"class":147}," rendered ",[137,1672,226],{"class":154},[137,1674,1575],{"class":143},[137,1676,1677],{"class":229}," render",[137,1679,1680],{"class":147},"(url)\n",[137,1682,1683],{"class":139,"line":206},[137,1684,210],{"emptyLinePlaceholder":209},[137,1686,1688,1690,1692,1694,1696,1698],{"class":1687,"line":213},[139,270],[137,1689,1366],{"class":219},[137,1691,1507],{"class":147},[137,1693,226],{"class":154},[137,1695,1575],{"class":143},[137,1697,1648],{"class":229},[137,1699,1700],{"class":147},"( ",[137,1702,1703,1706,1708,1710],{"class":139,"line":235},[137,1704,1705],{"class":147},"  rendered",[137,1707,115],{"class":154},[137,1709,530],{"class":147},[137,1711,241],{"class":154},[137,1713,1714,1717,1719,1722,1724,1727,1730,1732,1734,1737,1739,1742,1745,1748],{"class":139,"line":244},[137,1715,1716],{"class":147},"  template",[137,1718,115],{"class":154},[137,1720,1721],{"class":229},"replace",[137,1723,283],{"class":147},[137,1725,1726],{"class":154},"`",[137,1728,1729],{"class":158},"\u003C!--app-html-->",[137,1731,1726],{"class":154},[137,1733,1042],{"class":154},[137,1735,1736],{"class":147}," rendered",[137,1738,115],{"class":154},[137,1740,1741],{"class":147},"html ",[137,1743,1744],{"class":154},"??",[137,1746,1747],{"class":154}," ''",[137,1749,301],{"class":147},[137,1751,1752],{"class":139,"line":250},[137,1753,301],{"class":147},[10,1755,1756,1757,1760],{},"The function ",[134,1758,1759],{},"transformHtmlTemplate"," extracts the current head elements from the template and injects them into the head before re-rendering the HTML and injecting it into the page. This ensures that the head is correctly sorted before sending it to the client.",[10,1762,1763],{},"It looked like a good solution to fix the head sorting issue. This led me to create a PR to Vite SSG to improve the integration with Unhead.",[10,1765,1766],{},[32,1767,1770],{"href":1768,"rel":1769},"https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Fvite-ssg\u002Fpull\u002F464",[36],"refactor: uses unhead\u002Fserver to optimize head",[10,1772,1773],{},"In order to quickly use this fix in my project, I patched Vite SSG locally. This allowed me to continue developing my project without waiting for my PR to be merged and then for a release.",[10,1775,1776],{},"But not everything went as planned.",[10,1778,1779,1780],{},"The head was, nearly, well-ordered, but after that fix, the FOUC was still there. ",[40,1781,1782],{},"Sad.",[10,1784,1785],{},"It means that the problem isn't related to the head sorting.",[100,1787,1789],{"id":1788},"deeper-investigation","Deeper investigation",[10,1791,1792],{},"The Vite SSG patch isn't useless, as it improves the head sorting. However, it didn't solve the FOUC issue. I had to dig deeper.",[10,1794,1795],{},"I continued to look for the root cause. I tried to analyze the application performance to identify any bottlenecks, race conditions, or other issues that could be causing the FOUC. I didn't find anything.",[10,1797,1798],{},"I inspected the CSS and found that the styles from the prose were correctly applied, but not the ones from Nuxt UI. If you look closely at the video, you'll see the title color change from black to white, but the text underneath is already white. The CSS variables for Nuxt UI aren't available on the first render but are applied when the JavaScript loads.",[10,1800,1801,1802,1805],{},"To confirm this suspicion, I checked the Nuxt UI source code where I found a Vue plugin named ",[134,1803,1804],{},"colors.ts",". The same that caused the build issue at the beginning.",[10,1807,1808],{},"Everything was leading to this plugin, so I took the time to understand it, and I found the root cause of both the FOUC and the build issue:",[126,1810,1814],{"className":1811,"code":1812,"filename":1813,"language":131,"meta":132,"style":132},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const headData: UseHeadInput = {\n  style: [{\n    innerHTML: () => root.value,\n    tagPriority: -2,\n    id: 'nuxt-ui-colors'\n  }]\n}\n\nif (import.meta.client && nuxtApp.isHydrating && !nuxtApp.payload.serverRendered) {\n  const style = document.createElement('style')\n\n  style.innerHTML = root.value\n  style.setAttribute('data-nuxt-ui-colors', '')\n  document.head.appendChild(style)\n\n  headData.script = [{\n    innerHTML: 'document.head.removeChild(document.querySelector(\\'[data-nuxt-ui-colors]\\'))'\n  }]\n}\n\nuseHead(headData)\n","src\u002Fruntime\u002Fplugins\u002Fcolors.ts",[134,1815,1816,1832,1845,1866,1882,1896,1904,1909,1913,1961,1988,1992,2010,2034,2054,2058,2073,2097,2103,2107,2111],{"__ignoreMap":132},[137,1817,1818,1820,1823,1825,1828,1830],{"class":139,"line":140},[137,1819,1366],{"class":219},[137,1821,1822],{"class":147}," headData",[137,1824,1479],{"class":154},[137,1826,1827],{"class":1428}," UseHeadInput",[137,1829,891],{"class":154},[137,1831,266],{"class":154},[137,1833,1834,1837,1839,1842],{"class":139,"line":165},[137,1835,1836],{"class":282},"  style",[137,1838,1479],{"class":154},[137,1840,1841],{"class":147}," [",[137,1843,1844],{"class":154},"{\n",[137,1846,1847,1850,1852,1854,1856,1859,1861,1864],{"class":139,"line":189},[137,1848,1849],{"class":229},"    innerHTML",[137,1851,1479],{"class":154},[137,1853,878],{"class":154},[137,1855,263],{"class":219},[137,1857,1858],{"class":147}," root",[137,1860,115],{"class":154},[137,1862,1863],{"class":147},"value",[137,1865,241],{"class":154},[137,1867,1868,1871,1873,1876,1880],{"class":139,"line":206},[137,1869,1870],{"class":282},"    tagPriority",[137,1872,1479],{"class":154},[137,1874,1875],{"class":154}," -",[137,1877,1879],{"class":1878},"sbssI","2",[137,1881,241],{"class":154},[137,1883,1884,1887,1889,1891,1894],{"class":139,"line":213},[137,1885,1886],{"class":282},"    id",[137,1888,1479],{"class":154},[137,1890,155],{"class":154},[137,1892,1893],{"class":158},"nuxt-ui-colors",[137,1895,162],{"class":154},[137,1897,1898,1901],{"class":139,"line":235},[137,1899,1900],{"class":154},"  }",[137,1902,1903],{"class":147},"]\n",[137,1905,1906],{"class":139,"line":244},[137,1907,1908],{"class":154},"}\n",[137,1910,1911],{"class":139,"line":250},[137,1912,210],{"emptyLinePlaceholder":209},[137,1914,1915,1917,1919,1921,1923,1925,1927,1930,1933,1936,1938,1941,1943,1946,1949,1951,1954,1956,1959],{"class":139,"line":271},[137,1916,1454],{"class":143},[137,1918,419],{"class":147},[137,1920,144],{"class":143},[137,1922,115],{"class":154},[137,1924,426],{"class":147},[137,1926,115],{"class":154},[137,1928,1929],{"class":147},"client ",[137,1931,1932],{"class":154},"&&",[137,1934,1935],{"class":147}," nuxtApp",[137,1937,115],{"class":154},[137,1939,1940],{"class":147},"isHydrating ",[137,1942,1932],{"class":154},[137,1944,1945],{"class":154}," !",[137,1947,1948],{"class":147},"nuxtApp",[137,1950,115],{"class":154},[137,1952,1953],{"class":147},"payload",[137,1955,115],{"class":154},[137,1957,1958],{"class":147},"serverRendered) ",[137,1960,1844],{"class":154},[137,1962,1963,1966,1969,1971,1973,1975,1978,1980,1982,1984,1986],{"class":139,"line":292},[137,1964,1965],{"class":219},"  const",[137,1967,1968],{"class":147}," style",[137,1970,891],{"class":154},[137,1972,1476],{"class":147},[137,1974,115],{"class":154},[137,1976,1977],{"class":229},"createElement",[137,1979,283],{"class":282},[137,1981,913],{"class":154},[137,1983,644],{"class":158},[137,1985,913],{"class":154},[137,1987,301],{"class":282},[137,1989,1990],{"class":139,"line":298},[137,1991,210],{"emptyLinePlaceholder":209},[137,1993,1994,1996,1998,2001,2003,2005,2007],{"class":139,"line":465},[137,1995,1836],{"class":147},[137,1997,115],{"class":154},[137,1999,2000],{"class":147},"innerHTML",[137,2002,891],{"class":154},[137,2004,1858],{"class":147},[137,2006,115],{"class":154},[137,2008,2009],{"class":147},"value\n",[137,2011,2012,2014,2016,2019,2021,2023,2026,2028,2030,2032],{"class":139,"line":470},[137,2013,1836],{"class":147},[137,2015,115],{"class":154},[137,2017,2018],{"class":229},"setAttribute",[137,2020,283],{"class":282},[137,2022,913],{"class":154},[137,2024,2025],{"class":158},"data-nuxt-ui-colors",[137,2027,913],{"class":154},[137,2029,1042],{"class":154},[137,2031,1747],{"class":154},[137,2033,301],{"class":282},[137,2035,2036,2039,2041,2043,2045,2048,2050,2052],{"class":139,"line":929},[137,2037,2038],{"class":147},"  document",[137,2040,115],{"class":154},[137,2042,530],{"class":147},[137,2044,115],{"class":154},[137,2046,2047],{"class":229},"appendChild",[137,2049,283],{"class":282},[137,2051,644],{"class":147},[137,2053,301],{"class":282},[137,2055,2056],{"class":139,"line":968},[137,2057,210],{"emptyLinePlaceholder":209},[137,2059,2060,2063,2065,2067,2069,2071],{"class":139,"line":1013},[137,2061,2062],{"class":147},"  headData",[137,2064,115],{"class":154},[137,2066,863],{"class":147},[137,2068,891],{"class":154},[137,2070,1841],{"class":282},[137,2072,1844],{"class":154},[137,2074,2075,2077,2079,2081,2084,2087,2090,2092,2095],{"class":139,"line":1051},[137,2076,1849],{"class":282},[137,2078,1479],{"class":154},[137,2080,155],{"class":154},[137,2082,2083],{"class":158},"document.head.removeChild(document.querySelector(",[137,2085,2086],{"class":147},"\\'",[137,2088,2089],{"class":158},"[data-nuxt-ui-colors]",[137,2091,2086],{"class":147},[137,2093,2094],{"class":158},"))",[137,2096,162],{"class":154},[137,2098,2099,2101],{"class":139,"line":1060},[137,2100,1900],{"class":154},[137,2102,1903],{"class":282},[137,2104,2105],{"class":139,"line":1070},[137,2106,1908],{"class":154},[137,2108,2109],{"class":139,"line":1115},[137,2110,210],{"emptyLinePlaceholder":209},[137,2112,2113,2116],{"class":139,"line":1151},[137,2114,2115],{"class":229},"useHead",[137,2117,2118],{"class":147},"(headData)\n",[10,2120,2121],{},"There are multiple issues at play here.",[10,2123,2124],{},"The first seven lines, along with the last, are used to inject the Nuxt UI CSS variables into the head, at both build time and runtime. By disabling the plugin, I removed this behavior, so the FOUC inevitably occurs.",[10,2126,2127,2128,2130],{},"The ",[134,2129,1454],{}," block creates a style element and must only be executed on the client side, during hydration, if the content is not server-rendered. My app is using Vite SSG to be server-rendered, so this should not happen.",[10,2132,2133,2134,2136],{},"So, what is this ",[134,2135,1948],{},"? Let's continue to investigate.",[10,2138,2139,2140,2145,2146,2149],{},"At first, Nuxt UI is made for Nuxt, but thanks to the work of ",[32,2141,2144],{"href":2142,"rel":2143},"https:\u002F\u002Fgithub.com\u002Fdanielroe",[36],"Daniel Roe"," and a lot of stubs, Nuxt UI is also usable outside of Nuxt. One of the stubs is the ",[134,2147,2148],{},"useNuxtApp"," composable:",[126,2151,2154],{"className":1811,"code":2152,"filename":2153,"language":131,"meta":132,"style":132},"export function useNuxtApp() {\n  return {\n    isHydrating: true,\n    payload: { serverRendered: false },\n    hooks,\n    hook: hooks.hook\n  }\n}\n","src\u002Fruntime\u002Fvue\u002Fstubs.ts",[134,2155,2156,2170,2177,2188,2208,2215,2230,2235],{"__ignoreMap":132},[137,2157,2158,2160,2163,2166,2168],{"class":139,"line":140},[137,2159,216],{"class":143},[137,2161,2162],{"class":219}," function",[137,2164,2165],{"class":229}," useNuxtApp",[137,2167,1434],{"class":154},[137,2169,266],{"class":154},[137,2171,2172,2175],{"class":139,"line":165},[137,2173,2174],{"class":143},"  return",[137,2176,266],{"class":154},[137,2178,2179,2182,2184,2186],{"class":139,"line":189},[137,2180,2181],{"class":282},"    isHydrating",[137,2183,1479],{"class":154},[137,2185,1046],{"class":1045},[137,2187,241],{"class":154},[137,2189,2190,2193,2195,2197,2200,2202,2205],{"class":139,"line":206},[137,2191,2192],{"class":282},"    payload",[137,2194,1479],{"class":154},[137,2196,170],{"class":154},[137,2198,2199],{"class":282}," serverRendered",[137,2201,1479],{"class":154},[137,2203,2204],{"class":1045}," false",[137,2206,2207],{"class":154}," },\n",[137,2209,2210,2213],{"class":139,"line":213},[137,2211,2212],{"class":147},"    hooks",[137,2214,241],{"class":154},[137,2216,2217,2220,2222,2225,2227],{"class":139,"line":235},[137,2218,2219],{"class":282},"    hook",[137,2221,1479],{"class":154},[137,2223,2224],{"class":147}," hooks",[137,2226,115],{"class":154},[137,2228,2229],{"class":147},"hook\n",[137,2231,2232],{"class":139,"line":244},[137,2233,2234],{"class":154},"  }\n",[137,2236,2237],{"class":139,"line":250},[137,2238,1908],{"class":154},[10,2240,2241,2242,2245],{},"Here it is. The ",[134,2243,2244],{},"serverRendered"," property is set to false, always. So even when Vite SSG is used, the plugin thinks it's in a browser and tries to create the style element. Obviously, this doesn't work.",[10,2247,2248,2249,2252],{},"The patch is really simple. Instead of always returning false, we need to return ",[134,2250,2251],{},"import.meta.env.SSR || false"," to properly detect server-side rendering.",[2254,2255,2257,2258,2260,2261,2263,2269,2273,2276,2279,2282,2285,2308,2317,2320],"pull-request",{"href":2256},"https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fui\u002Fpull\u002F5347","\n  fix(vue): check ",[134,2259,318],{}," to support ",[134,2262,184],{},[10,2264,2265,2266],{},"Then, I re-enabled the Nuxt UI plugin, the build passed, and the FOUC was gone! ",[40,2267,2268],{},"Love it. A small boolean change made a big difference.",[100,2270,2272],{"id":2271},"making-a-better-head","Making a better head",[10,2274,2275],{},"The build was passing and the FOUC was gone. I continued to check the head of the generated file to ensure everything was in order.",[10,2277,2278],{},"It wasn't the case.",[10,2280,2281],{},"I could stop there, but I was on a roll of pull requests, plunged in Unhead, Nuxt UI and Vite SSG.",[10,2283,2284],{},"I discovered two things.",[55,2286,2287,2298],{},[58,2288,2289,2290,2293,2294,2297],{},"Nuxt UI sets a ",[134,2291,2292],{},"tagPriority"," of ",[134,2295,2296],{},"-2"," to the style element when injecting it. This means it will be one of the first elements in the head instead of being automatically placed.",[58,2299,2300,2301,2304,2305,2307],{},"Unhead was incorrectly handling the order of ",[134,2302,2303],{},"inlined"," and ",[134,2306,1084],{}," scripts.",[10,2309,2310,2311,2316],{},"For the first, I only ",[32,2312,2315],{"href":2313,"rel":2314},"https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fui\u002Fpull\u002F5347#issuecomment-3472594042",[36],"wrote a comment"," to understand why it was set. It could be a requirement for the Nuxt integration.",[10,2318,2319],{},"However, for the second point, knowing it was a bug, I opened two pull requests.",[2254,2321,2324],{"title":2322,"href":2323},"fix: inline scripts ordering","https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F596",[2325,2326,2329,2332],"pullrequest",{"title":2327,"href":2328},"fix: module scripts","https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F600",[10,2330,2331],{},"While doing these pull requests, I discovered that the CI wasn't working, some tests were failing, and the typecheck was broken, so I created 3 PRs to fix all of them.",[2254,2333,2336],{"title":2334,"href":2335},"test: update snapshots","https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F598",[2325,2337,2340],{"title":2338,"href":2339},"test: fix tsc","https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F597",[2325,2341,2344,2347,2350,2355,2359,2362,2368,2371,2374,2377,2379,2385,2388,2394],{"title":2342,"href":2343},"ci: fix","https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F599",[10,2345,2346],{},"I was just fixing a simple bug in the way Unhead handled script ordering and I finished fixing the tests and the integration.",[10,2348,2349],{},"When you care about the details, that's often how it ends up, and by making clean and working pull requests, you increase your chances of being accepted.",[10,2351,2352],{},[40,2353,2354],{},"I didn't even have time to write a blog post about them before they merged.",[100,2356,2358],{"id":2357},"open-source-not-by-choice","Open-source not by choice",[10,2360,2361],{},"I just wanted to build my website to document my infrastructure. I ended up fixing bugs in three tools, and opening 7 pull requests.",[10,2363,2364,2365],{},"Honestly, I would love to never have to fix them, and I would love to never have to spend my time on fixing these bugs. But that's not the open-source implicit contract, nor its mindset. ",[40,2366,2367],{},"It's how I see it. Not everyone does.",[10,2369,2370],{},"People offer you, for free, and often as is, their code for your own usage. It saves you time and money and allows you to build faster and better. Yes, I spend time fixing bugs, but how many times have I saved by using open-source software? How often have I been able to contribute to open-source software to build the experience I wanted within my projects? It's not even worth starting to count. Giving back, a little of your knowledge and time, seems to be a fair deal.",[10,2372,2373],{},"And that's normal that software has bugs, missing features or simply unplanned use cases. It's inherent to software and neither closed-source software nor open-source software is immune to this.",[10,2375,2376],{},"However, with open-source, you can change the situation by having this freedom to contribute to the software you're using.",[26,2378],{},[10,2380,2381,2382,115],{},"Just build stuff, anything you want, but bring your dreams to life, on internet, don't stop to ",[134,2383,2384],{},"localhost",[10,2386,2387],{},"Along the journey, you'll encounter bugs, missing features, or simply things that could be improved. When you do, don't hesitate to dive into your stack, to understand it, and try to fix it.",[10,2389,2390,2393],{},[505,2391,2392],{},"That's how you contribute to open-source",". Not the other way around. That's also one of the best ways to learn.",[10,2395,2396],{},"Cultivate the open-source mindset and opportunities will naturally arise.",[644,2398,2399],{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":132,"searchDepth":165,"depth":165,"links":2401},[2402,2403,2404,2405],{"id":102,"depth":165,"text":103},{"id":1339,"depth":165,"text":1340},{"id":1788,"depth":165,"text":1789},{"id":2271,"depth":165,"text":2272},"93da817a-f293-412c-8696-befcb4f95a8f","2025-11-03","I'm often asked on how to contribute to open-source and I rarely find a satisfying answer. So I took some time to reflect on it. Here's my take.","md",{},"\u002Fposts\u002Fwant-to-contribute-to-open-source-youre-doing-it-wrong","---\ncontentId: 93da817a-f293-412c-8696-befcb4f95a8f\ntitle: Want to Contribute to Open Source? You're Doing It Wrong\ndescription: I'm often asked on how to contribute to open-source and I rarely find a satisfying answer. So I took some time to reflect on it. Here's my take.\ndate: 2025-11-03\n---\n\nI have been actively contributing to open-source since 2020. Since then, I've spoken with many developers who want to contribute but don't know where to start, often asking for an issue to work on.\n\nI'm deeply convinced that open-source is more than that and, more importantly, doesn't work like that.\n\nTo me, open-source, at its core, is a mindset. It's about the freedom to explore, understand, modify, and improve the source code of the software you want. Of course, this model encourages collaboration and sharing by allowing anyone to contribute, but it doesn't require you to.\n\nOpen-source shouldn't, and should never, be a goal in itself. It's part of the journey, but it's not the destination. Otherwise, you'll end up feeling frustrated and unfulfilled by never being able to contribute.\n\nLet me tell you a story to illustrate this point.\n\n---\n\nI'm building a new website called [infra.soubiran.dev](https:\u002F\u002Finfra.soubiran.dev) to document my own infrastructure. It's a personal wiki but open to everyone. _I won't go into details; a dedicated post will come later._\n\nTo build it, I needed to find the right tools. Until now, I was using [VitePress with a custom theme for my personal blog](.\u002Fvitepress-for-a-personal-website-is-that-possible.md), the one you're reading right now. It works great, but I've nearly reached its limits, and with all the ideas I have, I know it could be time to move on.\n\nSo, I started looking into alternatives. Honestly, there are two options:\n\n1. Using [Nuxt](https:\u002F\u002Fnuxt.com) with [Nuxt Content](https:\u002F\u002Fcontent.nuxt.com)\n2. Using Vite with Vue and some [unplugins](https:\u002F\u002Funplugin.unjs.io\u002F) to handle markdown files.\n\nI went for the second option. Funny enough, before moving to VitePress, I had the same dilemma: Nuxt Content, VitePress, or Vite with Vue. At that time, I chose VitePress.\n\nAnyway, I started building the website.\n\nFor the past couple of months, there's one tech I can't live without: [Nuxt UI](https:\u002F\u002Fui.nuxt.com). It's an incredible component library made by the Nuxt team. All my projects that use Vue or Nuxt use Nuxt UI, and I regularly push it to its limits. _This is also a reason why I decided to move away from VitePress, which is not compatible with Nuxt UI._\n\n## Building with open-source software\n\nThe website I'm building is content-driven. This means that the content is primarily text-based and, in my case, all the pages are written in Markdown. To update the website, I edit the Markdown files and then rebuild the site.\n\nThe pages are generated and pre-rendered at build time. There is no need to generate them at runtime, as every user will receive the same pre-rendered content. To make it work, I use [Vite SSG](https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Fvite-ssg).\n\nIt's a drop-in replacement for Vite when building the website. It automatically builds the client and the server, then uses the server to pre-render each page, as if a user were requesting it.\n\nAfter installing it, everything worked as expected.\n\nSo I continued to build the website. I installed Nuxt UI and started configuring it. To use Nuxt UI within a Vue application, you have to use a plugin.\n\n```ts [src\u002Fmain.ts]\nimport ui from '@nuxt\u002Fui\u002Fvue-plugin'\nimport { ViteSSG } from 'vite-ssg'\nimport App from '.\u002FApp.vue'\n\nexport const createApp = ViteSSG(\n  App,\n  {},\n  ({ app }) => {\n    app.use(ui) \u002F\u002F [!code highlight]\n  },\n)\n```\n\nAnd then, everything broke. During the pre-rendering, something tried to access `document` to create a style: `const style = document.createElement('style')`. Really strange.\n\nWhen pre-rendering, Vite SSG injects `import.meta.env.SSR` to detect if it's running in SSR mode. It also allows the bundler to tree-shake unused code effectively. So, to fix the bug, I wrapped the plugin usage in a conditional check for SSR mode.\n\n```ts [src\u002Fmain.ts]\nimport ui from '@nuxt\u002Fui\u002Fvue-plugin'\nimport { ViteSSG } from 'vite-ssg'\nimport App from '.\u002FApp.vue'\n\nexport const createApp = ViteSSG(\n  App,\n  {},\n  ({ app }) => {\n    if (import.meta.env.SSR) { \u002F\u002F [!code highlight]\n      app.use(ui)\n    } \u002F\u002F [!code highlight]\n  },\n)\n```\n\nNow, pre-rendering worked as expected, or so I thought.\n\nI wrote some pages, created components, and pushed everything to production. It built successfully, so I opened the website and saw the following:\n\n\u003Cfigure>\n  \u003Cvideo autoplay loop muted playsinline>\n    \u003Csource src=\"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Fwhant-to-contribute-to-open-source-youre-doing-it-wrong\u002Finfra-soubiran-dev-fouc.mp4\" type=\"video\u002Fmp4\">\n  \u003C\u002Fvideo>\n  \u003Cfigcaption>Loading of my new website\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\nCan you see it?\n\nBefore going further, and if you noticed it, would you have fixed it? You know it will take time, and you won't be able to continue writing content. That's a real dilemma. Taking time for something you might consider a detail, or continuing to work on your content.\n\n**Stunning websites are made of details**. For me, details are very important, and it's essential to pay attention to them. They can transform the user experience, improve user comfort, and increase loyalty. Never underestimate them.\n\nFor those who don't see it, that's called a [FOUC](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FFlash_of_unstyled_content), a flash of unstyled content. The content appears before all the style is available. It usually happens when the script needs to add a `dark` class to the HTML element. The website loads with the default theme (light mode), appears, and then the JavaScript is parsed and executed. The script adds the `dark` class and the theme switches to dark mode. The light mode lasts a few hundred milliseconds, but it's enough to be noticed. This is all related to the way a browser renders a page.\n\nIt can be very tricky to fix. Usually, it happens when tags in your `head` aren't correctly ordered or when the script is not executed at the right time. So, I jumped into the head of the generated page:\n\n```html [dist\u002Findex.html]\n\u003Chtml lang=\"en\">\n  \u003Chead>\n    \u003Clink rel=\"preconnect\" href=\"https:\u002F\u002Ffonts.gstatic.com\u002F\" crossorigin=\"anonymous\">\n    \u003Clink rel=\"preload\" as=\"style\" onload=\"this.rel='stylesheet'\" href=\"https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=DM Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&amp;family=DM Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&amp;family=Sofia Sans:ital,wght@0,1..1000;1,1..1000&amp;display=swap\">\n    \u003Cmeta charset=\"utf-8\">\n    \u003Cmeta name=\"author\" content=\"Estéban Soubiran\">\n    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    \u003Ctitle>Estéban's Infra · Estéban Soubiran\u003C\u002Ftitle>\n    \u003Cmeta name=\"msapplication-TileColor\" content=\"#ffffff\">\n    \u003Cmeta name=\"theme-color\" content=\"#ffffff\">\n    \u003Cscript>\n      ;(function () {\n        const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches\n        const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'\n        if (setting === 'dark' || (prefersDark && setting !== 'light'))\n          document.documentElement.classList.toggle('dark', true)\n      })()\n    \u003C\u002Fscript>\n    \u003Cscript type=\"module\" crossorigin=\"\" src=\"\u002Fassets\u002Fapp-IQH-kuhC.js\">\u003C\u002Fscript>\n    \u003Clink rel=\"stylesheet\" crossorigin=\"\" href=\"\u002Fassets\u002Fapp-B27WEjTb.css\">\n    \u003Clink rel=\"modulepreload\" crossorigin=\"\" href=\"\u002Fassets\u002Fpages-DeF1Ma75.js\">\n    \u003Clink rel=\"modulepreload\" crossorigin=\"\" href=\"\u002Fassets\u002FWrapperContent-BVl8okbX.js\">\n    \u003Cmeta property=\"og:title\" content=\"Estéban's Infra\">\n    \u003Cmeta name=\"twitter:title\" content=\"Estéban's Infra\">\n  \u003C\u002Fhead>\n\u003C\u002Fhtml>\n```\n\nAnd I tried to understand what was happening.\n\nTo know if the head is correctly sorted, you can use [capo.js](https:\u002F\u002Frviscomi.github.io\u002Fcapo.js\u002F). After a quick inspection, I had the following result:\n\n![capo.js result showing head issues](\u002Fposts\u002Fwhant-to-contribute-to-open-source-youre-doing-it-wrong\u002Fcapo-js-result.webp)\n\nIn the Vite SSG documentation, it says to use [Unhead](https:\u002F\u002Funhead.unjs.io), so I read all the documentation about it. I discovered that Unhead automatically [sorts the head](https:\u002F\u002Funhead.unjs.io\u002Fdocs\u002Fvue\u002Fhead\u002Fguides\u002Fcore-concepts\u002Fpositions#sort-order) elements, but if it's already used, why isn't the head correctly sorted?\n\n## Everything didn't go as planned\n\nIf Unhead is supposed to sort the head elements, could the problem be in Vite SSG?\n\nOnly one way to find out: debugging.\n\nAfter reading the whole source code of Vite SSG, I found an interesting part:\n\n```ts [src\u002Fnode\u002Fbuild.ts]\n\u002F\u002F create jsdom from renderedHTML\nconst jsdom = new JSDOM(renderedHTML)\n\n\u002F\u002F render current page's preloadLinks\nrenderPreloadLinks(jsdom.window.document, ctx.modules || new Set\u003Cstring>(), ssrManifest)\n\n\u002F\u002F render head\nif (head) \u002F\u002F [!code highlight]\n  await renderDOMHead(head, { document: jsdom.window.document }) \u002F\u002F [!code highlight]\n\nconst html = jsdom.serialize()\nlet transformed = (await onPageRendered?.(route, html, appCtx)) || html\nif (beasties)\n  transformed = await beasties.process(transformed)\n\nconst formatted = await formatHtml(transformed, formatting)\n```\n\nThis part is responsible for generating the HTML before being saved. The interesting line is `await renderDOMHead(head, { document: jsdom.window.document })`. This is the only part where the head is used and rendered inside the HTML.\n\nMore interestingly, the function comes from `@unhead\u002Fdom`. Strange, because we're using SSR, so having a front-end function on the server looks suspicious.\n\nAnd I was right. This function doesn't optimize the head. It's supposed to change the head elements once the page is loaded, on the client. Bringing the whole sorting logic to the client side is completely pointless.\n\nWhile reading the Unhead documentation, I discovered the recommended way to use it with SSR.\n\n```ts [server.ts]\nimport { transformHtmlTemplate } from '@unhead\u002Fvue\u002Fserver' \u002F\u002F [!code highlight]\n\nconst rendered = await render(url)\n\nconst html = await transformHtmlTemplate( \u002F\u002F [!code highlight]\n  rendered.head,\n  template.replace(`\u003C!--app-html-->`, rendered.html ?? '')\n)\n```\n\nThe function `transformHtmlTemplate` extracts the current head elements from the template and injects them into the head before re-rendering the HTML and injecting it into the page. This ensures that the head is correctly sorted before sending it to the client.\n\nIt looked like a good solution to fix the head sorting issue. This led me to create a PR to Vite SSG to improve the integration with Unhead.\n\n[refactor: uses unhead\u002Fserver to optimize head](https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Fvite-ssg\u002Fpull\u002F464)\n\nIn order to quickly use this fix in my project, I patched Vite SSG locally. This allowed me to continue developing my project without waiting for my PR to be merged and then for a release.\n\nBut not everything went as planned.\n\nThe head was, nearly, well-ordered, but after that fix, the FOUC was still there. _Sad._\n\nIt means that the problem isn't related to the head sorting.\n\n## Deeper investigation\n\nThe Vite SSG patch isn't useless, as it improves the head sorting. However, it didn't solve the FOUC issue. I had to dig deeper.\n\nI continued to look for the root cause. I tried to analyze the application performance to identify any bottlenecks, race conditions, or other issues that could be causing the FOUC. I didn't find anything.\n\nI inspected the CSS and found that the styles from the prose were correctly applied, but not the ones from Nuxt UI. If you look closely at the video, you'll see the title color change from black to white, but the text underneath is already white. The CSS variables for Nuxt UI aren't available on the first render but are applied when the JavaScript loads.\n\nTo confirm this suspicion, I checked the Nuxt UI source code where I found a Vue plugin named `colors.ts`. The same that caused the build issue at the beginning.\n\nEverything was leading to this plugin, so I took the time to understand it, and I found the root cause of both the FOUC and the build issue:\n\n```ts [src\u002Fruntime\u002Fplugins\u002Fcolors.ts]\nconst headData: UseHeadInput = {\n  style: [{\n    innerHTML: () => root.value,\n    tagPriority: -2,\n    id: 'nuxt-ui-colors'\n  }]\n}\n\nif (import.meta.client && nuxtApp.isHydrating && !nuxtApp.payload.serverRendered) {\n  const style = document.createElement('style')\n\n  style.innerHTML = root.value\n  style.setAttribute('data-nuxt-ui-colors', '')\n  document.head.appendChild(style)\n\n  headData.script = [{\n    innerHTML: 'document.head.removeChild(document.querySelector(\\'[data-nuxt-ui-colors]\\'))'\n  }]\n}\n\nuseHead(headData)\n```\n\nThere are multiple issues at play here.\n\nThe first seven lines, along with the last, are used to inject the Nuxt UI CSS variables into the head, at both build time and runtime. By disabling the plugin, I removed this behavior, so the FOUC inevitably occurs.\n\nThe `if` block creates a style element and must only be executed on the client side, during hydration, if the content is not server-rendered. My app is using Vite SSG to be server-rendered, so this should not happen.\n\nSo, what is this `nuxtApp`? Let's continue to investigate.\n\nAt first, Nuxt UI is made for Nuxt, but thanks to the work of [Daniel Roe](https:\u002F\u002Fgithub.com\u002Fdanielroe) and a lot of stubs, Nuxt UI is also usable outside of Nuxt. One of the stubs is the `useNuxtApp` composable:\n\n```ts [src\u002Fruntime\u002Fvue\u002Fstubs.ts]\nexport function useNuxtApp() {\n  return {\n    isHydrating: true,\n    payload: { serverRendered: false },\n    hooks,\n    hook: hooks.hook\n  }\n}\n```\n\nHere it is. The `serverRendered` property is set to false, always. So even when Vite SSG is used, the plugin thinks it's in a browser and tries to create the style element. Obviously, this doesn't work.\n\nThe patch is really simple. Instead of always returning false, we need to return `import.meta.env.SSR || false` to properly detect server-side rendering.\n\n\u003CPullRequest href=\"https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fui\u002Fpull\u002F5347\">\n  fix(vue): check \u003Ccode>import.meta.env.SSR\u003C\u002Fcode> to support \u003Ccode>vite-ssg\u003C\u002Fcode>\n\u003C\u002FPullRequest>\n\nThen, I re-enabled the Nuxt UI plugin, the build passed, and the FOUC was gone! _Love it. A small boolean change made a big difference._\n\n## Making a better head\n\nThe build was passing and the FOUC was gone. I continued to check the head of the generated file to ensure everything was in order.\n\nIt wasn't the case.\n\nI could stop there, but I was on a roll of pull requests, plunged in Unhead, Nuxt UI and Vite SSG.\n\nI discovered two things.\n\n1. Nuxt UI sets a `tagPriority` of `-2` to the style element when injecting it. This means it will be one of the first elements in the head instead of being automatically placed.\n2. Unhead was incorrectly handling the order of `inlined` and `module` scripts.\n\nFor the first, I only [wrote a comment](https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fui\u002Fpull\u002F5347#issuecomment-3472594042) to understand why it was set. It could be a requirement for the Nuxt integration.\n\nHowever, for the second point, knowing it was a bug, I opened two pull requests.\n\n\u003CPullRequest title=\"fix: inline scripts ordering\" href=\"https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F596\" \u002F>\n\u003CPullRequest title=\"fix: module scripts\" href=\"https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F600\" \u002F>\n\nWhile doing these pull requests, I discovered that the CI wasn't working, some tests were failing, and the typecheck was broken, so I created 3 PRs to fix all of them.\n\n\u003CPullRequest title=\"test: update snapshots\" href=\"https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F598\" \u002F>\n\u003CPullRequest title=\"test: fix tsc\" href=\"https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F597\" \u002F>\n\u003CPullRequest title=\"ci: fix\" href=\"https:\u002F\u002Fgithub.com\u002Funjs\u002Funhead\u002Fpull\u002F599\" \u002F>\n\nI was just fixing a simple bug in the way Unhead handled script ordering and I finished fixing the tests and the integration.\n\nWhen you care about the details, that's often how it ends up, and by making clean and working pull requests, you increase your chances of being accepted.\n\n_I didn't even have time to write a blog post about them before they merged._\n\n## Open-source not by choice\n\nI just wanted to build my website to document my infrastructure. I ended up fixing bugs in three tools, and opening 7 pull requests.\n\nHonestly, I would love to never have to fix them, and I would love to never have to spend my time on fixing these bugs. But that's not the open-source implicit contract, nor its mindset. _It's how I see it. Not everyone does._\n\nPeople offer you, for free, and often as is, their code for your own usage. It saves you time and money and allows you to build faster and better. Yes, I spend time fixing bugs, but how many times have I saved by using open-source software? How often have I been able to contribute to open-source software to build the experience I wanted within my projects? It's not even worth starting to count. Giving back, a little of your knowledge and time, seems to be a fair deal.\n\nAnd that's normal that software has bugs, missing features or simply unplanned use cases. It's inherent to software and neither closed-source software nor open-source software is immune to this.\n\nHowever, with open-source, you can change the situation by having this freedom to contribute to the software you're using.\n\n---\n\nJust build stuff, anything you want, but bring your dreams to life, on internet, don't stop to `localhost`.\n\nAlong the journey, you'll encounter bugs, missing features, or simply things that could be improved. When you do, don't hesitate to dive into your stack, to understand it, and try to fix it.\n\n**That's how you contribute to open-source**. Not the other way around. That's also one of the best ways to learn.\n\nCultivate the open-source mindset and opportunities will naturally arise.\n",null,{"title":5,"description":2408},{"loc":2411,"images":2416},[2417],{"loc":1321},"posts\u002Fwant-to-contribute-to-open-source-youre-doing-it-wrong","tyIAbOiKNyiSnvygkr2QCXnMymNL2L3HZl1rIwq4PU4",1790086900689]