[{"data":1,"prerenderedAt":469},["ShallowReactive",2],{"project-\u002Fprojects\u002Fportfolio-dev-en":3,"related-\u002Fprojects\u002Fportfolio-dev-en":449,"legal-link-en":467},{"id":4,"title":5,"body":6,"description":423,"extension":424,"git":425,"git_soon":428,"meta":429,"navigation":430,"order":431,"path":432,"seo":433,"stack":434,"stem":442,"type":443,"updated":444,"web":445,"__hash__":448},"projects_en\u002Fprojects\u002Fportfolio-dev.md","Developer Portfolio",{"type":7,"value":8,"toc":409},"minimark",[9,17,22,38,42,232,236,356,360,369,372,376,381,387,389,393,398,400,404],[10,11,12,16],"p",{},[13,14,15],"strong",{},"The very site you're reading right now"," — bilingual by URL, statically prerendered, and built for SEO and accessibility alike.",[18,19,21],"h2",{"id":20},"whats-the-context-behind-this-portfolio","What's the context behind this portfolio?",[10,23,24,25,29,30,33,34,37],{},"Personal portfolio covering projects, work experience, and contact information. The site is bilingual by URL (FR at ",[26,27,28],"code",{},"\u002F",", EN under ",[26,31,32],{},"\u002Fen\u002F…","), embeds a live Spotify integration to display the currently playing track, and is deployed on Netlify with SSR + static prerendering (every page of both languages is generated at build time, with ",[26,35,36],{},"hreflang"," and canonical tags). The Spotify API route runs as a Netlify Edge Function (Deno).",[18,39,41],{"id":40},"whats-the-tech-stack","What's the tech stack?",[43,44,45,60,90,132,157,187,197,209,222],"ul",{},[46,47,48,55,56,59],"li",{},[13,49,50,51,54],{},"Nuxt 3 (",[26,52,53],{},"ssr: true"," + prerendering)"," — the build prerenders every route of both languages to full HTML (",[26,57,58],{},"nitro.prerender.crawlLinks","), served statically by Netlify then hydrated client-side. The HTML carries content, OG meta and JSON-LD on first paint (LCP + indexability), with no Node server at runtime.",[46,61,62,65,66,69,70,73,74,77,78,81,82,85,86,89],{},[13,63,64],{},"@nuxt\u002Fcontent v2"," — all content in Markdown with TypeScript-typed frontmatter. No database, no REST API — ",[26,67,68],{},".md"," files are the source of truth, queried with ",[26,71,72],{},"queryContent()",". Note: content ",[26,75,76],{},"_path"," values have no locale prefix (",[26,79,80],{},"\u002Fprojects\u002Ffinixa","), so queries target the slug rather than ",[26,83,84],{},"route.path"," (which is ",[26,87,88],{},"\u002Fen","-prefixed in English).",[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],{},"URL-based i18n (",[26,96,97],{},"@nuxtjs\u002Fi18n",", ",[26,100,101],{},"prefix_except_default",")"," — FR at ",[26,105,28],{},[26,107,32],{},", each language with its own crawlable URL. Per-locale ",[26,110,36],{}," + canonical via ",[26,113,114],{},"useLocaleHead()",", with the ",[26,117,118],{},"lang"," cookie kept for the visitor's preference. Switching language is a navigation (",[26,121,122],{},"switchLocalePath()",") — the blind animation is played by the page transition. ",[26,125,126],{},"queryContent"," queries filter by ",[26,129,130],{},"_locale",".",[46,133,134,140,141,144,145,148,149,152,153,156],{},[13,135,136,137,102],{},"Auto sitemap (",[26,138,139],{},"@nuxtjs\u002Fsitemap"," — ",[26,142,143],{},"\u002Fsitemap.xml"," is a per-locale index (",[26,146,147],{},"fr-FR.xml"," + ",[26,150,151],{},"en-US.xml",") generated from the prerendered routes, fed by the canonical site URL (",[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 without a CSS framework"," — custom design system: ",[26,164,165],{},"space($n)"," function (= ",[26,168,169],{},"n × 4px","), ",[26,172,173],{},"transition()"," mixin, CSS custom properties for light\u002Fdark theming (",[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 with Netlify provider"," — project cover images (",[26,194,195],{},".webp",") are served through the Netlify CDN with on-the-fly resizing at delivery.",[46,198,199,140,202,204,205,208],{},[13,200,201],{},"Git LFS",[26,203,195],{}," files are tracked via ",[26,206,207],{},".gitattributes"," to avoid storing large binaries in git history.",[46,210,211,140,214,217,218,221],{},[13,212,213],{},"Netlify Edge Function (Deno)",[26,215,216],{},"netlify\u002Fedge-functions\u002Fspotify.ts"," serves the ",[26,219,220],{},"\u002Fapi\u002Fspotify"," route. Exchanges a refresh token for an access token on every request, without storing tokens or exposing credentials client-side.",[46,223,224,227,228,231],{},[13,225,226],{},"Umami Analytics"," — async script loaded via ",[26,229,230],{},"useHead",", no cookies, no personal data sent to third parties.",[18,233,235],{"id":234},"notable-technical-points","Notable technical points",[43,237,238,265,286,303,338],{},[46,239,240,246,247,250,251,254,255,258,259,261,262,264],{},[13,241,242,243],{},"CSS Grid with ",[26,244,245],{},"display: contents",": the main grid is defined on ",[26,248,249],{},".page"," (4–5 columns). ",[26,252,253],{},"AppHeader"," and ",[26,256,257],{},"AppSection"," use ",[26,260,245],{},", making their child cells direct participants of the parent grid regardless of depth in the Vue component tree. Editorial layout without wrapper noise — but ",[26,263,245],{}," removes elements from the accessibility tree, requiring explicit ARIA handling on affected components.",[46,266,267,270,271,274,275,278,279,282,283,131],{},[13,268,269],{},"CSS-only page transitions",": transitions use the ",[26,272,273],{},".cell:before"," pseudo-element positioned off-screen by default (",[26,276,277],{},"inset: -1px calc(100% + 1px) -1px -1px","). The ",[26,280,281],{},".page-leave-to"," class slides it over the full cell, creating coordinated blind-closing effects. Zero JavaScript — only Nuxt page transition hooks + CSS. Cleanly disabled via ",[26,284,285],{},"prefers-reduced-motion",[46,287,288,291,292,295,296,298,299,302],{},[13,289,290],{},"Slug validation middleware",": ",[26,293,294],{},"middleware\u002Fproject.ts"," runs a ",[26,297,72],{}," before rendering the project page. If the slug doesn't exist, ",[26,300,301],{},"abortNavigation()"," is called — navigation is cancelled cleanly instead of rendering an empty page. The check is locale-agnostic (projects exist in both FR and EN), which keeps the middleware out of the i18n lifecycle.",[46,304,305,291,308,311,312,315,316,319,320,323,324,98,327,254,330,333,334,337],{},[13,306,307],{},"Inlined SVGs for diagrams",[26,309,310],{},"ProseImg.vue"," detects ",[26,313,314],{},".svg"," images and fetches them via ",[26,317,318],{},"useFetch"," to inject as raw HTML inside a ",[26,321,322],{},".svg-wrapper",". Diagrams are therefore themable via CSS — hence the explicit ",[26,325,326],{},"fill",[26,328,329],{},"stroke",[26,331,332],{},"color"," overrides on internal SVG elements in ",[26,335,336],{},"[slug].vue"," for dark mode.",[46,339,340,343,344,347,348,351,352,355],{},[13,341,342],{},"Stagger animation via IntersectionObserver",": the project list animates with an ",[26,345,346],{},"IntersectionObserver"," (threshold 0.15). Each project link gets a ",[26,349,350],{},"transitionDelay"," calculated from its index (",[26,353,354],{},"index × 400ms","), producing the staggered entrance without an animation library.",[18,357,359],{"id":358},"what-i-learned","What I learned",[10,361,362,363,365,366,368],{},"Designing a CSS Grid layout that flows through multiple Vue component levels via ",[26,364,245],{}," requires a precise understanding of its accessibility tree impact: elements with ",[26,367,245],{}," are transparent to the visual DOM but not to screen readers. The CSS-only page transition required several iterations of timing to coordinate the old component's exit with the new one's entrance without a flash of content in between.",[370,371],"hr",{},[18,373,375],{"id":374},"diagrams","Diagrams",[377,378,380],"h3",{"id":379},"global-architecture","Global architecture",[10,382,383],{},[384,385],"img",{"alt":380,"src":386},"\u002Fdiagrams\u002Fportfolio-dev\u002Farchitecture.svg",[370,388],{},[377,390,392],{"id":391},"reactive-i18n-system","Reactive i18n system",[10,394,395],{},[384,396],{"alt":392,"src":397},"\u002Fdiagrams\u002Fportfolio-dev\u002Fi18n.svg",[370,399],{},[377,401,403],{"id":402},"css-grid-layout-structure","CSS Grid layout structure",[10,405,406],{},[384,407],{"alt":403,"src":408},"\u002Fdiagrams\u002Fportfolio-dev\u002Fgrid.svg",{"title":410,"searchDepth":411,"depth":411,"links":412},"",2,[413,414,415,416,417],{"id":20,"depth":411,"text":21},{"id":40,"depth":411,"text":41},{"id":234,"depth":411,"text":235},{"id":358,"depth":411,"text":359},{"id":374,"depth":411,"text":375,"children":418},[419,421,422],{"id":379,"depth":420,"text":380},3,{"id":391,"depth":420,"text":392},{"id":402,"depth":420,"text":403},"Bilingual portfolio built with Nuxt 3 in SSR + static prerendering — per-URL bilingual (FR \u002F EN), Markdown content, live Spotify integration and CSS-only page transitions.","md",[426,427],"Git Repository","https:\u002F\u002Fgithub.com\u002FOwenLB\u002Fportfolio-dev",false,{},true,4,"\u002Fprojects\u002Fportfolio-dev",{"title":5,"description":423},[435,436,437,438,439,97,139,440,441],"Nuxt 3","TypeScript","Vue 3","@nuxt\u002Fcontent","@nuxt\u002Fimage","SCSS","Netlify","projects\u002Fportfolio-dev","Website","2026-07-17",[446,447],"Visit site","https:\u002F\u002Fowenlebec.fr","ls4IUJeSbYP3nlU2xjDUlJxpSTJmbsfKUQMCJVIIEJw",[450,454,458,462],{"title":451,"type":452,"path":80,"order":453},"Finixa","Web & mobile app",1,{"title":455,"type":456,"path":457,"order":411},"Peliculle","iOS app","\u002Fprojects\u002Fpeliculle",{"title":459,"type":460,"path":461,"order":420},"ScanAuto","Web app","\u002Fprojects\u002Fscanauto",{"title":463,"type":464,"path":465,"order":466},"Photo Portfolio","Full-stack site","\u002Fprojects\u002Fportfolio-photo",5,{"title":468},"Legal notices",1784470460454]