iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Vue · Intermédiaire

OpenFolio

Construire un portfolio open-source moderne avec Nuxt 3 — thèmes CSS, données JSON, SEO automatique, et déploiement Docker.

Nuxt 3Vue 3TypeScriptTailwind CSSDocker
GitHub
01Setup et configuration02Structure et routing Nuxt 303Données JSON et composables04Système de thèmes CSS05Pages et composants06SEO et performances07Déploiement Docker et Vercel
Chapitre 6·20 min

SEO et performances

OpenFolio cible un score Lighthouse 95+. Ça ne vient pas par hasard — c'est une combinaison de SSR, de balises méta dynamiques, et de quelques règles simples sur les images et les fonts.

useSeoMeta — balises méta dynamiques

Nuxt 3 expose useSeoMeta pour définir les balises <head> depuis le <script setup> d'une page :

pages/index.vue
<script setup lang="ts">
const { data: profile } = await useJson<Profile>('/data/profile.json')
 
useSeoMeta({
  title: () => `${profile.value?.name} — Portfolio`,
  description: () => profile.value?.bio ?? '',
  ogTitle: () => `${profile.value?.name} — Portfolio`,
  ogDescription: () => profile.value?.bio ?? '',
  ogType: 'website',
  twitterCard: 'summary_large_image',
  twitterTitle: () => `${profile.value?.name} — Portfolio`,
  twitterDescription: () => profile.value?.bio ?? '',
})
</script>

Les valeurs en fonction () => ... sont réactives : si profile.value change, les balises méta se mettent à jour automatiquement.

JSON-LD — données structurées

Les bots de Google et les LLMs (Perplexity, ChatGPT) consomment le JSON-LD pour comprendre qui vous êtes. À ajouter dans index.vue :

pages/index.vue
<script setup lang="ts">
const { data: profile } = await useJson<Profile>('/data/profile.json')
 
useHead({
  script: [
    {
      type: 'application/ld+json',
      innerHTML: JSON.stringify({
        '@context': 'https://schema.org',
        '@type': 'Person',
        name: profile.value?.name,
        jobTitle: profile.value?.title,
        url: 'https://votre-domaine.com',
        sameAs: profile.value?.social.map(s => s.url) ?? [],
      }),
    },
  ],
})
</script>

Le type Person est le bon schéma pour un portfolio. Google Search Console l'affiche dans le Knowledge Panel si les données sont cohérentes.

Images — next/image n'existe pas ici

Nuxt n'a pas de composant d'image optimisé natif comme Next.js. Pour rester performant :

<!-- Toujours spécifier width et height pour éviter le Layout Shift -->
<img
  :src="project.image"
  :alt="project.title"
  width="400"
  height="300"
  loading="lazy"
  decoding="async"
/>

loading="lazy" charge l'image seulement quand elle entre dans le viewport. decoding="async" débloque le thread principal pendant le décodage.

Pour aller plus loin, le module @nuxt/image ajoute un composant <NuxtImg> qui fait la conversion WebP et le redimensionnement automatique.

Fonts — éviter le FOUT

OpenFolio charge Inter depuis Google Fonts. Sans précaution, la police se charge après le CSS, provoquant un flash de police système.

Solution dans nuxt.config.ts :

nuxt.config.ts
app: {
  head: {
    link: [
      {
        rel: 'preconnect',
        href: 'https://fonts.googleapis.com',
      },
      {
        rel: 'preconnect',
        href: 'https://fonts.gstatic.com',
        crossorigin: '',
      },
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
      },
    ],
  },
},

display=swap dans l'URL Google Fonts garantit que le texte reste visible avec la police de fallback pendant le chargement.

Cache des JSON — routeRules

nuxt.config.ts
nitro: {
  routeRules: {
    '/data/**': {
      headers: { 'cache-control': 'public, max-age=300, stale-while-revalidate=600' },
    },
  },
},

stale-while-revalidate=600 : si le cache de 5 minutes est expiré, le navigateur sert la version stale pendant qu'il télécharge la nouvelle en fond. Zéro latence perçue pour l'utilisateur.

Canonical et robots

nuxt.config.ts
app: {
  head: {
    link: [{ rel: 'canonical', href: 'https://votre-domaine.com' }],
    meta: [{ name: 'robots', content: 'index, follow' }],
  },
},

À adapter par page si plusieurs URLs pointent vers le même contenu (ex: paramètres de filtre dans l'URL de la page projets).

Checklist Lighthouse 95+

  • SSR activé → HTML complet indexable dès le premier chargement
  • loading="lazy" sur toutes les images hors viewport
  • preconnect sur les domaines tiers (Google Fonts)
  • Fonts avec display=swap
  • JSON en cache avec Cache-Control
  • Pas de JavaScript bloquant au-dessus du fold
  • width et height explicites sur les images pour éviter le Cumulative Layout Shift
Précédent
Pages et composants
Suivant
Déploiement Docker et Vercel

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main