Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/generate-pdf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ jobs:
steps:
- name: Checkout du dépôt
uses: actions/checkout@v5
with:
# Historique complet : le pré-rendu date la création de la page profil
# (JSON-LD dateCreated) d'après le premier commit de index.html.
fetch-depth: 0

- name: Installer Node.js
uses: actions/setup-node@v5
Expand Down Expand Up @@ -75,18 +79,20 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A -- index.html results.html project-detail.html en projets assets/cv-antoine-berthaud-fr.pdf assets/cv-antoine-berthaud-en.pdf assets/cv-antoine-berthaud-fr-court.pdf assets/cv-antoine-berthaud-en-short.pdf
# sitemap.xml : le pré-rendu y tient à jour les <lastmod> des pages
# (zone static:pages) ; ceux des PDF sont posés ci-dessous.
git add -A -- index.html results.html project-detail.html en projets sitemap.xml assets/cv-antoine-berthaud-fr.pdf assets/cv-antoine-berthaud-en.pdf assets/cv-antoine-berthaud-fr-court.pdf assets/cv-antoine-berthaud-en-short.pdf
if git diff --cached --quiet; then
echo "Aucun changement dans les pages ni les PDF, rien à committer."
else
# Pages et PDF sont déclarés dans sitemap.xml : on reporte la date de
# régénération (UTC) sur le <lastmod> de chaque fichier qui a changé.
python3 - "$(date -u +%Y-%m-%d)" $(git diff --cached --name-only) <<'EOF'
# Les PDF sont déclarés dans sitemap.xml : on reporte la date de
# régénération (UTC) sur le <lastmod> de chaque PDF qui a changé.
python3 - "$(date -u +%Y-%m-%d)" $(git diff --cached --name-only -- '*.pdf') <<'EOF'
import re, sys, pathlib
today, changed = sys.argv[1], set(sys.argv[2:])
# Fichier du dépôt → URL du sitemap qu'il porte (index.html → dossier).
# Fichier du dépôt → URL du sitemap qu'il porte.
def url(f):
return "https://cv.antoine.berthaud.me/" + (f[: -len("index.html")] if f.endswith("index.html") else f)
return "https://cv.antoine.berthaud.me/" + f
p = pathlib.Path("sitemap.xml")
s = p.read_text()
total = 0
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
steps:
- name: Checkout du dépôt
uses: actions/checkout@v5
with:
fetch-depth: 0 # le pré-rendu date la création de la page profil (premier commit de index.html)

- name: Installer Node.js
uses: actions/setup-node@v5
Expand Down Expand Up @@ -57,12 +59,12 @@ jobs:
# recommittera après le merge (durées mises à jour, par exemple).
- name: Écart entre les pages committées et le pré-rendu de la CI
run: |
if git diff --quiet -- index.html results.html project-detail.html en/ projets/ && [ -z "$(git ls-files --others --exclude-standard -- en/ projets/)" ]; then
if git diff --quiet -- index.html results.html project-detail.html en/ projets/ sitemap.xml && [ -z "$(git ls-files --others --exclude-standard -- en/ projets/)" ]; then
echo "Pages pré-rendues à jour : aucun écart."
else
git --no-pager diff --stat -- index.html results.html project-detail.html en/ projets/
git --no-pager diff --stat -- index.html results.html project-detail.html en/ projets/ sitemap.xml
git ls-files --others --exclude-standard -- en/ projets/
git --no-pager diff -- index.html results.html project-detail.html en/ projets/ | head -120
git --no-pager diff -- index.html results.html project-detail.html en/ projets/ sitemap.xml | head -120
fi

- name: Générer les PDF FR et EN (sans les committer)
Expand Down
15 changes: 12 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ donnée : tout le contenu vit dans `js/data.js`, le rendu dans `js/app.js`.
pages ; les courts sont hors sitemap, voulu), avec
`scripts/lib/site-server.js` en commun. Le
workflow `.github/workflows/generate-pdf.yml` enchaîne les deux et
recommit pages + PDF (+ `lastmod` du sitemap) ; il tourne aussi une fois
recommit pages, sitemap et PDF ; il tourne aussi une fois
par mois (1er, 4h UTC) indépendamment de tout push, pour rafraîchir les
durées d'expérience affichées (calculées jusqu'à "aujourd'hui") — et
ouvre une issue GitHub (label `pdf-generation-failure`) s'il échoue.
Expand Down Expand Up @@ -356,8 +356,17 @@ plein ont tous les deux été essayés et jugés trop nets avant ça.
tenue par les job boards). Ce qui pèse le plus est hors code : liens
depuis `antoine.berthaud.me` et `tourdegrowth.com`, LinkedIn, demandes
d'indexation dans Search Console (propriété existante, accessible via le
MCP SEO Gets). Le `lastmod` des pages et des deux PDF dans `sitemap.xml`
est posé par `generate-pdf.yml` à chaque régénération, jamais à la main.
MCP SEO Gets). Dans `sitemap.xml`, les entrées des pages sont une zone
générée par `generate-static.js` : le `lastmod` d'une page n'avance que si
son HTML généré change (comparaison au fichier sur le disque), et le
`dateModified` de l'accueil reprend exactement cette date ; le
`dateCreated` vient du premier commit de `index.html`, d'où `fetch-depth: 0`
dans les deux workflows (le générateur refuse un clone superficiel). Le
`lastmod` des deux PDF est posé par `generate-pdf.yml`. Jamais à la main.
Avant sept. 2026, le workflow datait aussi les pages, mais seulement quand
la CI elle-même les modifiait : une PR arrivée avec ses pages déjà
régénérées laissait le `lastmod` figé (06/09 alors que la page avait changé
le 07/09).
Les PDF portent des métadonnées (titre, auteur, langue) posées par
`scripts/generate-pdf.js` via `pdf-lib`.
- **Site considéré fonctionnellement complet** (sept. 2026) : bilingue,
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ Le bouton **"Télécharger PDF"** ne fait plus un simple `Ctrl/Cmd+P` navigateur

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.
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, le sitemap et les PDF à jour (le pré-rendu date les pages, le workflow les PDF) — **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)

Expand Down Expand Up @@ -329,8 +329,11 @@ Soyons lucides sur l'objectif : sur une requête générique comme « product ma
Ce qui est en place dans le code :
- **Français à la racine, anglais sous `/en/`** (`/en/`, `/en/results.html`), deux vraies pages statiques, sans détection de la langue du navigateur (voir section 0) : Googlebot navigue en anglais et indexait la version anglaise à l'URL française. Chaque page déclare son propre canonical et les `hreflang` fr/en/x-default (x-default = français), dans le `<head>` et dans le sitemap. Les anciennes URL `?lang=en` redirigent vers `/en/…` (script en tête du `<head>`, GitHub Pages n'offrant pas de redirection serveur). Seule exception à « pas de détection navigateur » : `404.html`, qui n'est pas indexée, garde le bloc FR ou EN d'après l'URL cassée si elle est sous `/en/`, sinon 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 `ProfilePage` → `Person`** (JSON-LD de l'accueil, en français et en anglais, écrit par le pré-rendu depuis `scripts/lib/structured-data.js`) : 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 (même `@id` dans les deux langues).
- **`sitemap.xml`** avec toutes les pages indexables (accueil et Résultats en FR et EN avec leurs `hreflang`, é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`.
- **Données structurées** (JSON-LD, écrites par le pré-rendu depuis `scripts/lib/structured-data.js`, dans la langue de chaque page ; la personne a partout le même `@id`, `https://cv.antoine.berthaud.me/#person`) :
- accueil : `ProfilePage` → `Person` (métier, lieu, formation, sujets maîtrisés, `sameAs` vers LinkedIn et le portfolio photo), avec `dateCreated` (premier commit de `index.html`) et `dateModified` (le `lastmod` de la page) ;
- études de cas : `CollectionPage` et fil d'Ariane (CV → Études de cas) ;
- pages projet : `WebPage`, fil d'Ariane et `WebApplication` dont Antoine est le `creator` — c'est ce lien, et non un `sameAs`, qui relie Tour de Growth à la personne.
- **`sitemap.xml`** avec toutes les pages indexables, en FR et EN avec leurs `hreflang` : ces entrées sont générées par le pré-rendu, dont le `lastmod` n'avance que quand le HTML de la page change (le même jour que le `dateModified` de l'accueil) ; 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` / `en/results.html` et les pages projet (`/projets/…`, `/en/projects/…`) ont leur propre `<title>`/description par langue, canonical et hreflang. L'ancienne adresse `project-detail.html?slug=…` redirige vers la page statique ; pour un slug inconnu elle affiche « Projet introuvable » en `noindex` (posé par JavaScript, jamais dans le HTML brut, pour que Google suive la redirection). Historique : l'étude de cas déclarait 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 (le Fit-Checker) et lie les PDF.
Expand Down
5 changes: 3 additions & 2 deletions en/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"url": "https://cv.antoine.berthaud.me/en/",
"name": "Antoine Berthaud's CV — Senior Product Manager in Nantes",
"inLanguage": "en",
"dateCreated": "2026-08-22",
"dateModified": "2026-09-24",
"mainEntity": {
"@type": "Person",
"@id": "https://cv.antoine.berthaud.me/#person",
Expand Down Expand Up @@ -110,8 +112,7 @@
],
"sameAs": [
"https://www.linkedin.com/in/antoine-berthaud-pm/",
"https://antoine.berthaud.me/",
"https://www.tourdegrowth.com/"
"https://antoine.berthaud.me/"
]
}
}
Expand Down
46 changes: 46 additions & 0 deletions en/results.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,52 @@
<meta property="og:locale:alternate" content="fr_FR">
<!-- /static:langLinks -->

<!-- Données structurées (schema.org) : la page (CollectionPage) et son fil
d'Ariane, dans la langue de la page. Zone générée par
scripts/generate-static.js à partir de scripts/lib/structured-data.js —
ne pas éditer à la main. -->
<!-- static:jsonLd -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "CollectionPage",
"@id": "https://cv.antoine.berthaud.me/en/results.html#page",
"url": "https://cv.antoine.berthaud.me/en/results.html",
"name": "Product Manager case studies — Antoine Berthaud, Nantes",
"description": "Antoine Berthaud's product case studies, Product Manager in Nantes, France: context, hypothesis, options ruled out, action and result (AB Tasty, Everysens, SNCF Connect & Tech).",
"inLanguage": "en",
"author": {
"@id": "https://cv.antoine.berthaud.me/#person"
},
"breadcrumb": {
"@id": "https://cv.antoine.berthaud.me/en/results.html#breadcrumb"
}
},
{
"@type": "BreadcrumbList",
"@id": "https://cv.antoine.berthaud.me/en/results.html#breadcrumb",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "CV",
"item": "https://cv.antoine.berthaud.me/en/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Case studies",
"item": "https://cv.antoine.berthaud.me/en/results.html"
}
]
}
]
}
</script>
<!-- /static:jsonLd -->

<meta property="og:type" content="article">
<meta property="og:title" content="Product Manager case studies — Antoine Berthaud, Nantes">
<meta property="og:description" content="Antoine Berthaud's product case studies, Product Manager in Nantes, France: context, hypothesis, options ruled out, action and result (AB Tasty, Everysens, SNCF Connect &amp; Tech).">
Expand Down
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"url": "https://cv.antoine.berthaud.me/",
"name": "CV d'Antoine Berthaud — Senior Product Manager à Nantes",
"inLanguage": "fr",
"dateCreated": "2026-08-22",
"dateModified": "2026-09-24",
"mainEntity": {
"@type": "Person",
"@id": "https://cv.antoine.berthaud.me/#person",
Expand Down Expand Up @@ -123,8 +125,7 @@
],
"sameAs": [
"https://www.linkedin.com/in/antoine-berthaud-pm/",
"https://antoine.berthaud.me/",
"https://www.tourdegrowth.com/"
"https://antoine.berthaud.me/"
]
}
}
Expand Down
46 changes: 46 additions & 0 deletions results.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,52 @@
<meta property="og:locale:alternate" content="en_US">
<!-- /static:langLinks -->

<!-- Données structurées (schema.org) : la page (CollectionPage) et son fil
d'Ariane, dans la langue de la page. Zone générée par
scripts/generate-static.js à partir de scripts/lib/structured-data.js —
ne pas éditer à la main. -->
<!-- static:jsonLd -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "CollectionPage",
"@id": "https://cv.antoine.berthaud.me/results.html#page",
"url": "https://cv.antoine.berthaud.me/results.html",
"name": "Études de cas Product Manager — Antoine Berthaud, Nantes",
"description": "Les études de cas produit d'Antoine Berthaud, Product Manager à Nantes : contexte, hypothèse, options écartées, action et résultat (AB Tasty, Everysens, SNCF Connect & Tech).",
"inLanguage": "fr",
"author": {
"@id": "https://cv.antoine.berthaud.me/#person"
},
"breadcrumb": {
"@id": "https://cv.antoine.berthaud.me/results.html#breadcrumb"
}
},
{
"@type": "BreadcrumbList",
"@id": "https://cv.antoine.berthaud.me/results.html#breadcrumb",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "CV",
"item": "https://cv.antoine.berthaud.me/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Études de cas",
"item": "https://cv.antoine.berthaud.me/results.html"
}
]
}
]
}
</script>
<!-- /static:jsonLd -->

<meta property="og:type" content="article">
<meta property="og:title" content="Études de cas Product Manager — Antoine Berthaud, Nantes">
<meta property="og:description" content="Les études de cas produit d'Antoine Berthaud, Product Manager à Nantes : contexte, hypothèse, options écartées, action et résultat (AB Tasty, Everysens, SNCF Connect &amp; Tech).">
Expand Down
Loading
Loading