Gabriel Mustiere
Gabriel Mustiere
Portrait de Gabriel Mustiere
CTO
freelance

Tech · Business · IA. Nantes — remote.

Parcours
№ 017 min

Comment j'ai construit ce site avec Claude et Astro

Retour d'expérience sur la fabrication de mustiere.fr : design avec Claude, implémentation avec Claude Code et Astro, et pourquoi le SEO s'est joué dans le schéma de contenu plutôt que dans les mots-clés.

§ sommaire
  1. 01Étape 1 — designer avec Claude Design, pas avec Figma
  2. 02Étape 2 — implémenter avec Claude Code et Astro
  3. 03Focus SEO — six briques non négociables
  4. 1. Un schéma de contenu strict
  5. 2. Hreflang et canonical systématiques
  6. 3. JSON-LD pour chaque type d’entité
  7. 4. Sitemap et robots.txt IA-friendly
  8. 5. Un llms.txt pour l’ère des LLM
  9. 6. Statique, léger, rapide
  10. 04Ce que je ferais différemment
  11. 05Le code est ouvert

Ce site existait depuis trois ans sous forme d’une page HTML figée. Je voulais un vrai blog, bilingue, propre sur le SEO, sans me transformer en développeur frontend à temps plein. Je m’en suis sorti en deux week-ends grâce à deux outils : Claude pour le design, Claude Code pour l’implémentation en Astro. Voici ce que j’ai retenu, avec un focus appuyé sur la partie référencement — c’est là que la plupart des portfolios de CTO se plantent.

Étape 1 — designer avec Claude Design, pas avec Figma

Je ne suis pas designer. Ouvrir Figma pour itérer sur une page d’accueil coûte cher en friction. Claude Design, lui, sait produire une maquette, du HTML + Tailwind qui s’affiche immédiatement dans un navigateur. Le processus :

  1. Je décris l’identité visuelle (éditoriale, sobre, typographie mixte serif + sans + mono) et les sections attendues.
  2. Claude Deisgn me rend trois artifacts HTML autonomes : accueil, liste d’articles, article.
  3. J’itère en langage naturel (« la sidebar est trop dense », « fais sauter le bandeau Tweaks », « passe l’accent de cette section en ocre »).
  4. À la fin, je demande à Claude de rédiger un handoff document : tokens de design, palette oklch, structure Astro recommandée, comportements JS.

Ce handoff, c’est le fichier README.md du repo — je l’utilise comme brief à l’entrée de Claude Code. Les maquettes HTML deviennent la référence de fidélité pixel-perfect ; Claude Code n’a plus qu’à les recoder en composants Astro idiomatiques.

Étape 2 — implémenter avec Claude Code et Astro

Pourquoi Astro ? Trois raisons concrètes pour un site de contenu :

  • Zéro JS par défaut. Ce que je n’écris pas ne peut pas casser, ne ralentit pas les Core Web Vitals, et ne m’oblige pas à hydrater des îlots pour afficher du texte.
  • Content Collections typées. Chaque article est un .mdx validé par un schéma Zod. Le build casse si un champ manque — pas de date manquante qui plombe le sitemap en prod.
  • i18n natif. Un simple bloc dans astro.config.mjs et je tiens deux arbres /fr et /en sans router maison.

Claude Code, lui, est à son aise sur ce genre de codebase : lecture large de la structure, édits ciblés, respect des conventions du projet. Je travaille en plan-then-execute : je lui demande d’abord de planifier un découpage en petites tâches, je relis, puis je laisse exécuter.

Focus SEO — six briques non négociables

Pour un site de CTO freelance, le SEO n’est pas un plan marketing. C’est l’hygiène qui fait que Google, Perplexity, ChatGPT et les recruteurs trouvent les bonnes pages dans le bon ordre. Voici ce qui porte 90 % du résultat.

1. Un schéma de contenu strict

La première brique SEO n’est pas dans le HTML, elle est dans le modèle de données. Chaque article déclare des champs obligatoires via Zod, sinon le build casse :

// src/content.config.ts
const blog = defineCollection({
  loader: chapteredGlob({
    base: './src/content/blog',
    extensions: ['.mdx', '.md'],
  }),
  schema: ({ image }) =>
    z.object({
      title: z.string().max(120),
      excerpt: z.string().min(80).max(220),
      publishedAt: z.coerce.date(),
      updatedAt: z.coerce.date().optional(),
      category: z.enum(['IA', 'Tech', 'Lead', 'Business']),
      tags: z.array(z.string()).default([]),
      keywords: z.array(z.string()).default([]),
      cover: image().optional(),
      resume: resumeSchema, // injecté depuis resume.mdx
      faq: z.array(faqItem).default([]), // injecté depuis faq.mdx
      sources: z.array(sourceItem).default([]), // injecté depuis sources.mdx
      number: z.number().int().positive(),
      lang: z.enum(['fr', 'en']).default('fr'),
      translationOf: z.string().optional(),
    }),
});

excerpt est borné entre 80 et 220 caractères parce que c’est la longueur utile d’une meta description. Le champ resume n’est pas une chaîne de frontmatter : c’est un objet { markdown, html, plain } injecté par chapteredGlob à partir d’un fichier resume.mdx réservé à la racine du dossier de l’article — même mécanique pour faq.mdx (questions structurées exposées en JSON-LD FAQPage) et sources.mdx (citations vérifiables). On édite du contenu, pas du YAML, et le résultat reste exploitable côté LLM via resume.plain. translationOf relie deux versions linguistiques du même article : indispensable pour les balises hreflang.

Format dossier multi-chapitres. Au-delà de 1500 mots, je découpe l’article en index.mdx (frontmatter + intro) + chapitres NN-<kebab>.mdx que le loader agrège alphabétiquement. Cet article-ci en est un exemple. Le slug public ne change pas — c’est le nom du dossier — mais la rédaction et la review d’un long billet deviennent gérables, fichier par fichier, plutôt que de naviguer dans un seul .mdx de 2000 lignes.

2. Hreflang et canonical systématiques

Un site bilingue mal balisé se tire une balle dans le pied : Google indexe les deux versions comme des doublons. Le BaseLayout émet une balise canonique et deux hreflang sur chaque page, en se basant sur le translationOf du contenu :

<link rel="canonical" href={canonicalURL} />
<link rel="alternate" hreflang={otherLang} href={altUrl} />
<link rel="alternate" hreflang={lang} href={canonicalURL} />
<link rel="alternate" hreflang="x-default" href={lang === 'fr' ? canonicalURL : altUrl} />

x-default pointe vers la version française — c’est ma version principale. Sans ce bloc, un moteur ne saurait pas laquelle des deux pages servir à un utilisateur anglophone.

3. JSON-LD pour chaque type d’entité

Les données structurées sont encore sous-utilisées par les devs et lues par toutes les IA qui crawlent le web. J’émets trois schémas sur chaque article : Person, BlogPosting, BreadcrumbList.

// src/utils/schema.ts (extrait)
export function blogPostingSchema(p: BlogPostingInput) {
  const lang = p.lang ?? 'fr';
  const url = `${SITE.url}${localizedPath(lang, `/blog/${p.slug}`)}`;
  return {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    '@id': `${url}#article`,
    mainEntityOfPage: { '@type': 'WebPage', '@id': url },
    url,
    headline: p.title,
    description: p.description,
    datePublished: p.publishedAt,
    dateModified: p.updatedAt ?? p.publishedAt,
    inLanguage: LANG_META[lang].bcp47,
    articleSection: p.category,
    keywords: p.keywords.join(', '),
    timeRequired: p.readingTime ? `PT${p.readingTime}M` : undefined,
    author: { '@id': `${SITE.url}/#person` },
    publisher: { '@id': `${SITE.url}/#person` },
  };
}

Le même Person est référencé partout via @id, pas dupliqué. C’est ce que Google veut : un graphe, pas un tas de JSON dispersés.

4. Sitemap et robots.txt IA-friendly

Deux intégrations Astro font 95 % du travail. Le sitemap est généré au build avec les locales déclarées ; robots.txt autorise explicitement les crawlers IA que je veux voir citer mon contenu.

// astro.config.mjs (extrait)
integrations: [
  sitemap({
    i18n: { defaultLocale: 'fr', locales: { fr: 'fr-FR', en: 'en-GB' } },
    changefreq: 'weekly',
    priority: 0.7,
  }),
  robotsTxt({
    sitemap: [`${SITE.url}/sitemap-index.xml`],
    policy: [
      { userAgent: 'GPTBot', allow: '/' },
      { userAgent: 'ClaudeBot', allow: '/' },
      { userAgent: 'PerplexityBot', allow: '/' },
      { userAgent: 'Google-Extended', allow: '/' },
      { userAgent: 'Bytespider', disallow: '/' },
      { userAgent: '*', allow: '/', disallow: ['/404'] },
    ],
  }),
],

Bloquer Bytespider n’est pas de l’idéologie : c’est un crawler bruyant qui consomme de la bande passante sans citer ses sources. Ouvrir GPTBot, ClaudeBot et PerplexityBot, au contraire, c’est accepter d’être cité par les assistants que mes prospects utilisent déjà.

5. Un llms.txt pour l’ère des LLM

Personne ne sait encore si le standard llms.txt va s’imposer, mais il coûte presque rien et résout un vrai problème : un LLM qui crawle mon site a besoin d’un plan éditorial, pas d’un sitemap XML.

// src/pages/llms.txt.ts (extrait — FR à la racine, defaultLocale sans préfixe)
export const GET: APIRoute = async () => {
  const posts = (await getCollection('blog', (entry) => isPublished(entry, 'fr'))).sort((a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime());

  const lines: string[] = [`# ${SITE.name}`, '', '## Articles', ''];
  for (const post of posts) {
    lines.push(`- [${post.data.title}](${SITE.url}/blog/${post.id}/) ` + `(${toISODate(post.data.publishedAt)}, ${post.data.category}) : ${post.data.excerpt}`);
  }
  return new Response(lines.join('\n'), {
    headers: { 'Content-Type': 'text/plain; charset=utf-8' },
  });
};

Les excerpts stricts du schéma Zod (point 1) deviennent ici des résumés utilisables par un modèle sans qu’il ait à charger chaque article. En complément, src/pages/llms-full.txt.ts génère le corpus markdown complet des articles — c’est le standard que pousse Jeremy Howard (auteur originel de llms.txt) pour que les modèles puissent raisonner sur l’intégralité du contenu sans crawler page par page. Coût marginal au build, gain potentiel si l’un de ces standards finit par s’imposer côté indexation IA.

6. Statique, léger, rapide

Les cinq premiers points ne valent rien si la page met 4 secondes à s’afficher. Astro sort du HTML pur, Tailwind v4 génère le CSS utilisé, et trois options dans astro.config.mjs achèvent le travail :

build: { inlineStylesheets: 'auto', format: 'file' },
prefetch: { prefetchAll: false, defaultStrategy: 'hover' },
trailingSlash: 'never',

Le inlineStylesheets: 'auto' embarque le CSS critique dans le HTML initial — zéro aller-retour pour le premier paint. Le prefetch au hover précharge la page au moment où la souris entre sur un lien. trailingSlash: 'never' évite les redirections 301 entre /blog/ et /blog qui plombent les scores PageSpeed.

Ce que je ferais différemment

  • Commencer par le schéma, pas par le design. Un champ keywords oublié au départ se paie en migration plus tard.
  • Écrire la page /llms.txt dès le jour 1. C’est cinq lignes de code et ça force à avoir des excerpts propres partout.
  • Mesurer le Lighthouse sur mobile à chaque commit. J’ai un pa11yci et un lighthouserc.json dans le repo ; je les aurais posés plus tôt.

Le code est ouvert

Le repo complet est public : github.com/gabrielmustiere/mustiere.fr. Si vous voulez cloner la structure pour votre propre site de consultant, servez-vous — le schéma, les utils SEO et le squelette i18n sont directement réutilisables.

§ faq

Questions fréquentes

Pourquoi Astro plutôt que Next.js, Hugo ou un générateur statique classique ?
Pour un site de contenu qui n'a presque jamais besoin de JS interactif, Astro touche le point d'équilibre : zéro JS par défaut (rien à hydrater, rien pour casser les Core Web Vitals), des content collections typées et validées par Zod qui font échouer le build sur un champ manquant, et un i18n natif dans la config. Next.js est surdimensionné quand on n'a pas besoin d'un runtime React, Hugo build plus vite mais son templating devient à l'étroit au-delà de quelques collections, et un SSG maison oblige à réécrire sitemap, hreflang et pipeline d'images soi-même. Sur un portfolio + blog, Astro fait gagner le plus de temps sans sacrifier la rigueur SEO.
Peut-on vraiment confier design et implémentation à Claude de bout en bout ?
Pas "confier" — collaborer. Claude Design a produit les maquettes HTML dans une boucle serrée où je décrivais l'intention éditoriale ("sobre, typographie mixte serif/sans/mono", "vire le panneau Tweaks") et je relisais chaque itération. Claude Code a ensuite transformé les maquettes en composants Astro idiomatiques, mais je travaille en plan-then-execute : je demande d'abord un découpage en sous-tâches, je le relis, et seulement ensuite je laisse l'agent dérouler. Le levier est réel (deux week-ends pour sortir tout le site), mais il faut savoir ce qu'on veut et lire chaque diff. C'est un binôme sénior, pas un pilote automatique.
Le dispositif SEO n'est-il pas surdimensionné pour un portfolio de CTO freelance ?
Les six briques (schéma Zod, hreflang, JSON-LD, sitemap, llms.txt, livraison statique rapide) ne sont pas du "surdimensionné" — c'est le plancher. Chacune répond à un mode de panne concret : un `excerpt` manquant qui casse silencieusement les meta descriptions, une page FR/EN dupliquée que Google enterre, un article invisible aux citations de ChatGPT/Perplexity. Coût total : quelques heures, et la majorité est réutilisable sur n'importe quel projet futur. Ce qui est surdimensionné, c'est le bourrage de mots-clés dans les textes, l'achat de liens, ou la course au 100/100 Lighthouse au-delà de 95 — aucun de ces leviers ne bouge l'aiguille sur un site de CTO.
§ fin Publié le · Mis à jour le