[{"data":1,"prerenderedAt":284},["ShallowReactive",2],{"i-lucide:sun":3,"i-lucide:moon":8,"i-lucide:menu":10,"i-lucide:x":12,"i-social:linkedin":14,"i-social:github":17,"i-social:dailydev":21,"article-migrer-webpack-vers-nuxt":23,"article-migrer-webpack-vers-nuxt-voisins":273,"i-lucide:chevron-right":274,"ArticleBody_oQa5cJRYYJekmmz08ej8Nr4kNnQTaAbeEG8pMQ6sNw":276},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"4\"\u002F>\u003Cpath d=\"M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 5h16M4 12h16M4 19h16\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"\u002F>",{"left":4,"top":4,"width":15,"height":15,"rotate":4,"vFlip":6,"hFlip":6,"body":16},44.856,"\u003Cpath fill=\"currentColor\" d=\"M39.376,0a5.483,5.483,0,0,1,5.48,5.48v33.9a5.483,5.483,0,0,1-5.48,5.48H5.48A5.483,5.483,0,0,1,0,39.376V5.48A5.483,5.483,0,0,1,5.48,0h33.9ZM14.058,37.084V17.308H7.484V37.084Zm23.7,0V25.743c0-6.074-3.243-8.9-7.568-8.9a6.529,6.529,0,0,0-5.924,3.265v-2.8H17.7c.087,1.856,0,19.775,0,19.775h6.573V26.04a4.494,4.494,0,0,1,.217-1.6,3.6,3.6,0,0,1,3.372-2.4c2.377,0,3.33,1.814,3.33,4.471V37.084H37.76ZM10.815,7.773a3.426,3.426,0,1,0-.087,6.834h.042a3.428,3.428,0,1,0,.045-6.834Z\"\u002F>",{"left":4,"top":4,"width":18,"height":19,"rotate":4,"vFlip":6,"hFlip":6,"body":20},46.142,44.459,"\u003Cpath fill=\"currentColor\" d=\"M55.071,32a22.76,22.76,0,0,0-7.292,44.422c1.154.208,1.576-.5,1.576-1.1,0-.546-.021-1.975-.031-3.88-6.417,1.379-7.776-3.057-7.776-3.057a6.079,6.079,0,0,0-2.565-3.335c-2.091-1.419.154-1.389.154-1.389a4.825,4.825,0,0,1,3.533,2.352c2.06,3.483,5.4,2.481,6.715,1.9a4.829,4.829,0,0,1,1.463-3.047c-5.119-.576-10.506-2.531-10.506-11.264a8.739,8.739,0,0,1,2.369-6.113,8.107,8.107,0,0,1,.227-6.034s1.936-.615,6.345,2.332a22.227,22.227,0,0,1,11.556,0c4.408-2.948,6.334-2.332,6.334-2.332a8.107,8.107,0,0,1,.227,6.034A8.775,8.775,0,0,1,69.769,53.6c0,8.753-5.4,10.679-10.536,11.244A5.418,5.418,0,0,1,60.8,69.058c0,3.047-.031,5.508-.031,6.252,0,.605.412,1.32,1.586,1.092A22.747,22.747,0,0,0,55.071,32Z\" transform=\"translate(-32 -32)\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":22},"\u003Cpath fill=\"currentColor\" d=\"M18.29 5.706a1.405 1.405 0 0 0-1.987 0L4.716 17.296l1.324-2.65-2.65-2.649 3.312-3.311 2.65 2.65 1.986-1.988-3.642-3.642a1.405 1.405 0 0 0-1.987 0L.411 11.004a1.404 1.404 0 0 0 0 1.987l4.305 4.304.993.993a1.405 1.405 0 0 0 1.987 0L19.285 6.7l-.993-.994Zm-.332 3.647 2.65 2.65-4.306 4.305a1.404 1.404 0 1 0 1.986 1.986l5.299-5.298a1.404 1.404 0 0 0 0-1.987l-4.305-4.304-1.324 2.648Z\"\u002F>",{"id":24,"title":25,"body":26,"cover":258,"coverAlt":103,"date":259,"description":260,"draft":6,"extension":261,"meta":262,"navigation":263,"path":264,"seo":265,"seoTitle":266,"stem":267,"tags":268,"updated":258,"__hash__":272},"blog\u002Fblog\u002Fmigrer-webpack-vers-nuxt.md","Ce que la refonte de mon portfolio m'a appris sur les dépendances",{"type":27,"value":28,"toc":248},"minimark",[29,33,38,41,80,83,87,94,104,107,111,116,119,122,126,133,136,200,206,210,221,224,231,235,238,241,244],[30,31,32],"p",{},"Mon portfolio a tenu cinq ans sur une base artisanale : cinq pages HTML écrites\nà la main, du Sass, la grille de Bootstrap et une configuration Webpack montée\npièce par pièce. Il fonctionnait. Deux points ont fini par le rendre coûteux à\nmaintenir : le contenu vivait dans le balisage, et le nombre de domaines\nextérieurs contactés au chargement avait augmenté sans que je m'en aperçoive.",[34,35,37],"h2",{"id":36},"cinq-domaines-pour-un-site-statique","Cinq domaines pour un site statique",[30,39,40],{},"En reprenant l'ancien code, j'ai compté les domaines contactés au chargement\nd'une page :",[42,43,44,56,62,68,74],"ul",{},[45,46,47,51,52,55],"li",{},[48,49,50],"code",{},"fonts.googleapis.com"," et ",[48,53,54],{},"fonts.gstatic.com"," pour la police,",[45,57,58,61],{},[48,59,60],{},"cdn.jsdelivr.net"," pour une feuille de reset — importée depuis le Sass,\ndonc bloquante au rendu,",[45,63,64,67],{},[48,65,66],{},"media.giphy.com"," pour une image décorative,",[45,69,70,73],{},[48,71,72],{},"www.google.com"," pour un reCAPTCHA rattaché à un formulaire qui, lui, ne\nfonctionnait plus,",[45,75,76,79],{},[48,77,78],{},"instant.page"," pour du préchargement au survol.",[30,81,82],{},"Cinq domaines, aucun sous mon contrôle. En reprenant chaque cas, je n'ai trouvé\naucune raison de les garder. Le reCAPTCHA protégeait un formulaire hors service\ndepuis un moment.",[34,84,86],{"id":85},"leffet-de-bord-qui-valait-le-détour","L'effet de bord qui valait le détour",[30,88,89,90,93],{},"J'ai fait le ménage pour le temps de chargement. Le gain que je n'attendais pas\nest ailleurs : sans ressource externe, la politique de sécurité du contenu peut\ndémarrer à ",[48,91,92],{},"default-src 'none'",".",[95,96,101],"pre",{"className":97,"code":99,"language":100},[98],"language-text","default-src 'none'; script-src 'self' 'sha256-…'; style-src 'self';\nimg-src 'self' data:; font-src 'self'; connect-src 'self';\nbase-uri 'none'; form-action 'none'; frame-ancestors 'none'\n","text",[48,102,99],{"__ignoreMap":103},"",[30,105,106],{},"Tout est interdit par défaut, et seul ce qui vient de mon domaine passe. Cette\nrègle devient intenable dès qu'une seule police est chargée depuis un CDN.",[34,108,110],{"id":109},"trois-pièges-rencontrés-en-chemin","Trois pièges rencontrés en chemin",[112,113,115],"h3",{"id":114},"une-police-récupérée-sans-cdn","Une police récupérée sans CDN",[30,117,118],{},"Auto-héberger une police suppose d'obtenir les fichiers. Le réflexe est de les\ntélécharger depuis Google Fonts. Sauf que ça recrée une dépendance réseau au\nmoment du build : sans accès à internet, la génération échoue.",[30,120,121],{},"Fontsource publie les mêmes polices sur npm. Le fichier arrive par le\ngestionnaire de paquets et il est versionné dans le dépôt. Rubik en variable\npèse 35 Ko et couvre toutes les graisses en une requête.",[112,123,125],{"id":124},"des-icônes-qui-appelaient-une-api-distante","Des icônes qui appelaient une API distante",[30,127,128,129,132],{},"Iconify résout les icônes via son API quand la collection n'est pas installée\nlocalement. Le paquet était bien présent, mais un repli restait actif et le\nbuild sortait vers ",[48,130,131],{},"api.iconify.design"," sans rien signaler.",[30,134,135],{},"La configuration explicite règle le problème :",[95,137,141],{"className":138,"code":139,"language":140,"meta":103,"style":103},"language-ts shiki shiki-themes github-light-high-contrast github-light-high-contrast github-dark-high-contrast","icon: {\n  mode: 'svg',           \u002F\u002F SVG inline plutôt que masques CSS\n  fallbackToApi: false,  \u002F\u002F une icône manquante casse le build\n}\n","ts",[48,142,143,156,176,194],{"__ignoreMap":103},[144,145,148,152],"span",{"class":146,"line":147},"line",1,[144,149,151],{"class":150},"szw1H","icon",[144,153,155],{"class":154},"s7iLA",": {\n",[144,157,159,162,165,169,172],{"class":146,"line":158},2,[144,160,161],{"class":150},"  mode",[144,163,164],{"class":154},": ",[144,166,168],{"class":167},"semJd","'svg'",[144,170,171],{"class":154},",           ",[144,173,175],{"class":174},"sq92B","\u002F\u002F SVG inline plutôt que masques CSS\n",[144,177,179,182,184,188,191],{"class":146,"line":178},3,[144,180,181],{"class":150},"  fallbackToApi",[144,183,164],{"class":154},[144,185,187],{"class":186},"ssYy9","false",[144,189,190],{"class":154},",  ",[144,192,193],{"class":174},"\u002F\u002F une icône manquante casse le build\n",[144,195,197],{"class":146,"line":196},4,[144,198,199],{"class":154},"}\n",[30,201,202,205],{},[48,203,204],{},"fallbackToApi: false"," est la ligne qui compte. Sans elle, une icône mal\northographiée passe inaperçue en développement et déclenche une requête externe\nen production.",[112,207,209],{"id":208},"un-thème-sombre-sans-fond","Un thème sombre sans fond",[30,211,212,213,216,217,220],{},"Le plus difficile à diagnostiquer. Tailwind 4 élague les variables déclarées dans\n",[48,214,215],{},"@theme"," qu'aucune classe n'utilise littéralement. Ma palette de neutres était\nbien définie, mais comme aucune classe ne s'appelait ",[48,218,219],{},"bg-ink-900",", les\nvariables disparaissaient du CSS final, et avec elles les tokens que Nuxt UI en\ndérive.",[30,222,223],{},"Le thème sombre passait donc le texte en blanc et laissait le fond blanc. Du\ntexte invisible, uniquement en mode sombre, uniquement en production.",[30,225,226,227,230],{},"Le correctif tient en un mot-clé : ",[48,228,229],{},"@theme static"," conserve toutes les\nvariables, utilisées ou non.",[34,232,234],{"id":233},"ce-que-jen-ai-fait","Ce que j'en ai fait",[30,236,237],{},"Aucun des trois ne serait apparu dans un test fonctionnel. Le site s'affichait\ncorrectement dans les trois cas, ce qui est précisément le problème.",[30,239,240],{},"J'ai donc écrit des scripts de vérification. L'un parcourt le HTML généré et\néchoue si une ressource pointe hors du domaine. Un autre calcule les ratios de\ncontraste de la palette : c'est ainsi que j'ai découvert qu'une de mes teintes\nplafonnait à 4,49:1, pour un seuil fixé à 4,5.",[30,242,243],{},"Ils s'exécutent à chaque modification. Aucune de ces trois erreurs n'était\ndétectable à l'œil, et rien ne garantit qu'elles le deviendraient dans six\nmois.",[245,246,247],"style",{},"html pre.shiki code .szw1H, html code.shiki .szw1H{--shiki-light:#702C00;--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .s7iLA, html code.shiki .s7iLA{--shiki-light:#0E1116;--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .semJd, html code.shiki .semJd{--shiki-light:#032563;--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .sq92B, html code.shiki .sq92B{--shiki-light:#66707B;--shiki-default:#66707B;--shiki-dark:#BDC4CC}html pre.shiki code .ssYy9, html code.shiki .ssYy9{--shiki-light:#023B95;--shiki-default:#023B95;--shiki-dark:#91CBFF}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);}",{"title":103,"searchDepth":178,"depth":178,"links":249},[250,251,252,257],{"id":36,"depth":158,"text":37},{"id":85,"depth":158,"text":86},{"id":109,"depth":158,"text":110,"children":253},[254,255,256],{"id":114,"depth":178,"text":115},{"id":124,"depth":178,"text":125},{"id":208,"depth":178,"text":209},{"id":233,"depth":158,"text":234},null,"2026-09-07","Migration d'un portfolio Webpack vers Nuxt avec zéro ressource tierce : police auto-hébergée, icônes locales et politique de sécurité stricte.","md",{},true,"\u002Fblog\u002Fmigrer-webpack-vers-nuxt",{"title":25,"description":260},"Migrer un portfolio Webpack vers Nuxt","blog\u002Fmigrer-webpack-vers-nuxt",[269,270,271],"Nuxt","Performance","Sécurité","hLoOHhNwx-gcMutwKSfapHzeVoskzPqJCQvN5-bfvYM",[258,258],{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":275},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m9 18l6-6l-6-6\"\u002F>",["Island",277],{"key":278,"result":279},"ArticleBody_oQa5cJRYYJekmmz08ej8Nr4kNnQTaAbeEG8pMQ6sNw",{"head":280},{"style":281},[282],{"innerHTML":283},"pre code .line{display:block}",1791663008601]