Files
2026-08-24 09:36:25 +02:00

373 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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é.