373 lines
16 KiB
Markdown
373 lines
16 KiB
Markdown
# courcellematthi.eu
|
||
|
||
Site personnel et CV en ligne de Matthieu Courcelle.
|
||
|
||
Statique, sans dépendance, sans étape de compilation. Aucun framework, aucun
|
||
gestionnaire de paquets, aucun outil de build : les fichiers déposés sur le
|
||
serveur sont exactement ceux qui sont servis au navigateur.
|
||
|
||
---
|
||
|
||
## Arborescence
|
||
|
||
```
|
||
.
|
||
├── index.html Document unique, sections ancrées
|
||
├── favicon.ico Repli multi-résolutions (16/32/48)
|
||
├── favicon.svg Icône vectorielle — monogramme MC
|
||
├── robots.txt Directives d'exploration, y compris robots IA
|
||
├── sitemap.xml Plan du site
|
||
├── site.webmanifest Manifeste d'application web
|
||
├── llms.txt Résumé structuré pour agents conversationnels
|
||
├── humans.txt Crédits (convention humanstxt.org)
|
||
├── .htaccess Configuration Apache (mutualisé)
|
||
├── nginx.conf.example Configuration Nginx (serveur dédié, hors conteneur)
|
||
├── Dockerfile Image du conteneur
|
||
├── docker-compose.yaml Déploiement Coolify (mode Compose)
|
||
├── .dockerignore Exclusions du contexte de build
|
||
├── DEPLOIEMENT-COOLIFY.md Guide de mise en ligne pas à pas
|
||
├── deploy/
|
||
│ └── nginx.conf Configuration Nginx DANS le conteneur
|
||
├── .well-known/
|
||
│ └── security.txt Contact de divulgation (RFC 9116)
|
||
└── assets/
|
||
├── css/
|
||
│ ├── tokens.css Variables + ordre de cascade
|
||
│ ├── base.css Reset et éléments HTML natifs
|
||
│ ├── layout.css Conteneurs, en-tête, navigation, grilles
|
||
│ ├── components.css Blocs réutilisables (BEM)
|
||
│ ├── utilities.css Classes utilitaires
|
||
│ └── print.css Feuille d'impression (media="print")
|
||
├── js/
|
||
│ ├── main.js Point d'entrée (module ES)
|
||
│ └── modules/
|
||
│ ├── navigation.js Menu mobile, piège de focus
|
||
│ ├── scroll-spy.js Section active (IntersectionObserver)
|
||
│ ├── back-to-top.js Bouton de remontée
|
||
│ └── copy-email.js Copie de l'adresse e-mail
|
||
└── img/
|
||
├── og-image.png Bannière de partage 1200×630
|
||
├── apple-touch-icon.png Écran d'accueil iOS
|
||
├── icon-192.png PWA
|
||
├── icon-512.png PWA
|
||
├── icon-maskable-512.png PWA, zone sûre Android
|
||
├── favicon-16/32/48.png Replis
|
||
└── safari-pinned-tab.svg Onglet épinglé Safari
|
||
```
|
||
|
||
---
|
||
|
||
## Architecture CSS
|
||
|
||
### Couches en cascade
|
||
|
||
L'ordre de priorité est déclaré une seule fois, en tête de `tokens.css` :
|
||
|
||
```css
|
||
@layer tokens, base, layout, components, utilities;
|
||
```
|
||
|
||
Conséquence : une règle de `utilities` l'emporte sur une règle de `components`
|
||
même si son sélecteur est moins spécifique, et l'ordre des balises `<link>`
|
||
dans le `<head>` n'a aucune incidence. C'est ce qui permet de se passer
|
||
entièrement de `!important` et des sélecteurs à rallonge écrits uniquement
|
||
pour gagner un duel de spécificité.
|
||
|
||
### Pas de `@import`
|
||
|
||
Les feuilles sont liées séparément dans le `<head>`. Un `@import` obligerait
|
||
le navigateur à télécharger et analyser le fichier parent avant de découvrir
|
||
l'existence de l'enfant : les requêtes s'enchaînent en série au lieu de
|
||
partir en parallèle. Sur cinq fichiers, l'écart est mesurable.
|
||
|
||
### Jetons de conception
|
||
|
||
Toute couleur, taille, espacement ou durée est déclarée dans `tokens.css`.
|
||
Aucune valeur littérale n'apparaît ailleurs. Changer l'identité visuelle du
|
||
site ne demande donc la modification que d'un seul fichier.
|
||
|
||
Le fichier ne contient aucun jeton inutilisé : 56 variables définies,
|
||
56 utilisées.
|
||
|
||
### Propriétés logiques
|
||
|
||
`inline-size` plutôt que `width`, `padding-block` plutôt que
|
||
`padding-top/bottom`, `inset-inline-start` plutôt que `left`. Le site est
|
||
en français, mais ces propriétés sont aujourd'hui le standard et rendent
|
||
une éventuelle version en écriture de droite à gauche triviale.
|
||
|
||
### Typographie fluide
|
||
|
||
`clamp()` sur toutes les tailles de titre. Le terme central mêle `rem` et
|
||
`vw` : le `rem` garantit que le zoom navigateur et la taille de police
|
||
système continuent d'agir (une valeur en `vw` seule casserait
|
||
l'accessibilité), le `vw` apporte la fluidité entre les points de rupture.
|
||
|
||
Résultat : une seule media query dans tout le projet, pour la bascule du
|
||
menu en mode mobile.
|
||
|
||
---
|
||
|
||
## Architecture JavaScript
|
||
|
||
### Amélioration progressive
|
||
|
||
Le site est intégralement lisible, navigable et imprimable sans JavaScript.
|
||
Aucun contenu n'est injecté par script — le HTML servi est complet. C'est ce
|
||
qui garantit l'indexation par les moteurs et la lecture par les agents
|
||
conversationnels, dont beaucoup n'exécutent pas JavaScript.
|
||
|
||
Les quatre modules n'ajoutent que du confort : repli du menu sur petit écran,
|
||
surbrillance de la section lue, bouton de remontée, copie de l'adresse.
|
||
|
||
### Modules ES natifs
|
||
|
||
`<script type="module">` implique `defer` : le téléchargement est parallèle à
|
||
l'analyse du HTML, l'exécution a lieu après. Le script est donc dans le
|
||
`<head>` sans bloquer le rendu — la vieille habitude de le placer en fin de
|
||
`<body>` n'a plus lieu d'être. Les modules apportent en prime le mode strict
|
||
et une portée isolée, sans aucune variable globale.
|
||
|
||
### Pas d'écouteur de défilement
|
||
|
||
Le scroll-spy et le bouton de remontée reposent sur `IntersectionObserver`.
|
||
Un écouteur `scroll` se déclenche des dizaines de fois par seconde et impose
|
||
d'appeler `getBoundingClientRect()` à chaque fois, ce qui force le navigateur
|
||
à recalculer la mise en page (*layout thrashing*) et fait chuter la fluidité.
|
||
`IntersectionObserver` délègue ce travail au moteur de rendu, hors du fil
|
||
principal.
|
||
|
||
---
|
||
|
||
## Accessibilité
|
||
|
||
| Point | Mise en œuvre |
|
||
|---|---|
|
||
| Contourner les blocs (WCAG 2.4.1) | Lien d'évitement, premier élément focusable |
|
||
| Structure sémantique | `header`, `nav`, `main`, `section`, `article`, `footer` |
|
||
| Repères nommés | `aria-label` sur chaque `<nav>`, `aria-labelledby` sur chaque section |
|
||
| État des composants | `aria-expanded` et `aria-controls` sur le bouton de menu |
|
||
| Position courante (2.4.8) | `aria-current="true"` sur le lien de section active |
|
||
| Messages dynamiques | Région `role="status" aria-live="polite"` |
|
||
| Focus visible | `:focus-visible`, jamais `outline: none` sans remplacement |
|
||
| Piège de focus | Tabulation bouclée dans le menu mobile ouvert |
|
||
| Touche Échap | Ferme le menu et rend le focus au bouton |
|
||
| Cibles tactiles (2.5.5) | 44 px minimum, vérifié à 44 px exactement |
|
||
| Couleur non porteuse seule (1.4.1) | Section active signalée par couleur **et** bordure |
|
||
| Contrastes | Texte principal 14,8:1, corps 7,4:1, liens 4,3:1 |
|
||
| Animations | `prefers-reduced-motion` respecté, y compris pour le défilement |
|
||
| Contraste renforcé | `prefers-contrast: more` ajuste la palette |
|
||
| Images décoratives | `aria-hidden="true"` sur les 20 SVG d'ornement |
|
||
|
||
L'accent pur `#05BC8A` n'est jamais utilisé pour du texte sur fond clair : son
|
||
contraste (2,3:1) est insuffisant. Il sert uniquement de trait, de bordure et
|
||
de puce. Les liens utilisent `#03996F` (4,3:1).
|
||
|
||
---
|
||
|
||
## Référencement
|
||
|
||
### Balises
|
||
|
||
`title` de 56 caractères (sous le seuil de troncature de 60), description de
|
||
153 caractères (plage utile 120-170), `rel="canonical"`, `hreflang` avec
|
||
`x-default`, directives `robots` explicites avec `max-image-preview:large`.
|
||
|
||
### Données structurées
|
||
|
||
Graphe JSON-LD schema.org à trois entités reliées par `@id` :
|
||
`WebSite` → `ProfilePage` → `Person`, avec `worksFor`, `alumniOf`,
|
||
`hasCredential`, `hasOccupation` et `knowsAbout`.
|
||
|
||
Double bénéfice : Google associe la page à une entité « personne » plutôt
|
||
qu'à un simple document, et les agents conversationnels disposent d'une
|
||
déclaration non ambiguë plutôt que d'avoir à inférer depuis la prose.
|
||
|
||
### Fichier `llms.txt`
|
||
|
||
Résumé du site en texte brut à destination des moteurs de réponse et des
|
||
agents. Contient une section « Notes à l'attention des agents » qui lève
|
||
explicitement les pièges : l'établissement a changé de nom (IPEPS
|
||
Tournai-Leuze = Institut Gabrielle Petit), `underscorelab` s'écrit en
|
||
minuscules, et les projets menés pour des établissements tiers ne sont pas
|
||
des fonctions officielles.
|
||
|
||
### Robots IA
|
||
|
||
`robots.txt` autorise explicitement GPTBot, ClaudeBot, PerplexityBot,
|
||
Google-Extended et Applebot-Extended. Arbitrage assumé : sur un CV, être cité
|
||
par un agent lors d'une recherche de profil est un bénéfice. Basculer ces
|
||
blocs en `Disallow` si l'arbitrage change.
|
||
|
||
---
|
||
|
||
## Partage sur les réseaux sociaux
|
||
|
||
Bannière `og:image` de 1200×630 (ratio 1,91:1) générée à partir du monogramme,
|
||
avec `og:image:width`, `og:image:height` et `og:image:alt` — les dimensions
|
||
déclarées évitent le recadrage aléatoire sur LinkedIn. URL absolue
|
||
obligatoire : les robots sociaux ne résolvent pas les chemins relatifs.
|
||
|
||
Open Graph de type `profile` avec `profile:first_name` / `profile:last_name`,
|
||
carte Twitter en `summary_large_image`.
|
||
|
||
---
|
||
|
||
## Impression
|
||
|
||
Un CV est imprimé et exporté en PDF : `print.css` traite ce cas comme un
|
||
livrable, pas comme un accident.
|
||
|
||
**L'écran et le papier ne servent pas le même usage.** À l'écran, le visiteur
|
||
parcourt et approfondit : les descriptions de cours, le texte de présentation
|
||
et les projets ont leur place. Sur papier, un recruteur trie une pile en
|
||
quelques secondes — il veut les postes, les compétences, les diplômes et de
|
||
quoi rappeler.
|
||
|
||
La feuille d'impression ne masque donc pas « un peu » de contenu : elle
|
||
recompose le document en **CV classique sur une seule page A4**.
|
||
|
||
| Conservé | Retiré |
|
||
|---|---|
|
||
| Nom, fonction, coordonnées | Texte de présentation |
|
||
| Postes, dates, établissements | Descriptions de cours |
|
||
| Intitulés de cours (en ligne) | Projets *(option, voir ci-dessous)* |
|
||
| Coordination pédagogique | Navigation, boutons, pied de page |
|
||
| Compétences (3 colonnes) | Section contact (doublon de l'en-tête) |
|
||
| Formations | |
|
||
|
||
Mises en œuvre notables : marges à 12 mm, corps à 9,1 pt avec un interligne de
|
||
1,42, trait de chronologie supprimé (6 mm de marge gauche récupérés pour un
|
||
gain de lisibilité nul sur papier), dates flottées à droite, et surtout les
|
||
neuf fiches de cours converties en énumération courante séparée par des points
|
||
médians — empilées elles occupaient 40 mm, en ligne elles tiennent en 12 mm.
|
||
|
||
L'aération a été calibrée par itérations mesurées : la version la plus dense
|
||
laissait un quart de page vide, la plus aérée débordait de 30 mm sur une
|
||
deuxième page. Le réglage retenu remplit la page en gardant une réserve
|
||
d'environ 8 % en pied, marge nécessaire pour absorber les variations de rendu
|
||
entre navigateurs et pilotes d'impression.
|
||
|
||
Les couleurs sont remises à plat en noir sur blanc : lisible sur imprimante
|
||
monochrome, où le vert `#05BC8A` sortirait en gris quasi invisible.
|
||
|
||
**Les projets sont masqués par défaut mais réactivables.** Un bloc commenté en
|
||
fin de `print.css` donne la marche à suivre. Vérifié en test : la version avec
|
||
projets tient encore sur une page et remplit le quart de page actuellement
|
||
vide. L'arbitrage dépend du destinataire — underscorelab, Festifun et les
|
||
outils IA sont ce qui distingue ce CV d'un profil d'enseignant classique, mais
|
||
un jury de recrutement statutaire n'en a pas l'usage.
|
||
|
||
Résultat mesuré : **1 page A4**, dans les deux configurations.
|
||
|
||
---
|
||
|
||
## Sécurité
|
||
|
||
Deux configurations serveur fournies, à choisir selon l'hébergement
|
||
(`nginx.conf.example` ou `.htaccess` — jamais les deux).
|
||
|
||
Elles couvrent : redirection HTTP → HTTPS, redirection vers l'URL canonique,
|
||
HSTS, Content-Security-Policy, `X-Content-Type-Options`, `Referrer-Policy`,
|
||
`Permissions-Policy`, `X-Frame-Options`, compression gzip/brotli, cache
|
||
différencié (HTML revalidé systématiquement, assets mis en cache longuement),
|
||
types MIME explicites et blocage du listage de répertoires.
|
||
|
||
> **Avertissement HSTS.** Ne l'activer qu'une fois le HTTPS confirmé
|
||
> fonctionnel. Un `Strict-Transport-Security` émis par erreur rend le site
|
||
> inaccessible jusqu'à expiration, sans possibilité d'annulation côté serveur.
|
||
|
||
La directive `script-src` inclut `'unsafe-inline'` uniquement à cause de
|
||
l'attribut `onclick` du bouton d'impression. Déplacer ce bouton dans un module
|
||
permettrait de durcir la CSP.
|
||
|
||
---
|
||
|
||
## Déploiement conteneurisé (Coolify)
|
||
|
||
Le projet contient tout le nécessaire pour un déploiement Coolify derrière
|
||
Traefik. Voir **DEPLOIEMENT-COOLIFY.md** pour la procédure complète.
|
||
|
||
Point d'attention : il existe **deux** configurations Nginx, et elles ne sont
|
||
pas interchangeables.
|
||
|
||
| Fichier | Usage | TLS |
|
||
|---|---|---|
|
||
| `nginx.conf.example` | Serveur Nginx classique, sans conteneur | Gère TLS et redirections |
|
||
| `deploy/nginx.conf` | À l'intérieur du conteneur Docker | **Aucun** — Traefik s'en charge |
|
||
|
||
Utiliser `nginx.conf.example` dans le conteneur provoquerait une boucle de
|
||
redirection infinie : Traefik transmet la requête en HTTP clair, le conteneur
|
||
la redirigerait vers HTTPS, ce qui repasserait par Traefik.
|
||
|
||
Le HSTS n'est pas émis par le conteneur mais par Traefik : un réglage
|
||
irréversible n'a pas sa place dans une couche redéployée à chaque `git push`.
|
||
|
||
## Développement local
|
||
|
||
Les modules ES ne fonctionnent pas en `file://` : un serveur HTTP est requis.
|
||
|
||
```bash
|
||
python3 -m http.server 8000
|
||
# puis http://localhost:8000
|
||
```
|
||
|
||
## Déploiement
|
||
|
||
Copier la racine du projet vers le serveur. Aucune compilation.
|
||
|
||
Après déploiement, penser à :
|
||
|
||
1. Mettre à jour `<lastmod>` dans `sitemap.xml`
|
||
2. Soumettre le sitemap dans Google Search Console
|
||
3. Vérifier le rendu des partages via le débogueur LinkedIn et l'outil de
|
||
test des résultats enrichis de Google
|
||
4. Contrôler les en-têtes sur securityheaders.com
|
||
|
||
---
|
||
|
||
## Vérifications effectuées
|
||
|
||
Testé dans Chromium via Playwright, en 1280×900 et 390×844 :
|
||
|
||
- Aucune erreur JavaScript en console
|
||
- Scroll-spy fonctionnel (section active correctement détectée)
|
||
- Menu mobile : ouverture, fermeture, touche Échap, `aria-expanded` cohérent
|
||
- Aucun débordement horizontal en 390 px
|
||
- Navigation par ancre : le titre ciblé ne passe pas sous l'en-tête collant
|
||
(217 px contre 57 px de hauteur d'en-tête)
|
||
- Première tabulation : focus sur le lien d'évitement
|
||
- Cible tactile du menu mobile : 44 px
|
||
- Export PDF : 5 pages A4, mise en page correcte
|
||
- Balises HTML équilibrées, un seul `<h1>`, JSON-LD valide, toutes les
|
||
ancres résolvent, aucune ressource manquante, aucune classe HTML
|
||
sans style, aucun jeton CSS inutilisé
|
||
- Contenu de l'image Docker simulé et servi : 29 fichiers publiés, les
|
||
fichiers de build (`Dockerfile`, `deploy/`, `README.md`, `.htaccess`,
|
||
`docker-compose.yaml`) renvoient bien 404
|
||
|
||
---
|
||
|
||
## Pistes d'amélioration
|
||
|
||
**Auto-héberger les polices.** JetBrains Mono et Inter proviennent
|
||
actuellement de Google Fonts. Les héberger dans `assets/fonts/` supprimerait
|
||
une dépendance tierce, un point de friction RGPD pour un site belge, et deux
|
||
résolutions DNS. À faire avec `preload` sur les fichiers `.woff2` critiques
|
||
et `font-display: swap` dans la déclaration `@font-face`.
|
||
|
||
**Durcir la CSP.** Retirer `'unsafe-inline'` de `script-src` en déplaçant le
|
||
bouton d'impression dans un module JavaScript.
|
||
|
||
**Mesure d'audience.** Si un suivi devient nécessaire, privilégier une
|
||
solution sans cookie et auto-hébergée (Umami, Plausible) plutôt que Google
|
||
Analytics : pas de bandeau de consentement requis, pas de transfert de
|
||
données hors UE.
|
||
|
||
**Photo de profil.** Le CV imprimé d'origine en comportait une. Son absence
|
||
sur le site est un choix par défaut, pas une décision — à trancher.
|
||
|
||
**Informations de contact.** Le numéro de téléphone est absent. À ajouter
|
||
dans la liste de contacts, le JSON-LD (`telephone`) et `llms.txt` si
|
||
souhaité.
|