Skip to content

Repository files navigation

CV — Antoine Berthaud

Site CV statique en HTML/CSS/JS vanilla. Aucun build nécessaire pour le site lui-même : tu peux l'ouvrir en local ou le déployer tel quel sur GitHub Pages. Une seule brique optionnelle a besoin de Node.js : la génération automatique des PDF (section 5).

Arborescence

index.html               → structure de la page ; les zones entre marqueurs static: sont pré-rendues par la CI (voir section 5)
css/style.css             → tous les styles (tokens de couleur en haut du fichier)
js/i18n.js                  → dictionnaire des textes fixes de l'interface (FR/EN) + logique de bascule de langue
js/data.js                 → ⭐ LE FICHIER À ÉDITER : ton contenu (profil, expériences, compétences, side projects)
js/app.js                  → génère le HTML à partir de data.js, gère les durées, focus mode, impression, langue, rail chronologique (desktop)
js/gemini.js                → logique du Fit-Checker (appelle la fonction Supabase, jamais Gemini directement)
results.html + js/results.js → page « Résultats » : les études de cas (format STAR) derrière les chiffres du hero (zone pré-rendue aussi)
project-detail.html + js/project-detail.js → page gabarit des études de cas de side projects (?slug=), pré-rendue avec le seul side project
supabase/functions/gemini-fit/index.ts  → code de la fonction serveur à déployer sur Supabase
scripts/generate-static.js   → pré-rend les trois pages en français dans leurs zones static: (voir section 5)
scripts/generate-pdf.js      → génère les PDF FR/EN à partir du site (voir section 5)
scripts/lib/site-server.js   → mini-serveur local partagé par les deux scripts
package.json + package-lock.json → dépendances des scripts, versions figées (voir section 5)
CLAUDE.md                    → contexte et décisions pour Claude Code (exclu du site publié)
.github/workflows/generate-pdf.yml → régénère les pages pré-rendues et les PDF à chaque changement, et une fois par mois (GitHub Actions)
assets/cv-antoine-berthaud-fr.pdf, -en.pdf, -fr-court.pdf, -en-short.pdf → PDF générés, complets et courts (ne pas éditer à la main, ils sont régénérés à chaque fois)
assets/photo/               → dépose ta photo ici
assets/logos/               → dépose les logos des entreprises ici (WebP ou PNG 224×224, fond transparent de préférence)
robots.txt / sitemap.xml     → référencement (voir section 10)
CNAME                        → domaine personnalisé pour GitHub Pages (cv.antoine.berthaud.me)
_config.yml                  → exclut du site publié les fichiers de travail (README, CLAUDE.md, scripts…)
404.html                     → page « introuvable » servie par GitHub Pages ; FR et EN dans le HTML, le bloc affiché suit la langue du navigateur (seule détection navigateur du site, voir CLAUDE.md)
.github/workflows/pr-checks.yml → rejoue le pré-rendu et la génération des PDF sur chaque PR, sans commit (garde-fou avant merge)
.github/workflows/keepalive.yml + surveiller-fit-checker.yml → continuité du Fit-Checker (voir section 12)
supabase/migrations/         → la table keepalive lue chaque jour pour garder le projet Supabase actif

0. Le site est bilingue FR/EN

Un bouton FR/EN dans le menu bascule toute l'interface, y compris le résultat du Fit-Checker (Gemini reçoit une instruction pour répondre dans la langue affichée).

  • Les textes fixes de l'interface (menu, boutons, libellés...) sont dans js/i18n.js, sous forme de dictionnaire UI_STRINGS.

  • Le contenu (js/data.js) utilise le même principe partout : un champ traduisible s'écrit { fr: "...", en: "..." }. Un champ laissé en texte simple (ex: "AB Tasty", une date, un id) s'affiche à l'identique dans les deux langues — pas besoin de dupliquer les noms propres.

    // Traduit :
    pitch: { fr: "Product Manager avec...", en: "Product Manager with..." }
    
    // Pas besoin de traduire (identique dans les deux langues) :
    company: "AB Tasty"

    Si tu ajoutes une expérience ou modifies un texte, respecte ce même format pour que la bascule de langue continue de fonctionner. Si tu ne fournis qu'une seule langue sur un champ qui devrait être bilingue, ce texte s'affichera tel quel des deux côtés — ce n'est pas une erreur bloquante, juste un oubli de traduction à combler.

1. Remplir ton contenu

Ouvre js/data.js : tout ce qui est marqué [À REMPLACER] ou EXEMPLE doit être remplacé. C'est le seul fichier que tu dois éditer pour le contenu.

  • Photo : dépose le fichier dans assets/photo/, puis renseigne PROFILE.photo = "assets/photo/tonfichier.jpg". Si tu laisses vide, un avatar avec tes initiales s'affiche à la place.
  • Logos d'entreprise : même principe avec assets/logos/ et le champ logo de chaque entreprise dans EXPERIENCES. Vide = pastille avec les 2 premières lettres du nom de l'entreprise, générée automatiquement.
  • Plusieurs rôles chez une même entreprise : ajoute plusieurs objets dans le tableau roles de cette entreprise, du plus RÉCENT au plus ANCIEN (celui du haut = poste actuel ou le plus récent). Le site calcule automatiquement la durée de chaque rôle et la durée totale chez l'entreprise (de la date de début la plus ancienne à la date de fin la plus récente parmi tous les rôles).
  • Contenu d'un rôle : chaque rôle se décompose en 4 blocs pour rester lisible (fini le pavé de texte unique) :
    • context — 1 à 2 phrases de mise en situation (produit, marché, périmètre)
    • achievements — tableau de réalisations concrètes, idéalement chiffrées, affichées en liste à puces
    • methodology (optionnel) — méthodo utilisée (ex: "Scrum, OKR"). Omets le champ si tu ne veux rien afficher.
    • team (optionnel) — taille/composition de l'équipe (ex: { fr: "4 développeurs, 1 QA", en: "4 developers, 1 QA" }). Idem, omets si non pertinent.
  • Compétences/outils : la liste maîtresse est dans SKILLS. Chaque rôle et chaque side project référence des compétences par leur id. Ajoute/retire librement des entrées.
  • Side projects : mets CONFIG.showSideProjects = false en haut de data.js pour masquer entièrement la section (et le lien de menu) sans supprimer le contenu. Actuellement activé, avec Tour de Growth et sa page de détail (section 11).
  • Disponibilité : PROFILE.contact.availability = { fr, en } s'affiche sous le rôle dans le pied de page (ex. « Je réponds sous 48 h. »). Vide par défaut : n'écris que ce que tu tiendras.
  • Débuts repliés : CONFIG.collapseRolesEndingBefore = "2016-08" replie sur le site les rôles terminés à cette date ou avant (QA 2015-2016, ESN 2011-2015) derrière un bouton « Voir mes débuts (2011 – 2016) → ». Ils restent dans la page (indexables), se déroulent au clic, et le PDF les montre toujours. Mets null pour tout afficher. Sur téléphone, les tags par rôle sont masqués : ils répètent la section Compétences.

Compétences : cinq groupes de pratiques (SKILLS dans data.js, ordre d'apparition = ordre d'affichage) plus une ligne « Outils du quotidien » (EVERYDAY_TOOLS), en texte et non en pastilles. Les pastilles ne filtrent plus les expériences depuis le 7 septembre 2026 : la section est passée sous les expériences, où filtrer ce qu'on vient de lire n'avait plus de sens. Chaque rôle porte au plus six compétences (skills), affichées sur ordinateur et dans le PDF complet.

Français par défaut, anglais sur ?lang=en : l'URL nue (https://cv.antoine.berthaud.me/) affiche toujours le français ; ajoute ?lang=en pour ouvrir directement la version anglaise — c'est ce lien qu'il faut partager à un recruteur anglophone. Le bouton FR/EN met aussi l'URL à jour quand on clique dessus (?lang=en en anglais, URL nue en français), donc l'URL affichée reste copiable telle quelle.

Pas de détection de la langue du navigateur, et c'est volontaire : Googlebot rend la page avec un navigateur en anglais, et indexait donc la version anglaise à l'URL canonique — à contre-sens d'un CV qui vise des requêtes françaises (voir section 10). Un anglophone qui tombe sur le lien brut voit le français, avec le bouton EN bien visible dans le menu.

Menu qui suit le scroll : le lien correspondant à la section visible à l'écran est automatiquement surligné dans le menu (soulignement animé), que ce soit en scrollant ou en cliquant sur un lien du menu. C'est géré tout seul, rien à configurer.

2. Sécuriser l'appel à Gemini (Fit-Checker)

Le principe : ta clé Gemini ne doit jamais apparaître dans le code du site (visible par n'importe qui via l'inspecteur du navigateur). Elle vit uniquement côté serveur, dans une fonction Supabase.

Étape A — Obtenir une clé API Gemini

  1. Va sur https://aistudio.google.com/apikey
  2. Connecte-toi avec un compte Google
  3. Clique sur "Create API key"
  4. Copie la clé générée — garde-la de côté, tu vas la mettre dans Supabase (jamais dans le code du site)

Étape B — Créer un projet Supabase (gratuit)

  1. Va sur https://supabase.com → crée un compte → New project
  2. Une fois créé, note dans Project Settings → API :
    • l'URL du projet
    • la clé anon / public

Étape C — Déployer la fonction qui appelle Gemini

Le code est déjà prêt dans supabase/functions/gemini-fit/index.ts. Il te reste à le déployer :

⚠️ Piège classique à éviter : il y a deux fichiers qui parlent de Gemini dans ce projet, et ils ne vont pas au même endroit :

  • js/gemini.js → reste sur le site (il tourne dans le navigateur)
  • supabase/functions/gemini-fit/index.ts → part sur Supabase (c'est lui qui doit être collé dans l'éditeur de fonction Supabase, ou déployé via la CLI ci-dessous)

Si tu colles le mauvais fichier dans Supabase, tu obtiens une erreur du type Cannot destructure property 't' of 'window.i18n' as it is undefined — c'est le signe que js/gemini.js (qui a besoin du navigateur) a été déployé à la place de index.ts.

# Installer la CLI Supabase (une seule fois)
npm install -g supabase

# Te connecter et lier le projet
supabase login
supabase link --project-ref TON_PROJECT_REF

# Stocker ta clé Gemini en secret (jamais dans le code)
supabase secrets set GEMINI_API_KEY=colle_ta_cle_ici

# Déployer la fonction
supabase functions deploy gemini-fit --no-verify-jwt

--no-verify-jwt rend la fonction appelable sans compte utilisateur, ce qui est nécessaire ici puisque n'importe quel recruteur doit pouvoir l'utiliser sans se connecter. Pour éviter les abus (quelqu'un qui spam ta fonction et consomme ton quota Gemini), tu peux activer le rate limiting dans Supabase (Project Settings → API → Rate Limits), ce qui est recommandé mais optionnel pour démarrer.

ALLOWED_ORIGIN dans index.ts vaut déjà https://cv.antoine.berthaud.me : seul le site peut appeler la fonction depuis un navigateur.

Les modèles utilisés sont listés dans GEMINI_MODEL_CANDIDATES en haut du fichier, essayés dans l'ordre (le dernier, gemini-flash-latest, est un alias maintenu par Google qui ne casse jamais). Si tu obtiens une erreur côté Gemini malgré tout, va voir la liste à jour sur https://ai.google.dev/gemini-api/docs/models et ajuste cette liste. Avant tout redéploiement depuis le repo, compare avec la version déployée (le dashboard Supabase ou l'outil MCP get_edge_function) : un réglage changé directement en prod serait écrasé.

Étape D — Connecter le site à la fonction

✅ Déjà fait dans js/data.js avec les identifiants que tu m'as donnés :

supabaseFunctionUrl: "https://tpreesulucfsyalaipcj.supabase.co/functions/v1/gemini-fit",
supabaseAnonKey: "eyJhbGc...", // clé "anon public" — conçue par Supabase pour être exposée côté client, ce n'est pas un secret

Le bouton "Analyser le fit" reste désactivé (avec un message clair) tant que supabaseFunctionUrl est vide — donc rien ne casse si tu déploies le site avant d'avoir fini cette partie.

Étape E — Rate limiting avec Upstash (optionnel mais recommandé)

Une fois le lien public partagé à des recruteurs, n'importe qui peut aussi spammer ton endpoint et consommer ton quota Gemini. Le code est déjà prêt dans index.ts (basé sur l'exemple officiel Supabase) : 3 analyses par minute et par adresse IP. Il ne fait rien tant que tu n'as pas configuré Upstash — donc pas d'urgence à le faire avant de déployer.

  1. Crée un compte gratuit sur https://upstash.com, puis une base Redis (type "Global" pour minimiser la latence).
  2. Dans l'onglet REST API de ta base, copie UPSTASH_REDIS_REST_URL et UPSTASH_REDIS_REST_TOKEN.
  3. Stocke-les en secrets Supabase, comme pour la clé Gemini :
    supabase secrets set UPSTASH_REDIS_REST_URL=colle_l_url_ici
    supabase secrets set UPSTASH_REDIS_REST_TOKEN=colle_le_token_ici
  4. Redéploie la fonction : supabase functions deploy gemini-fit --no-verify-jwt.

Si un recruteur dépasse la limite, il voit un message clair (« Trop de tentatives depuis cet appareil. Réessayez dans une minute. ») plutôt qu'une erreur brute. Pour changer le seuil (3/minute, identique dans le repo et en prod depuis le 3 septembre 2026), ajuste Ratelimit.slidingWindow(3, "60 s") dans index.ts.

3. Tester en local

Comme le site charge plusieurs fichiers JS séparés, ouvrir index.html directement en double-cliquant dessus ne fonctionne pas toujours (le navigateur bloque le chargement des fichiers via file://, en particulier sur Chrome). Le plus fiable est de lancer un petit serveur local.

macOS / Linux

# Depuis le dossier du projet
python3 -m http.server 8000
# puis ouvre http://localhost:8000

Windows

La commande diffère légèrement selon ce que tu as installé.

Si Python est installé (vérifie avec python --version dans PowerShell ou l'invite de commandes — sur Windows c'est python, pas python3) :

cd chemin\vers\le\dossier\du\projet
python -m http.server 8000

Puis ouvre http://localhost:8000 dans ton navigateur.

Si tu n'as pas Python mais que tu as Node.js (utile de toute façon si tu déploies la fonction Supabase avec npm) :

cd chemin\vers\le\dossier\du\projet
npx serve .

Le terminal affiche l'URL à ouvrir (en général http://localhost:3000).

Si tu n'as ni l'un ni l'autre : installe Python depuis https://python.org (coche bien "Add Python to PATH" pendant l'installation) ou Node.js depuis https://nodejs.org, puis utilise une des deux méthodes ci-dessus. Double-cliquer sur index.html peut fonctionner selon le navigateur, mais si la page reste blanche ou que rien ne s'affiche, c'est le signe qu'il faut passer par un serveur local.

4. Déployer sur GitHub Pages avec ton domaine personnalisé (cv.antoine.berthaud.me)

  1. Crée un repo GitHub (public), pousse tout ce dossier dedans — le fichier CNAME à la racine (contenant cv.antoine.berthaud.me) est déjà prêt, ne le supprime pas.
  2. Dans le repo : Settings → Pages → Source : Deploy from a branch, branche main, dossier / (root).
  3. Toujours dans Settings → Pages, section Custom domain : renseigne cv.antoine.berthaud.me et valide (GitHub va lire le fichier CNAME automatiquement, ce champ devrait déjà le proposer).

Configuration DNS (chez ton registrar/hébergeur de domaine)

Comme cv.antoine.berthaud.me est un sous-domaine, tu ajoutes un enregistrement CNAME (pas un enregistrement A) dans la zone DNS de antoine.berthaud.me (ou berthaud.me selon où ta zone est gérée) :

Type Nom (host) Valeur (target)
CNAME cv TON_PSEUDO.github.io.

Remplace TON_PSEUDO par ton nom d'utilisateur GitHub. Le point final après .github.io. est parfois requis selon le registrar (certains l'ajoutent automatiquement) — regarde comment tes autres enregistrements CNAME existants sont formatés si tu en as.

Selon l'interface de ton registrar, le champ "Nom/Host" peut attendre soit juste cv, soit cv.antoine.berthaud.me en entier — s'il te demande le nom complet et râle sur un sous-sous-domaine, essaie les deux formats.

Vérification

  • La propagation DNS prend de quelques minutes à 24h.
  • Une fois propagé, GitHub Pages émet automatiquement un certificat HTTPS (Let's Encrypt) pour ton domaine — coche "Enforce HTTPS" dans Settings → Pages dès que l'option devient disponible (grisée tant que le certificat n'est pas prêt).
  • Teste avec dig cv.antoine.berthaud.me CNAME (ou https://dnschecker.org) si le site ne répond pas après plusieurs heures.

Alternative si tu changes d'avis sur GitHub Pages : Netlify ou Cloudflare Pages gèrent aussi les domaines personnalisés, souvent avec une configuration DNS plus simple (Cloudflare en particulier, si ton domaine y est déjà géré).

5. Pages pré-rendues, PDF téléchargeable et Focus Lecture

Pages pré-rendues (ce que lisent les robots sans JavaScript)

Le contenu vit dans js/data.js et c'est le JavaScript qui construit la page. Google exécute ce JavaScript, mais les robots des moteurs de réponse IA (ChatGPT, Perplexity, Claude…), les aperçus de lien et une partie des outils de tri de candidatures ne le font pas : sans pré-rendu, ils voyaient 258 mots sur 1 622 (le pitch, trois chiffres, le pied de page).

scripts/generate-static.js ouvre les trois pages dans Chromium, en français, laisse le JavaScript les rendre, puis recopie le HTML rendu dans les fichiers sources entre des marqueurs <!-- static:ID --><!-- /static:ID --> (une vingtaine de zones sur index.html : pastilles, pitch, chiffres, piliers, compétences, expériences, formation, témoignage, side projects, pied de page ; tout le <main> sur results.html et project-detail.html ; plus <title>, description et balises og:/twitter: de chaque page). Tout ce qui est entre deux marqueurs est généré : ne l'édite jamais à la main, la prochaine génération l'écraserait. Tu modifies data.js (ou i18n.js), tu push, la CI régénère et recommit les pages — exactement comme les PDF, dans le même workflow. Au chargement, app.js re-rend par-dessus (même HTML en français, version anglaise sur ?lang=en, où la page reste invisible le temps du rendu anglais pour éviter un flash de français). Le Fit-Checker et la bascule de langue exigent toujours JavaScript.

Garde-fous du script : erreur JavaScript, zone vide, marqueur manquant, page trop courte, mots attendus absents (Everysens, Polytech, le nom du témoignage…), et refus de tourner si PROJECT_DETAILS a plus d'une entrée (la page gabarit ne peut porter qu'un seul pré-rendu ; à ce moment-là il faudra décider d'une page par projet). node scripts/generate-static.js --check dit si les fichiers sont à jour sans rien écrire.

PDF téléchargeable

Deux PDF par langue. Depuis le 7 septembre 2026, le bouton « PDF » du menu sert le court (2 pages) : c'est le document qu'un recruteur ouvre et transmet. Le segment « complet » sert le complet (5 pages), le dossier qui reprend tout le site.

Le court garde le hero, les compétences en lignes, les expériences avec au plus trois puces (deux pour les rôles d'avant 2022), les débuts réduits à leur titre et la formation sur une rangée — sans piliers, études de cas, témoignage, side projects ni contexte de rôle. C'est la même page, imprimée avec ?pdf=court (classe body.cv-court, règles dans le @media print de style.css) : rien à entretenir en double, tu édites data.js et les quatre PDF suivent. Le générateur refuse un court de plus de 2 pages : si ça arrive après un ajout de contenu, resserrer les règles body.cv-court plutôt que d'accepter une 3e page. Les courts ne sont pas dans le sitemap (voulu : ils concurrenceraient les complets sur ton nom).

Pagination : le complet tient en 5 pages parce que deux règles de saut de page ont été mesurées et arbitrées le 7 septembre 2026. break-after:avoid sur les titres de section coûtait une page entière (Chromium déplace le titre et toute la section quand elle ne rentre pas) : retiré, au prix d'un titre parfois seul en bas de page. break-inside:avoid sur les cartes coûtait aussi une page mais a été gardé, une carte coupée en deux étant pire. Avant de toucher à ces règles, régénérer et compter les pages.

Le bouton "Télécharger PDF" ne fait plus un simple Ctrl/Cmd+P navigateur (rendu peu maîtrisé, dépendant des réglages de chacun). Il télécharge un vrai PDF pré-généré, produit par Chromium piloté en script (Playwright) directement à partir du site : mêmes couleurs, mêmes polices, mise en page adaptée au format papier (A4 pour le français, Letter pour l'anglais). Si le PDF n'existe pas encore (avant la première génération), le bouton retombe automatiquement sur l'impression navigateur classique — rien ne peut casser.

Comment ça marche

  1. scripts/generate-pdf.js lance un mini-serveur local, ouvre le site dans Chromium, et exporte quatre fichiers : assets/cv-antoine-berthaud-fr.pdf et -en.pdf (complets), -fr-court.pdf et -en-short.pdf (courts).
  2. Le CSS @media print (dans style.css) définit un rendu pensé spécifiquement pour le papier : les couleurs de marque sont conservées (bordures des piliers, dégradés d'avatar), les ombres portées sont retirées (elles ne rendent pas bien sur un support figé), et les sections interactives (navigation, Fit-Checker, bandeau teaser) sont masquées.
  3. .github/workflows/generate-pdf.yml relance le pré-rendu puis cette génération automatiquement à chaque git push sur main qui touche le contenu ou le style, et une fois par mois (le 1er à 4h UTC) pour rafraîchir les durées d'expérience calculées jusqu'à « aujourd'hui », puis recommit les pages et les PDF à jour (avec leur lastmod dans sitemap.xml) — tu n'as normalement jamais besoin de lancer ces scripts toi-même. S'il échoue, il ouvre une issue GitHub (label pdf-generation-failure). Les mêmes scripts tournent sur chaque PR via pr-checks.yml, sans commit, avec leurs garde-fous (erreur JS, page vide, polices absentes, lien local, PDF trop court). Le problème "CV à jour" est réglé une fois pour toutes : tu édites data.js, tu push, les pages et les PDF suivent.

Régénérer en local (pour prévisualiser un changement avant de push)

macOS / Linux :

npm install
npx playwright install --with-deps chromium
npm run generate          # pré-rendu puis PDF ; ou generate-static / generate-pdf séparément

Windows (PowerShell) :

npm install
npx playwright install chromium
npm run generate

(--with-deps installe des paquets système Linux et n'a pas d'équivalent nécessaire sous Windows — Playwright embarque tout ce qu'il faut pour Chromium.)

Les PDF sont écrits dans assets/, les pages pré-rendues dans les trois fichiers HTML. Ouvre-les pour vérifier avant de commiter, comme pour n'importe quel changement visuel.

Point d'attention pour le déploiement automatique

Le workflow a besoin d'écrire sur ton dépôt (pour committer les pages et les PDF régénérés) : la permission contents: write est déjà configurée dans le fichier, mais vérifie que Settings → Actions → General → Workflow permissions de ton repo autorise bien "Read and write permissions" (c'est le réglage par défaut sur les nouveaux dépôts, mais certains comptes/organisations le restreignent). Si ta branche principale s'appelle master plutôt que main, ajuste la ligne branches: [main] dans .github/workflows/generate-pdf.yml.

Focus Lecture

Le bouton "Focus lecture" ajoute une classe focus-mode sur <body> qui neutralise les dégradés/ombres pour une lecture plus sobre à l'écran. L'état n'est pas mémorisé entre deux visites (pas de localStorage) — dis-moi si tu veux que je l'ajoute, c'est une modification mineure.

Quand tu montes la version de Playwright dans package-lock.json (Chromium change avec elle), regarde les deux PDF régénérés par le check de la PR avant de merger : nombre de pages FR/EN et polices. C'est exactement le genre de changement qui repagine silencieusement.

6. Mettre en avant des mots ou chiffres (gras)

Dans js/data.js, entoure un mot, un chiffre ou une phrase de **deux astérisques** pour l'afficher en gras — ça marche dans pitch, context et chaque ligne d'achievements :

achievements: [
  { fr: "Refonte de l'onboarding : **-20% de Time-to-Value, +15% de taux d'activation**.",
    en: "Reworked onboarding: **-20% Time-to-Value, +15% activation rate**." },
]

Je l'ai déjà appliqué sur tes expériences les plus récentes (AB Tasty, Everysens, SNCF — Prêt à Voyager) : chiffres clés (-20%, +15%, 100%...) et 2-3 formulations qui méritent de ressortir à la lecture rapide (modèle hybride, tunnel d'acquisition autonome...). Je suis resté volontairement sobre — pas de gras sur les rôles juniors/anciens, pour ne pas noyer l'effet. Libre à toi d'ajuster : ajoute **...** où tu veux dans n'importe quel context/achievement/pitch, retire-en si tu trouves que j'en ai trop mis.

7. Favicon et aperçu de partage (Open Graph)

  • Favicon : icône violette avec tes initiales "AB", déjà en place (assets/favicon.svg + fallbacks PNG pour les navigateurs/appareils plus anciens). Rien à faire.
  • Image de partage (ce qui s'affiche quand tu partages le lien du CV sur LinkedIn, Slack, etc.) : déjà générée dans assets/og-image.png à partir de ta photo et de tes infos actuelles.
  • ⚠️ À faire une fois déployé : dans index.html, remplace les deux occurrences de https://REMPLACE-PAR-TON-DOMAINE.example (balises og:image, og:url, twitter:image) par ta vraie URL, ex. https://tonpseudo.github.io/cv. Sans ça, l'aperçu ne s'affichera pas correctement quand tu partageras le lien.
  • Si tu changes significativement ton nom/rôle/photo plus tard, dis-le-moi et je régénère og-image.png en conséquence (c'est une image statique, pas générée dynamiquement).
  • Ces balises restent toujours en français : les robots des réseaux sociaux n'exécutent pas le JavaScript, donc ils ne voient jamais la bascule EN. C'est normal et sans conséquence pour le site lui-même.

8. Analytics respectueux de la vie privée (GoatCounter)

Pas de cookies, pas de données personnelles collectées, pas de bannière de consentement nécessaire. Gratuit indéfiniment pour un usage personnel (contrairement à Plausible, qui n'a plus de forfait gratuit permanent depuis 2026 — je suis parti sur GoatCounter pour cette raison).

Compte configuré : site antoineberthaud, tableau de bord sur https://antoineberthaud.goatcounter.com. Le script count.js est chargé dans le <head> des trois pages (index.html, results.html, project-detail.html), précédé d'un petit script inline qui fige le chemin compté : seuls ?slug= et ?lang=en sont conservés (/, /?lang=en, /results.html, /project-detail.html?slug=tour-de-growth…), jamais ref/utm ni le fragment #…. Sans ça, /?lang=en remontait tantôt avec, tantôt sans son paramètre selon la vitesse du réseau.

Événements suivis en plus des visites (attribut data-goatcounter-click, ou goatcounter.count() dans gemini.js) :

Événement Ce qu'il compte
pdf-download clic sur le bouton « PDF » du menu (CV court) ou sur un lien court du pied de page
pdf-download-complet clic sur le segment « complet » du menu ou sur un lien complet du pied de page
contact-email, contact-linkedin, link-photos clics sur les liens du pied de page
hero-stat-<resultId>, hero-stats-link clics sur un chiffre du hero ou sur « Voir les études de cas »
project-tour-de-growth clic vers Tour de Growth (carte du CV ou bouton de l'étude de cas)
fit-checker-used analyse Fit-Checker réussie
fit-checker-error, fit-checker-rate-limited analyse en échec (panne, délai) ou bloquée par le rate limit

Dans GoatCounter, ces événements apparaissent dans la liste « Pages » du tableau de bord, marqués d'un exposant event — pas dans « Campaigns ». Les liens recréés par le JS (pied de page, chiffres du hero, cartes) sont reliés par bindAnalytics() (app.js) et son équivalent dans project-detail.js après chaque rendu.

Exclure tes propres visites : ouvre une fois https://cv.antoine.berthaud.me/#toggle-goatcounter sur chaque navigateur et appareil que tu utilises (ordinateur, téléphone) — un message confirme que les visites de ce navigateur ne sont plus comptées (réglage stocké en localStorage, à refaire si tu vides les données du site). Même URL pour réactiver.

Savoir d'où viennent les visites : ajoute ?utm_campaign=linkedin (ou un autre nom : ?utm_campaign=candidature-nomdelaboite) aux liens que tu partages — GoatCounter les affiche dans « Campaigns ». Jamais dans le sitemap, un canonical ou un lien du site lui-même.

8b. Rail chronologique (ordinateur seulement)

Pendant la lecture des expériences, un rail fixe dans la marge gauche montre tout le parcours d'un coup d'œil : une ligne proportionnelle aux durées (24 px par an), un logo par entreprise, le nom et les années, et un point qui suit la position de lecture. Un clic amène à l'entreprise (et déplie les débuts si besoin). Il n'existe qu'à partir de 1 300 px de large (logos seuls) et 1 480 px (noms et années), pour ne jamais empiéter sur les blocs ; jamais sur téléphone, dans le PDF ni pour les lecteurs d'écran. Un nom trop long pour le rail se raccourcit avec shortName dans l'entrée de l'entreprise (data.js).

9. Scorecard chiffré dans le Hero

Trois métriques clés (HERO_STATS dans data.js) affichées sous le pitch, pour donner un aperçu de l'impact avant même de lire les expériences. Modifie les valeurs/labels directement dans data.js, ou vide le tableau (HERO_STATS = []) pour le masquer entièrement — même logique que SIDE_PROJECTS/TESTIMONIALS.

10. SEO — être trouvé sur ton nom, et sur « product manager Nantes »

Soyons lucides sur l'objectif : sur une requête générique comme « product manager Nantes », la page de résultats appartient aux job boards (Indeed, HelloWork, Glassdoor…), et les recruteurs qui cherchent des candidats le font dans LinkedIn Recruiter, pas dans Google. Ce qui est atteignable, et ce que le site est équipé pour faire :

  1. sortir premier sur ton nom et ses variantes (« Antoine Berthaud product manager ») ;
  2. capter la longue traîne localisée (« senior growth product manager Nantes », « product manager PLG SaaS Nantes ») ;
  3. être lu par les moteurs IA (ChatGPT, Perplexity, Claude…), qui citent volontiers les sites perso mais n'exécutent souvent pas le JavaScript — d'où les pages pré-rendues (section 5).

Ce qui est en place dans le code :

  • Français par défaut, anglais sur ?lang=en, sans détection de la langue du navigateur (voir section 0) : Googlebot navigue en anglais et indexait la version anglaise à l'URL canonique. Balises hreflang fr/en dans le <head> et dans le sitemap ; le canonical suit la langue affichée (/ en FR, /?lang=en en EN). Seule exception : 404.html, qui n'est pas indexée et n'a aucun autre signal de langue, garde le bloc FR ou EN d'après la langue du navigateur (les deux restent dans le HTML sans JavaScript).
  • <title>, <meta description>, Open Graph et eyebrow du hero qui citent Nantes, le métier et le secteur — textes dans PROFILE.seo (js/data.js), posés dans le <head> de index.html par le pré-rendu (section 5), comme tout le contenu de la page. app.js prévient dans la console du navigateur si le pitch pré-rendu diverge de PROFILE.pitch.fr (page pas encore régénérée).
  • Données structurées ProfilePagePerson (JSON-LD dans index.html) : métier, lieu (Nantes), formation, sujets maîtrisés, et sameAs vers LinkedIn, le portfolio photo et Tour de Growth, pour que Google relie le tout à la même personne.
  • sitemap.xml avec toutes les pages indexables (accueil FR et EN, Résultats, étude de cas) et une date lastmod à mettre à jour quand une page change ; les deux PDF y figurent aussi (décision du 06/09/2026 : ils sont déjà liés depuis les pages, autant que Google les compte), avec un lastmod mis à jour automatiquement par generate-pdf.yml à chaque régénération ; robots.txt ; CNAME.
  • Pages secondaires : results.html et project-detail.html?slug=… ont leur propre <title>/description par langue. L'étude de cas déclare son propre canonical (avec le ?slug=) et la page gabarit sans slug (« Projet introuvable ») est en noindex.
  • PDF avec titre, auteur, sujet, mots-clés et langue dans leurs métadonnées (écrites par scripts/generate-pdf.js via pdf-lib) — les PDF sont indexables eux aussi, autant qu'ils se présentent bien.
  • Repli <noscript> qui dit ce qui manque sans JavaScript (Fit-Checker, anglais) et lie les PDF.

Ce qui reste hors code, et qui pèse plus lourd que tout le reste :

  1. Des liens depuis tes propres sites — la seule source de backlinks qui ne dépend de personne :
    • portfolio photo antoine.berthaud.me (Adobe Portfolio) : un lien « CV » dans le menu ou sur la page Contact, et idéalement une phrase « Product Manager à Nantes » quelque part dans le texte ;
    • tourdegrowth.com : un pied de page du type « Un side project d'Antoine Berthaud, Product Manager à Nantes » avec le lien vers le CV.
  2. LinkedIn : l'URL du CV dans le champ « Site web », dans « En vedette » et dans « Infos ». Vérifier aussi qu'il n'existe qu'un seul profil à ton nom chez AB Tasty — un ancien profil en doublon dilue la recherche sur ton nom et envoie les recruteurs au mauvais endroit.
  3. Google Search Console (propriété déjà créée) : après chaque mise en ligne importante, demander l'indexation des URLs via « Inspection de l'URL » plutôt que d'attendre le prochain crawl.

11. Side projects — carte + page de détail ("étude de cas")

Chaque side project affiché (CONFIG.showSideProjects = true) peut avoir, en plus de sa carte sur la page d'accueil, une page de détail dédiée façon mini-étude de cas — problème identifié, mécanismes de croissance mis en place, démarche produit, chiffres d'usage. Le tout pensé comme un gabarit réutilisable : ajouter un nouveau side project avec sa page de détail ne demande de toucher qu'à data.js, jamais à project-detail.html ni à project-detail.js.

Pour ajouter un nouveau side project avec sa page de détail :

  1. Ajoute une entrée dans SIDE_PROJECTS (titre, description, link vers le projet en ligne) comme d'habitude.
  2. Ajoute une clé correspondante dans PROJECT_DETAILS (juste après SIDE_PROJECTS dans data.js) : le plus simple est de copier l'entrée tour-de-growth et de remplacer les textes. Champs : title, tagline, liveUrl, problem, whatItIs (+ whatItIsPoints et whatItIsClosing optionnels), teachingMoment (optionnel, {title, body}), process, metrics (laisse [] tant que tu n'as pas de vrais chiffres significatifs — metricsFallback s'affiche à la place), techStack.
  3. Relie les deux en renseignant detailSlug sur l'entrée SIDE_PROJECTS avec la même clé que dans PROJECT_DETAILS.
  4. C'est tout — project-detail.html?slug=ta-clé fonctionne immédiatement, dans les deux langues, avec le lien retour vers le CV qui préserve la langue courante.

Pourquoi une page séparée plutôt qu'une section de plus sur la page d'accueil : ça permet de documenter un side project en profondeur (la démarche produit, pas juste le résultat) sans alourdir la page d'accueil, qui reste un CV scannable rapidement. Le lien "Voir l'étude de cas" sur la carte est optionnel — une entrée SIDE_PROJECTS sans detailSlug n'a tout simplement pas ce second lien.

  • Ajouter les logos d'entreprise (assets/logos/) — fait
  • Créer la clé Gemini + déployer la fonction Supabase — fait (v13 déployée le 6 septembre 2026)
  • Configurer Upstash si tu veux le rate limiting — fait, 3/min actif en prod
  • Créer un compte GoatCounter gratuit et remplacer TON-CODE dans index.html — fait (site antoineberthaud)
  • Configurer le CNAME chez ton registrar DNS + activer le domaine personnalisé — fait, cv.antoine.berthaud.me en ligne
  • Inscrire le site sur Google Search Console et soumettre le sitemap — fait ; demander l'indexation des URLs après chaque mise en ligne importante (section 10)
  • Ajouter le lien du CV sur LinkedIn (« Site web », « En vedette », « Infos ») et supprimer l'éventuel ancien profil en doublon
  • Ajouter un lien vers le CV depuis antoine.berthaud.me (Adobe Portfolio) et depuis tourdegrowth.com (section 10)
  • Vérifier les permissions du workflow GitHub Actions — fait, le workflow commite les PDF
  • Repasser CONFIG.showSideProjects à true — fait, Tour de Growth est en place
  • Remplir metrics dans PROJECT_DETAILS (Tour de Growth) dès qu'il y a de vrais chiffres d'usage (visites, taux de partage, coefficient viral) — le texte de repli actuel est temporaire, pas un oubli
  • Quand le projet climat sera prêt à être montré : lui ajouter aussi sa carte + page de détail en suivant le même pattern (section 11)
  • Déployer sur GitHub Pages — fait

12. Continuité du Fit-Checker (Supabase en plan Free)

Le projet Supabase (tpreesulucfsyalaipcj, plan Free) est mis en pause après 7 jours sans requête API — et les appels à la fonction gemini-fit ne comptent pas comme de l'activité, seules les requêtes à la base (PostgREST) comptent. Un projet en pause = un Fit-Checker qui affiche le message d'indisponibilité à tous les recruteurs sans que personne ne le sache.

Trois filets, du plus important au moins :

  1. .github/workflows/surveiller-fit-checker.yml — sonde quotidienne (6h43 UTC) : un vrai appel à la fonction, comme un recruteur. OK si HTTP 200 avec un score (ou 429 : le rate limit répond, donc la fonction vit). Second essai après 60 s, puis issue GitHub avec le label fit-checker-down et le lien vers le projet (bouton Restore). À tester à la main : onglet Actions → « Surveiller le Fit-Checker » → Run workflow → le log doit dire Fit-Checker OK (HTTP 200).
  2. .github/workflows/keepalive.yml + table public.keepalive (supabase/migrations/) — trois lectures quotidiennes (5h17 UTC) de cette table d'une ligne, en lecture seule pour la clé anon : c'est l'activité qui empêche la pause.
  3. Un moniteur externe gratuit (UptimeRobot ou cron-job.org) sur la même URL de lecture, toutes les 12 h. Il fait activité et alerte sans dépendre de GitHub : GitHub désactive les workflows planifiés d'un dépôt public après 60 jours sans commit (réactivation par « Enable workflow » dans l'onglet Actions, ou par un commit qui modifie la ligne cron). Deux façons de passer la clé anon (publique, c'est celle de js/data.js) — vérifiées le 06/09/2026, les deux répondent 200 [{"id":1}] :
    • en-tête HTTP apikey: <clé anon> sur https://tpreesulucfsyalaipcj.supabase.co/rest/v1/keepalive?select=id&limit=1 ;
    • ou, pour un moniteur qui ne sait pas envoyer d'en-tête, la clé dans l'URL : https://tpreesulucfsyalaipcj.supabase.co/rest/v1/keepalive?select=id&limit=1&apikey=<clé anon>. Sans clé, la réponse est un 401 « No API key found » ; en écriture (insert, update, delete), la clé anon est refusée (401, permission denied). Migration appliquée le 06/09/2026 (supabase/migrations/20260906120000_keepalive.sql).

Si le projet est passé en pause malgré tout : dashboard Supabase → Restore (quelques minutes), puis relancer la sonde à la main pour vérifier.

Clé Gemini en prépayé (depuis le 06/09/2026). Le projet Google de la clé est relié à un compte de facturation Prepay (Tier 1) : les offres collées par les recruteurs ne servent pas à améliorer les modèles Google (conditions Gemini API, « Paid Services »), et il est impossible de dépenser plus que le solde chargé. Ordre de grandeur : 0,6 centime par analyse, 5 $ couvrent largement un an ; les crédits expirent au bout d'un an. Si l'issue « Le Fit-Checker ne répond plus » s'ouvre, vérifier le solde sur https://aistudio.google.com/billing et recharger 5 $ (à zéro, toutes les clés du compte s'arrêtent). Le plafond mensuel de dépense se règle sur la page « Spend » d'AI Studio.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages