Mon portfolio - choix techniques et défis

jeu. 28 août 202514 minutes2424 mots

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 ! ✨