Site personnel courcellematthi.eu
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
# 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.
|
||||
|
||||
- Format A4, marges 15/14 mm, marge haute réduite sur la première page
|
||||
- Navigation, boutons et bouton de remontée masqués
|
||||
- Couleurs remises à plat en noir sur blanc : lisible sur imprimante
|
||||
monochrome, où le vert `#05BC8A` sortirait en gris quasi invisible
|
||||
- URL des liens externes affichées entre parenthèses ; `mailto:` et ancres
|
||||
internes exclus, où l'affichage serait redondant ou absurde
|
||||
- Fiches jamais coupées entre deux pages, mais entrées de parcours coupables :
|
||||
interdire la coupure des entrées faisait basculer des blocs entiers et
|
||||
gaspillait une page complète
|
||||
- `orphans` / `widows` à 3 pour éviter les lignes isolées
|
||||
|
||||
Résultat mesuré : 5 pages A4, sans titre séparé de son contenu.
|
||||
|
||||
---
|
||||
|
||||
## 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é.
|
||||
Reference in New Issue
Block a user