[{"data":1,"prerenderedAt":471},["ShallowReactive",2],{"project-\u002Fprojects\u002Fportfolio-dev-fr":3,"related-\u002Fprojects\u002Fportfolio-dev-fr":451,"legal-link-fr":469},{"id":4,"title":5,"body":6,"description":425,"extension":426,"git":427,"git_soon":430,"meta":431,"navigation":432,"order":433,"path":434,"seo":435,"stack":436,"stem":444,"type":445,"updated":446,"web":447,"__hash__":450},"projects_fr\u002Fprojects\u002Fportfolio-dev.md","Portfolio Développeur",{"type":7,"value":8,"toc":411},"minimark",[9,17,22,38,42,233,237,357,361,370,373,377,382,388,390,394,399,401,405],[10,11,12,16],"p",{},[13,14,15],"strong",{},"Le site que vous lisez en ce moment"," : bilingue par URL, prérendu statique, et pensé autant pour le SEO que pour l'accessibilité.",[18,19,21],"h2",{"id":20},"quel-est-le-contexte-de-ce-portfolio","Quel est le contexte de ce portfolio ?",[10,23,24,25,29,30,33,34,37],{},"Portfolio personnel qui regroupe projets, expériences et informations de contact. Le site est bilingue par URL (FR à ",[26,27,28],"code",{},"\u002F",", EN sous ",[26,31,32],{},"\u002Fen\u002F…","), embarque une intégration Spotify temps réel pour afficher le morceau en cours d'écoute, et est déployé sur Netlify en SSR + prérendu statique (chaque page des deux langues est générée à la build, avec ",[26,35,36],{},"hreflang"," et canonical). La route API Spotify tourne via une Netlify Edge Function (Deno).",[18,39,41],{"id":40},"quelle-stack-technique","Quelle stack technique ?",[43,44,45,60,90,132,157,187,197,210,223],"ul",{},[46,47,48,55,56,59],"li",{},[13,49,50,51,54],{},"Nuxt 3 (",[26,52,53],{},"ssr: true"," + prérendu)"," — le build prérend chaque route des deux langues en HTML complet (",[26,57,58],{},"nitro.prerender.crawlLinks","), servi statiquement par Netlify puis hydraté côté client. Le HTML contient le contenu, les meta OG et le JSON-LD dès la première peinture (LCP + indexabilité), sans serveur Node à l'exécution.",[46,61,62,65,66,69,70,73,74,77,78,81,82,85,86,89],{},[13,63,64],{},"@nuxt\u002Fcontent v2"," — contenu entièrement en Markdown avec frontmatter typé TypeScript. Pas de base de données ni d'API REST — les fichiers ",[26,67,68],{},".md"," sont la source de vérité, requêtés via ",[26,71,72],{},"queryContent()",". À noter : les ",[26,75,76],{},"_path"," de contenu ne portent pas de préfixe de locale (",[26,79,80],{},"\u002Fprojects\u002Ffinixa","), donc les requêtes ciblent le slug et non ",[26,83,84],{},"route.path"," (préfixé ",[26,87,88],{},"\u002Fen"," en anglais).",[46,91,92,103,104,29,106,108,109,111,112,115,116,119,120,123,124,127,128,131],{},[13,93,94,95,98,99,102],{},"i18n par URL (",[26,96,97],{},"@nuxtjs\u002Fi18n",", ",[26,100,101],{},"prefix_except_default",")"," — FR à ",[26,105,28],{},[26,107,32],{},", chaque langue ayant sa propre URL crawlable. ",[26,110,36],{}," + canonical par locale via ",[26,113,114],{},"useLocaleHead()",", cookie ",[26,117,118],{},"lang"," conservé pour la préférence du visiteur. Changer de langue est une navigation (",[26,121,122],{},"switchLocalePath()",") — l'animation de volets est jouée par la transition de page. Les ",[26,125,126],{},"queryContent"," filtrent par ",[26,129,130],{},"_locale",".",[46,133,134,140,141,144,145,148,149,152,153,156],{},[13,135,136,137,102],{},"Sitemap auto (",[26,138,139],{},"@nuxtjs\u002Fsitemap"," — ",[26,142,143],{},"\u002Fsitemap.xml"," est un index par locale (",[26,146,147],{},"fr-FR.xml"," + ",[26,150,151],{},"en-US.xml",") généré depuis les routes prérendues, alimenté par l'URL canonique (",[26,154,155],{},"site.url",").",[46,158,159,162,163,166,167,170,171,174,175,98,178,98,181,98,184,156],{},[13,160,161],{},"SCSS sans framework CSS"," — design system maison : fonction ",[26,164,165],{},"space($n)"," (= ",[26,168,169],{},"n × 4px","), mixin ",[26,172,173],{},"transition()",", variables CSS custom pour le thème clair\u002Fsombre (",[26,176,177],{},"--primary",[26,179,180],{},"--background",[26,182,183],{},"--accent",[26,185,186],{},"--text",[46,188,189,192,193,196],{},[13,190,191],{},"@nuxt\u002Fimage avec provider Netlify"," — les images de couverture des projets (",[26,194,195],{},".webp",") sont servies via le CDN Netlify avec redimensionnement automatique à la livraison.",[46,198,199,202,203,205,206,209],{},[13,200,201],{},"Git LFS"," — les ",[26,204,195],{}," sont trackées via ",[26,207,208],{},".gitattributes"," pour éviter de stocker des binaires lourds dans l'historique git.",[46,211,212,140,215,218,219,222],{},[13,213,214],{},"Netlify Edge Function (Deno)",[26,216,217],{},"netlify\u002Fedge-functions\u002Fspotify.ts"," sert la route ",[26,220,221],{},"\u002Fapi\u002Fspotify",". Échange le refresh token contre un access token à chaque appel, sans stocker de token ni exposer les credentials côté client.",[46,224,225,228,229,232],{},[13,226,227],{},"Umami Analytics"," — script async chargé via ",[26,230,231],{},"useHead",", sans cookie, sans transmission de données personnelles à des tiers.",[18,234,236],{"id":235},"points-techniques-notables","Points techniques notables",[43,238,239,266,287,304,339],{},[46,240,241,247,248,251,252,255,256,259,260,262,263,265],{},[13,242,243,244],{},"CSS Grid avec ",[26,245,246],{},"display: contents"," : la grille principale est définie sur ",[26,249,250],{},".page"," (4–5 colonnes). ",[26,253,254],{},"AppHeader"," et ",[26,257,258],{},"AppSection"," utilisent ",[26,261,246],{},", ce qui rend leurs cellules enfants participants directs de la grille du parent, quelle que soit la profondeur dans l'arbre Vue. Layout éditorial sans wrappers parasites — mais ",[26,264,246],{}," retire les éléments de l'arbre d'accessibilité, ce qui a demandé des ajustements ARIA sur les composants concernés.",[46,267,268,271,272,275,276,279,280,283,284,131],{},[13,269,270],{},"Transition de page CSS-only"," : les transitions utilisent le pseudo-élément ",[26,273,274],{},".cell:before"," positionné hors écran par défaut (",[26,277,278],{},"inset: -1px calc(100% + 1px) -1px -1px","). La classe ",[26,281,282],{},".page-leave-to"," le fait glisser sur toute la cellule, créant un effet de volets fermants coordonnés. Zéro JavaScript, uniquement les hooks Nuxt page transitions + CSS. Désactivé proprement via ",[26,285,286],{},"prefers-reduced-motion",[46,288,289,292,293,296,297,299,300,303],{},[13,290,291],{},"Middleware de validation de slug"," : ",[26,294,295],{},"middleware\u002Fproject.ts"," exécute un ",[26,298,72],{}," avant le rendu de la page projet. Si le slug n'existe pas, ",[26,301,302],{},"abortNavigation()"," est appelé — la navigation est annulée proprement plutôt que de rendre une page vide. La vérification est indépendante de la locale (les projets existent en FR et EN), ce qui garde le middleware hors du cycle de vie i18n.",[46,305,306,292,309,312,313,316,317,320,321,324,325,98,328,255,331,334,335,338],{},[13,307,308],{},"SVG inlinés pour les diagrammes",[26,310,311],{},"ProseImg.vue"," détecte les images ",[26,314,315],{},".svg"," et les récupère via ",[26,318,319],{},"useFetch"," pour les injecter en HTML brut dans un ",[26,322,323],{},".svg-wrapper",". Les diagrammes sont ainsi thémables via CSS — d'où les overrides explicites de ",[26,326,327],{},"fill",[26,329,330],{},"stroke",[26,332,333],{},"color"," sur les éléments SVG internes dans ",[26,336,337],{},"[slug].vue"," pour le mode sombre.",[46,340,341,344,345,348,349,352,353,356],{},[13,342,343],{},"Stagger animation via IntersectionObserver"," : la liste des projets s'anime avec un ",[26,346,347],{},"IntersectionObserver"," (threshold 0.15). Chaque lien projet reçoit un ",[26,350,351],{},"transitionDelay"," calculé depuis son index (",[26,354,355],{},"index × 400ms","), créant l'entrée décalée sans librairie d'animation.",[18,358,360],{"id":359},"ce-que-jai-appris","Ce que j'ai appris",[10,362,363,364,366,367,369],{},"Concevoir un layout CSS Grid qui traverse plusieurs niveaux de composants Vue via ",[26,365,246],{}," demande une connaissance précise de l'impact sur l'arbre d'accessibilité : les éléments avec ",[26,368,246],{}," sont transparents pour le DOM visuel mais pas pour les lecteurs d'écran. La transition de page CSS-only a demandé plusieurs itérations de timing pour coordonner la sortie de l'ancien composant et l'entrée du nouveau sans flash de contenu entre les deux.",[371,372],"hr",{},[18,374,376],{"id":375},"schémas","Schémas",[378,379,381],"h3",{"id":380},"architecture-globale","Architecture globale",[10,383,384],{},[385,386],"img",{"alt":381,"src":387},"\u002Fdiagrams\u002Fportfolio-dev\u002Farchitecture.svg",[371,389],{},[378,391,393],{"id":392},"système-i18n-réactif","Système i18n réactif",[10,395,396],{},[385,397],{"alt":393,"src":398},"\u002Fdiagrams\u002Fportfolio-dev\u002Fi18n.svg",[371,400],{},[378,402,404],{"id":403},"structure-du-layout-css-grid","Structure du layout CSS Grid",[10,406,407],{},[385,408],{"alt":409,"src":410},"Structure CSS Grid","\u002Fdiagrams\u002Fportfolio-dev\u002Fgrid.svg",{"title":412,"searchDepth":413,"depth":413,"links":414},"",2,[415,416,417,418,419],{"id":20,"depth":413,"text":21},{"id":40,"depth":413,"text":41},{"id":235,"depth":413,"text":236},{"id":359,"depth":413,"text":360},{"id":375,"depth":413,"text":376,"children":420},[421,423,424],{"id":380,"depth":422,"text":381},3,{"id":392,"depth":422,"text":393},{"id":403,"depth":422,"text":404},"Portfolio bilingue construit avec Nuxt 3 en SSR + prérendu statique — bilingue par URL (FR \u002F EN), contenu Markdown, intégration Spotify live et transitions de page CSS-only.","md",[428,429],"Répertoire Git","https:\u002F\u002Fgithub.com\u002FOwenLB\u002Fportfolio-dev",false,{},true,4,"\u002Fprojects\u002Fportfolio-dev",{"title":5,"description":425},[437,438,439,440,441,97,139,442,443],"Nuxt 3","TypeScript","Vue 3","@nuxt\u002Fcontent","@nuxt\u002Fimage","SCSS","Netlify","projects\u002Fportfolio-dev","Site","2026-07-17",[448,449],"Voir le site","https:\u002F\u002Fowenlebec.fr","0s70sO2qV2axrhx6TDFcIwqYNJl8Kyz370k3_PX8Zok",[452,456,460,464],{"title":453,"type":454,"path":80,"order":455},"Finixa","App web & mobile",1,{"title":457,"type":458,"path":459,"order":413},"Peliculle","App iOS","\u002Fprojects\u002Fpeliculle",{"title":461,"type":462,"path":463,"order":422},"Scan Auto","App web","\u002Fprojects\u002Fscanauto",{"title":465,"type":466,"path":467,"order":468},"Portfolio Photo","Site full-stack","\u002Fprojects\u002Fportfolio-photo",5,{"title":470},"Mentions légales",1784470460638]