Construire un portfolio open-source moderne avec Nuxt 3 — thèmes CSS, données JSON, SEO automatique, et déploiement Docker.
OpenFolio n'a pas de base de données. Tout le contenu vit dans /public/data/. Le composable useJson fait le pont entre ces fichiers et les composants Vue.
export function useJson<T>(url: string) {
return useFetch<T>(url, {
key: url,
server: true,
lazy: false,
})
}useFetch est le composable natif de Nuxt pour les requêtes HTTP. Il gère automatiquement :
pending, data, error<script setup lang="ts">
interface Profile {
name: string
username: string
title: string
bio: string
avatar: string
available: boolean
social: Array<{ type: string; url: string; label: string }>
}
const { data: profile, pending, error } = await useJson<Profile>('/data/profile.json')
</script>
<template>
<div v-if="pending">Chargement...</div>
<div v-else-if="error">Erreur de chargement</div>
<div v-else>
<h1>{{ profile?.name }}</h1>
<p>{{ profile?.bio }}</p>
</div>
</template>Le await sur useJson bloque le rendu jusqu'à ce que les données soient disponibles. En SSR, ça signifie que le HTML envoyé au client contient déjà le profil — pas de flash de contenu vide.
[
{
"id": 1,
"title": "E-Commerce Modern",
"description": "Plateforme e-commerce avec Nuxt.js et Stripe",
"category": "Web",
"status": "completed",
"technologies": ["Nuxt.js", "Stripe", "PostgreSQL"],
"github": "https://github.com/...",
"demo": "https://...",
"image": "/images/projects/ecommerce.jpg",
"featured": true,
"metrics": {
"duration": "3 months",
"team": 2,
"impact": "+40% ventes"
}
}
]status peut être "completed", "in-progress", ou "planned" — la page projets affiche un badge coloré selon cette valeur.
La page projets extrait dynamiquement les catégories uniques depuis les données :
<script setup lang="ts">
const { data: projects } = await useJson<Project[]>('/data/projects.json')
const activeFilter = ref<string>('Tous')
const categories = computed(() =>
['Tous', ...new Set(projects.value?.map(p => p.category) ?? [])]
)
const filtered = computed(() =>
activeFilter.value === 'Tous'
? projects.value ?? []
: projects.value?.filter(p => p.category === activeFilter.value) ?? []
)
</script>Ajouter une catégorie "Mobile" dans projects.json = elle apparaît automatiquement dans les filtres. Aucun code à modifier.
[
{
"company": "Startup XYZ",
"role": "Développeur Frontend",
"period": "2023 — 2025",
"description": "Refonte complète de l'interface avec Vue 3.",
"technologies": ["Vue 3", "TypeScript", "Figma"],
"current": false
}
]current: true marque le poste actuel avec un indicateur visuel dans la timeline.
[
{
"category": "Frontend",
"items": [
{ "name": "Vue.js", "level": 90 },
{ "name": "TypeScript", "level": 80 },
{ "name": "Nuxt 3", "level": 85 }
]
},
{
"category": "Backend",
"items": [
{ "name": "Node.js", "level": 75 },
{ "name": "PostgreSQL", "level": 70 }
]
}
]level est une valeur entre 0 et 100 qui pilote la largeur de la barre de progression. 80 = "solide", 90+ = "expert".
nuxt.config.ts précharge les JSON critiques pour éviter un waterfall de requêtes :
nitro: {
prerender: {
crawlLinks: true,
},
},
app: {
head: {
link: [
{ rel: 'preload', as: 'fetch', href: '/data/projects.json', crossorigin: 'anonymous' },
{ rel: 'preload', as: 'fetch', href: '/data/profile.json', crossorigin: 'anonymous' },
],
},
},Le navigateur commence à télécharger les JSON dès que le HTML arrive, avant même que Vue soit initialisé.