Pourquoi refaire son portfolio ?
Parce que, pourquoi pas ?
Mon ancien portfolio me faisait mal aux yeux (et pas que les miens apparemment). HTML/CSS vanilla avec du jQuery qui traînait depuis 2018... Autant dire que c'était l'époque des dinosaures du web. Il était temps de passer aux choses sérieuses et de montrer ce que je sais vraiment faire !
Cette fois-ci, j'ai voulu créer quelque chose de vraiment interactif avec des widgets qui bougent, une horloge temps réel, et même une carte interactive. Le tout avec une architecture moderne et des performances de ouf.
Le choix de la stack technique
Next.js 15 - La mise à jour qui change tout !
J'ai opté pour Next.js 15.5 avec l'App Router (parce que oui, je vis dangereusement). Pourquoi ? Simple :
- SSG (Static Site Generation) pour des performances de fou
- App Router pour une architecture moderne
- Turbopack en mode dev (c'est de la balle, croyez-moi)
- React 19 support intégral
- Image optimization automatique avec WebP/AVIF
- Metadata API pour le SEO
# Le bonheur du dev moderne
pnpm dev --turbo # Et c'est parti ! 🚀
Fini l'époque où il fallait configurer Webpack pendant 3 heures pour avoir un truc qui marche.
TypeScript - Parce que je n'aime pas les surprises
TypeScript partout, même dans mes rêves.
Plus jamais de undefined is not a function
à 3h du matin !
// Ça, c'est du propre 💪
interface PostMetadata extends BaseMetadata {
date: string;
}
export type MDXData<T extends BaseMetadata> = {
metadata: T;
slug: string;
content: string;
reading?: {
readingTime: string;
words: number;
};
};
Tailwind CSS v4 - La révolution des styles
Tailwind CSS v4 avec PostCSS intégration native (oui, la dernière version, parce que...). Mes collègues me disent encore : "C'est moche dans le HTML". Mes collègues ont tort.
<h1 className="text-4xl font-extrabold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
C'est beau ou c'est beau ?
</h1>
Le plus cool ? CSS Custom Properties natif et container queries out-of-the-box !
Les nouvelles additions qui claquent
Motion (ex-Framer Motion) pour les animations fluides :
import { motion } from 'motion';
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
Salut les animations smooth ! ✨
</motion.div>
Zustand pour la gestion d'état légère et efficace :
import { create } from 'zustand';
const useTimeStore = create((set) => ({
time: new Date(),
updateTime: () => set({ time: new Date() }),
}));
L'architecture du projet
Système de contenu avec MDX
Le plus stylé dans ce portfolio : tout le contenu est en MDX. Markdown + JSX = le mariage parfait.
J'ai créé un système de frontmatter custom :
const parseFrontmatter = <T extends BaseMetadata>(fileContent: string) => {
// Parsing magique du frontmatter
// Avec gestion d'erreurs qui claque
};
Et le plus cool ? J'ai ajouté des composants React directement dans mes articles :
<Today />
pour afficher la date du jour<CurrentDate />
avec format personnalisé- Système de headings avec anchors automatiques
Système de grille responsive multi-layout
Une grille modulaire avancée avec react-grid-layout
et layouts dynamiques :
- 4 layouts différents : all, about, projects, blog
- Responsive sur 3 breakpoints (lg, md, sm)
- Widgets interactifs avec état persisté
- Configuration TypeScript avec validation d'overlaps
export const layouts = {
all: {
lg: [
{ i: 'bio', x: 0, y: 0, w: 2, h: 1 },
{ i: 'location', x: 2, y: 0, w: 1, h: 1 },
{ i: 'clock', x: 3, y: 0, w: 1, h: 1 },
// ...11 widgets au total
],
md: [/* layout tablette */],
sm: [/* layout mobile */]
},
about: {/* layout page à propos */},
projects: {/* layout projets */},
blog: {/* layout blog */}
};
// Fonction de validation automatique des overlaps
export const checkOverlaps = (layouts) => {
// Détection automatique des collisions de widgets
const overlaps = [];
// ... logique de validation
return overlaps;
};
Widgets modulaires - L'écosystème interactif
11 widgets interactifs avec des fonctionnalités avancées :
Widgets d'information personnelle :
- AboutMe : Présentation avec photo et memojis animés
- ContactMe : Formulaire de contact avec validation
- MapLocation : Carte interactive Mapbox GL avec contrôles zoom
Widgets de données temps réel :
- TimeClock : Horloge animée avec séparateur clignotant
- LinkedInFollowers : Stats LinkedIn via API
- GitHubStars : Nombre de stars GitHub via Octokit
- GitHubCommits : Commits récents avec SWR caching
Widgets de contenu :
- MyJourney : Dernier article de blog avec temps de lecture
- WorkJourney : Affichage dynamique des posts
- PortfolioJourney : Showcase du processus de création
- ThemeSwitcher : Switcher dark/light mode avec next-themes
Exemple du widget TimeClock avec animation fluide :
export const TimeClock = memo(() => {
const { hours, minutes } = useTimeParts();
const showSeparator = useShowSeparator();
useClockSync(); // Hook personnalisé pour sync temps réel
return (
<Card>
<div className="flex items-center justify-center overflow-hidden">
<AnimatedNumber value={hours} />
<ClockSeparator show={showSeparator} />
<AnimatedNumber value={minutes} />
</div>
<CurrentDate className="text-xl uppercase" format="weekday" />
</Card>
);
});
// Composant d'animation des nombres avec @number-flow/react
export const AnimatedNumber = ({ value }) => (
<NumberFlow
className="font-archivo-black font-bold text-4xl tabular-nums"
format={{ minimumIntegerDigits: 2 }}
value={value}
/>
);
Les défis techniques relevés
Gestion d'état temps réel avec Zustand
Le défi : synchroniser l'horloge en temps réel avec état persisté et optimisations.
Solution : Store Zustand avec hooks personnalisés et devtools intégrés :
const useTimeStore = create()(
devtools(
persist(
subscribeWithSelector((set, get) => ({
time: new Date(),
showSeparator: true,
blinkInterval: null,
updateTime: () => set({ time: new Date() }),
toggleSeparator: () => set(state => ({
showSeparator: !state.showSeparator
})),
startBlinking: () => {
const interval = setInterval(() => {
get().toggleSeparator();
}, 500);
set({ blinkInterval: interval });
},
// Hook d'optimisation temps réel
getTimeParts: () => {
const { time } = get();
return {
hours: time.getHours(),
minutes: time.getMinutes(),
seconds: time.getSeconds(),
};
}
}))
)
)
);
// Hooks dérivés pour optimiser les re-renders
export const useTimeParts = () => {
const hours = useTimeStore(state => state.time.getHours());
const minutes = useTimeStore(state => state.time.getMinutes());
return { hours, minutes };
};
Résultat : horloge fluide avec 0 re-renders inutiles !
Calcul du temps de lecture optimisé
J'ai optimisé le calcul du temps de lecture avec caching React :
export const readingTimeOnArticle = cache(
(content: string, lang: Language = 'fr'): ReadingTimeResult => {
// Early return pour contenu vide
if (!content?.trim()) return defaultResult;
// Calcul optimisé avec regex compilées
const minutes = parseInt(duration.match(MINUTES_REGEX)?.[0] || '0');
// ...
}
);
Résultat : temps de build divisé par 3 !
Gestion des headings avec composants
Le défi : utiliser <Today />
dans un titre MDX sans planter l'extraction de slug.
Solution : fonction récursive pour extraire le texte des composants React :
const getTextContent = (node: React.ReactNode): string => {
if (typeof node === 'string') return node;
if (Array.isArray(node)) return node.map(getTextContent).join('');
if (React.isValidElement(node) && node.props.children) {
return getTextContent(node.props.children);
}
return '';
};
Intégrations API temps réel
Le défi : récupérer des données GitHub/LinkedIn sans impacter les performances.
Solution : SWR avec Octokit et mise en cache intelligente :
// Hook GitHub avec SWR et Octokit
export const useGitHubStats = () => {
const { data: stars } = useSWR('/api/github/stars', fetcher, {
refreshInterval: 300000, // 5 minutes
revalidateOnFocus: false,
});
const { data: commits } = useSWR('/api/github/commits', async () => {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const { data } = await octokit.rest.repos.listCommits({
owner: 'username',
repo: 'portfolio',
per_page: 5,
});
return data;
});
return { stars, commits };
};
Intégration Mapbox GL pour la géolocalisation
Carte interactive avec contrôles personnalisés :
import MapGL, { Marker } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
export const MapView = () => {
const [viewState, setViewState] = useState({
longitude: 2.3522,
latitude: 48.8566,
zoom: 10
});
return (
<MapGL
{...viewState}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
onMove={evt => setViewState(evt.viewState)}
mapStyle="mapbox://styles/mapbox/dark-v11"
>
<Marker longitude={2.3522} latitude={48.8566}>
<LocationMarker />
</Marker>
</MapGL>
);
};
Optimisations des performances
- Images optimisées avec
next/image
et formats WebP/AVIF - Fonts préchargés avec
next/font
(Geist Sans, Geist Mono, Pixelify Sans) - Bundle analysis avec Turbopack et webpack optimizations
- Lazy loading des composants non-critiques avec dynamic imports
- Caching intelligent avec React
cache()
et SWR - Code splitting automatique par route et composant
- Compression Gzip/Brotli côté Vercel
Les features qui claquent
Système d'animations avancé
Motion (ex-Framer Motion) avec animations custom :
- Sparkles particules avec
tsparticles
sur hover - Number Flow pour les animations de nombres
- Transitions fluides entre les pages avec
motion/stagger
- Hover effects avec CSS variables et transformations
// Composant Sparkles avec tsparticles
import { Particles } from '@tsparticles/react';
import { loadSlim } from '@tsparticles/slim';
export const Sparkles = () => {
return (
<Particles
init={loadSlim}
options={{
particles: {
color: { value: '#FFD700' },
move: {
enable: true,
speed: 2,
direction: 'none',
outModes: { default: 'destroy' }
},
opacity: {
value: { min: 0.3, max: 1 }
}
}
}}
/>
);
};
Thème sombre/clair avec persistance
Avec next-themes
et système de couleurs CSS custom :
// Provider global avec persistance système
export const ThemeProvider = ({ children }) => {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange={false}
>
{children}
</NextThemesProvider>
);
};
// Hook d'utilisation avec transition smooth
const { theme, setTheme, systemTheme } = useTheme();
Système d'icônes modulaire complet
25+ composants SVG optimisés avec tree-shaking :
- Technos : React, Next.js, TypeScript, Tailwind, Python, Node.js...
- Outils : Git, GitHub, Figma, Storybook, Docker...
- Optimisation : SVG minifiés, pas de icon font, loading lazy
// Exemple d'icône optimisée
export const ReactIcon = ({ size = 24 }) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
className="fill-current text-[#61DAFB]"
>
<path d="M12 10.11c1.03 0 1.87.84 1.87 1.89s-.84 1.89-1.87 1.89-1.87-.84-1.87-1.89.84-1.89 1.87-1.89z"/>
</svg>
);
Analytics et monitoring avancés
Vercel Analytics + Speed Insights intégrés :
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
// Dans layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}
Le workflow de développement
Stack d'outils moderne
- Biome v2.2.0 pour le linting et formatting (adieu ESLint + Prettier)
- TypeScript 5.9 strict mode avec path mapping
- pnpm 10.14 comme package manager avec workspace
- taze pour les updates de dépendances
- ultracite pour l'analyse de bundle
Configuration Biome optimisée :
{
"formatter": {
"indentStyle": "tab",
"lineWidth": 100
},
"linter": {
"rules": {
"style": {
"useImportType": "error",
"useExportType": "error"
},
"nursery": {
"useSortedClasses": "error"
}
}
},
"organizeImports": {
"enabled": true
}
}
# Commands que j'utilise tous les jours
pnpm dev --turbo # Dev server avec Turbopack 🚀
pnpm build # Build optimisé
pnpm biome check . # Linting + formatting
pnpm biome check --write . # Auto-fix
pnpm taze # Update dependencies
Déploiement
Vercel évidemment (ils font Next.js, logique non ?).
- Auto-deploy sur push main
- Preview deployments sur PR
- Analytics intégrées
- Performance monitoring
Les métriques qui font plaisir
Lighthouse Score
- Performance : 100/100 ✅
- Accessibility : 100/100 ✅
- Best Practices : 100/100 ✅
- SEO : 100/100 ✅
Ce que j'ai appris
Next.js c'est puissant
L'App Router est vraiment bien pensé une fois qu'on comprend les concepts. Les Server Components changent la donne pour les performances.
TypeScript partout
Même pour un projet personnel, TypeScript me fait gagner un temps fou. Plus d'erreurs de typos, autocomplétion parfaite, refactoring sécurisé.
L'optimisation, c'est important
Un portfolio qui met 10 secondes à charger, c'est 10 secondes de trop. Chaque milliseconde compte pour l'expérience utilisateur.
Ce qui vient ensuite
Fonctionnalités prévues
Widgets interactifs :
- Drag & drop des widgets (react-grid-layout le permet déjà)
- Widget Spotify avec lecture en temps réel
- Widget météo avec géolocalisation
- Widget GitHub activity timeline
Améliorations techniques :
- Migration vers Bun comme runtime (2x plus rapide)
- Integration Three.js pour des effets 3D subtils
- PWA avec service worker et offline mode
- Micro-interactions avec
@use-gesture/react
Optimisations avancées :
// Préchargement intelligent des routes
const PreloadLink = ({ href, children }) => {
return (
<Link
href={href}
onMouseEnter={() => router.prefetch(href)}
>
{children}
</Link>
);
};
// Lazy loading conditionnel
const LazyWidget = dynamic(
() => import('../widgets/HeavyWidget'),
{
loading: () => <Skeleton />,
ssr: false // Only load client-side if needed
}
);
Aujourd'hui, ...
Le portfolio est en ligne et je suis fier du résultat !
- 11 widgets interactifs avec données temps réel
- Architecture moderne avec React 19 + Next.js 15
- Performances parfaites : Lighthouse 100/100 sur tous les critères
- DX exceptionnelle avec Biome, TypeScript strict, et hot reload
- UX fluide avec animations Motion et transitions optimisées
Stack technique finale :
- Frontend : React 19, Next.js 15.5, TypeScript 5.9
- Styling : Tailwind CSS v4, CSS Custom Properties
- State : Zustand avec devtools et persistance
- Animation : Motion, Number Flow, tsParticles
- Data fetching : SWR, Octokit pour GitHub API
- Maps : Mapbox GL, react-map-gl
- Tooling : Biome, pnpm, taze, Turbopack
- Deploy : Vercel avec Analytics et Speed Insights
- Monitoring : Vercel Analytics, Core Web Vitals
Le plus drôle ? Maintenant, j'ai envie de le refactorer encore (maladie professionnelle). Peut-être migrer vers Bun, ou tester Tauri, ou ajouter WASM (pourquoi pas ?)
Un développeur qui n'a pas envie d'améliorer son code, c'est un développeur qui s'ennuie, non ? 😄
Fun fact : aucune ligne de jQuery dans ce projet, et même pas de useEffect
dans l'horloge grâce à Zustand ! ✨