Aller au contenu principal
Portfolio Développeur

ProjetSite

Portfolio Développeur

EN BREF

Portfolio bilingue construit avec Nuxt 3 en SSR + prérendu statique — bilingue par URL (FR / EN), contenu Markdown, intégration Spotify live et transitions de page natives (View Transitions API).

Statut
En ligne
Période
2023 — aujourd'hui
Rôle
Solo — workflow de développement agentique (Claude Code)
Répertoire Git Voir le site

STACK

  • Nuxt 3
  • TypeScript
  • @nuxt/content
  • @nuxt/image
  • @nuxtjs/i18n
  • @nuxtjs/sitemap
  • Netlify

Le site que vous lisez en ce moment : bilingue par URL, prérendu statique, et pensé autant pour le SEO et l'accessibilité que pour le soin du mouvement.

Quel est le contexte de ce portfolio ?

Portfolio personnel qui regroupe projets, expériences et informations de contact. Le site est bilingue par URL (FR à /, EN sous /en/…), embarque une intégration Spotify temps réel qui affiche le morceau que j'écoute à l'instant, et est déployé sur Netlify en SSR + prérendu statique : chaque page des deux langues est générée à la build, avec hreflang et canonical. La route API Spotify tourne via une Netlify Edge Function (Deno). Le contenu est entièrement en Markdown pour pouvoir le tenir à jour sans toucher au code.

Quelle stack technique ?

  • Nuxt 3 (ssr: true + prérendu) — le build prérend chaque route des deux langues en HTML complet (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.
  • @nuxt/content v3 — contenu entièrement en Markdown, découpé en collections typées par un schéma Zod (content.config.ts) : une collection par page et par langue (home_fr, projects_en…), requêtées via queryCollection(). Pas de base de données ni d'API REST — les fichiers .md sont la source de vérité. Les chemins de contenu ne portent pas de préfixe de locale (/projects/finixa), donc les requêtes ciblent le slug et non route.path (préfixé /en en anglais).
  • i18n par URL (@nuxtjs/i18n, prefix_except_default) — FR à /, EN sous /en/…, chaque langue ayant sa propre URL crawlable. hreflang + canonical par locale via useLocaleHead(), cookie lang conservé pour la préférence du visiteur. Changer de langue est une navigation (switchLocalePath()). Changement de langue par URL
  • Sitemap auto (@nuxtjs/sitemap) — un index par locale (fr-FR.xml + en-US.xml) généré depuis les routes prérendues, alimenté par l'URL canonique (site.url). Soumission IndexNow via un script dédié.
  • SCSS sans framework CSS — design system maison : tokens SCSS injectés dans chaque composant (space($n) = n × 4px, mixin transition()), et un unique fichier global qui émet les custom properties (thème clair/sombre, easings, durées, échelle typographique fluide en clamp()).
  • Polices auto-hébergées — PP Formula Condensed, Strawford et JetBrains Mono en woff2 subsettés, les trois fichiers au-dessus de la ligne de flottaison préchargés, et un @font-face de repli à métriques ajustées (size-adjust, ascent-override…) par famille pour garder un CLS ≈ 0.
  • @nuxt/image avec provider Netlify — images optimisées à la livraison par le CDN Netlify ; les .webp sont stockées via Git LFS.
  • Netlify Edge Function (Deno) — netlify/edge-functions/spotify.ts sert la route /api/spotify. Échange le refresh token contre un access token à chaque appel, sans stocker de token ni exposer les credentials côté client. Côté page, le widget est hydraté en différé (hydrate-on-idle) pour rester hors du chemin critique.
  • Umami Analytics — script async, sans cookie, sans transmission de données personnelles à des tiers.

Architecture globale

Points techniques notables

  • CSS Grid avec display: contents : la grille principale est définie sur .page (4–5 colonnes). AppHeader et AppSection utilisent display: contents, 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 display: contents retire les éléments de l'arbre d'accessibilité, ce qui a demandé des ajustements ARIA sur les composants concernés.

Structure CSS Grid

  • Transitions de page natives, avec repli : quand le navigateur supporte la View Transitions API, la navigation est animée nativement et la carte projet cliquée sur l'accueil se transforme directement en hero de la page projet, grâce à des view-transition-name par slug partagés entre les deux pages. Sans support, un rideau CSS prend le relais : le pseudo-élément .cell:before de chaque cellule glisse sur toute la cellule pendant la transition de page Vue. Le rideau est aussi conservé au premier chargement et au changement de langue, où il se lit mieux qu'un fondu.
  • Un système de mouvement unique, qui respecte prefers-reduced-motion : deux directives partagées pilotent l'essentiel — v-reveal (apparition au scroll via un seul IntersectionObserver partagé, avec un délai optionnel pour décaler les éléments) et v-decode (effet de décodage façon Matrix sur les titres, dont le nom accessible est figé via aria-label avant le brouillage). Les classes ne sont ajoutées que côté client : le HTML prérendu reste entièrement visible et lisible. Le changement de thème est une révélation circulaire depuis le bouton (View Transition + clip-path), la frise des expériences un scrollytelling avec compteur d'années, et la barre de progression de lecture des pages projet est en pur CSS (animation-timeline: scroll()).
  • Middleware de validation de slug : middleware/project.ts exécute un queryCollection() avant le rendu de la page projet. Si le slug n'existe pas, 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.
  • SVG inlinés pour les diagrammes : ProseImg.vue détecte les images .svg et les récupère via useFetch pour les injecter en HTML brut dans un .svg-wrapper. Les diagrammes sont ainsi thémables via CSS — d'où les overrides explicites de fill, stroke et color sur les éléments SVG internes dans [slug].vue pour le mode sombre.
  • Un contenu qui se tient à jour tout seul : les années d'expérience affichées sont calculées depuis la frise des expériences (alternance + CDI), les durées de chaque poste depuis leurs dates, et la date de mise à jour des projets (dateModified du JSON-LD) est tirée du dernier commit Git du fichier au moment de la build.

Ce que j'ai appris

Concevoir un layout CSS Grid qui traverse plusieurs niveaux de composants Vue via display: contents demande une connaissance précise de l'impact sur l'arbre d'accessibilité : les éléments avec display: contents sont transparents pour le rendu visuel mais pas pour les lecteurs d'écran. Côté mouvement, la difficulté a été d'empiler plusieurs mécanismes (View Transitions, rideau de repli, révélations au scroll) sans qu'ils se déclenchent en double, tout en gardant un HTML prérendu complet pour le SEO et un site entièrement utilisable sans animation.