From df12d211658b1334c0fe381a2843b439953b9433 Mon Sep 17 00:00:00 2001 From: lrdlg Date: Wed, 5 Aug 2026 22:40:03 +0200 Subject: [PATCH] Site personnel courcellematthi.eu --- .dockerignore | 56 ++ .gitignore | 18 + .htaccess | 103 ++++ .well-known/security.txt | 8 + DEPLOIEMENT-COOLIFY.md | 379 +++++++++++++ Dockerfile | 79 +++ README.md | 343 ++++++++++++ assets/css/base.css | 181 ++++++ assets/css/components.css | 455 +++++++++++++++ assets/css/layout.css | 330 +++++++++++ assets/css/print.css | 225 ++++++++ assets/css/tokens.css | 146 +++++ assets/css/utilities.css | 67 +++ assets/img/apple-touch-icon.png | Bin 0 -> 1216 bytes assets/img/favicon-16x16.png | Bin 0 -> 207 bytes assets/img/favicon-32x32.png | Bin 0 -> 300 bytes assets/img/favicon-48x48.png | Bin 0 -> 383 bytes assets/img/icon-192.png | Bin 0 -> 1277 bytes assets/img/icon-512.png | Bin 0 -> 3750 bytes assets/img/icon-maskable-512.png | Bin 0 -> 2883 bytes assets/img/og-image.png | Bin 0 -> 30275 bytes assets/img/safari-pinned-tab.svg | 6 + assets/js/main.js | 74 +++ assets/js/modules/back-to-top.js | 58 ++ assets/js/modules/copy-email.js | 64 +++ assets/js/modules/navigation.js | 107 ++++ assets/js/modules/scroll-spy.js | 92 +++ deploy/nginx.conf | 148 +++++ docker-compose.yaml | 96 ++++ favicon.ico | Bin 0 -> 2851 bytes favicon.svg | 23 + humans.txt | 15 + index.html | 930 +++++++++++++++++++++++++++++++ llms.txt | 85 +++ nginx.conf.example | 212 +++++++ robots.txt | 50 ++ site.webmanifest | 40 ++ sitemap.xml | 17 + verifier-depot.sh | 96 ++++ 39 files changed, 4503 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 .well-known/security.txt create mode 100644 DEPLOIEMENT-COOLIFY.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 assets/css/base.css create mode 100644 assets/css/components.css create mode 100644 assets/css/layout.css create mode 100644 assets/css/print.css create mode 100644 assets/css/tokens.css create mode 100644 assets/css/utilities.css create mode 100644 assets/img/apple-touch-icon.png create mode 100644 assets/img/favicon-16x16.png create mode 100644 assets/img/favicon-32x32.png create mode 100644 assets/img/favicon-48x48.png create mode 100644 assets/img/icon-192.png create mode 100644 assets/img/icon-512.png create mode 100644 assets/img/icon-maskable-512.png create mode 100644 assets/img/og-image.png create mode 100644 assets/img/safari-pinned-tab.svg create mode 100644 assets/js/main.js create mode 100644 assets/js/modules/back-to-top.js create mode 100644 assets/js/modules/copy-email.js create mode 100644 assets/js/modules/navigation.js create mode 100644 assets/js/modules/scroll-spy.js create mode 100644 deploy/nginx.conf create mode 100644 docker-compose.yaml create mode 100644 favicon.ico create mode 100644 favicon.svg create mode 100644 humans.txt create mode 100644 index.html create mode 100644 llms.txt create mode 100644 nginx.conf.example create mode 100644 robots.txt create mode 100644 site.webmanifest create mode 100644 sitemap.xml create mode 100644 verifier-depot.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..369e0f6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,56 @@ +# ============================================================================= +# .dockerignore — Fichiers exclus du contexte de build +# ----------------------------------------------------------------------------- +# Réduit le contexte envoyé au démon Docker : build plus rapide, et surtout +# aucun risque d'embarquer un secret ou l'historique Git dans une couche. +# +# ATTENTION — NE JAMAIS AJOUTER « Dockerfile » NI « .dockerignore » ICI. +# +# BuildKit (utilisé par Coolify, et par Docker depuis la version 23) applique +# les exclusions AVANT de lire le Dockerfile. Si le Dockerfile est filtré, le +# build échoue avec : +# +# #1 transferring dockerfile: 2B done +# ERROR: failed to solve: failed to read dockerfile: +# open Dockerfile: no such file or directory +# +# Le message est trompeur : le fichier existe bien dans le dépôt, c'est ce +# fichier-ci qui l'a rendu invisible. Le builder classique tolérait le cas ; +# BuildKit non. +# +# Ces deux fichiers sont retirés de l'image finale par le « RUN rm » du +# Dockerfile : ils n'apparaissent donc pas dans le dossier servi au public. +# ============================================================================= + +# Contrôle de version +.git +.gitignore +.gitattributes + +# Documentation, outillage et configurations hors conteneur +README.md +verifier-depot.sh +DEPLOIEMENT-COOLIFY.md +nginx.conf.example +.htaccess + +# Orchestration (le Dockerfile lui-même reste dans le contexte, cf. ci-dessus) +docker-compose.yaml +docker-compose.yml + +# Environnement — ne doit JAMAIS entrer dans une image +.env +.env.* +*.pem +*.key + +# Artefacts d'éditeur et de système +.vscode +.idea +.DS_Store +Thumbs.db +*.swp +*~ + +# Dépendances éventuelles (aucune aujourd'hui, mais si le projet évolue) +node_modules diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f62510 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Environnement et secrets +.env +.env.* +*.pem +*.key + +# Éditeurs +.vscode/ +.idea/ +*.swp +*~ + +# Systèmes +.DS_Store +Thumbs.db + +# Dépendances éventuelles +node_modules/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..bbf3965 --- /dev/null +++ b/.htaccess @@ -0,0 +1,103 @@ +# ============================================================================= +# .htaccess — Configuration Apache pour courcellematthi.eu +# ----------------------------------------------------------------------------- +# Équivalent de nginx.conf.example, pour un hébergement mutualisé où l'accès +# à la configuration serveur n'est pas possible. +# +# Ne PAS utiliser ce fichier ET la configuration Nginx : ils font double emploi. +# Sur Nginx, ce fichier est simplement ignoré. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# REDIRECTIONS CANONIQUES +# ----------------------------------------------------------------------------- + + RewriteEngine On + + # HTTP -> HTTPS + RewriteCond %{HTTPS} !=on + RewriteRule ^ https://www.courcellematthi.eu%{REQUEST_URI} [R=301,L] + + # Domaine nu -> www (doit correspondre au rel="canonical" du HTML) + RewriteCond %{HTTP_HOST} ^courcellematthi\.eu$ [NC] + RewriteRule ^ https://www.courcellematthi.eu%{REQUEST_URI} [R=301,L] + + +# ----------------------------------------------------------------------------- +# EN-TÊTES DE SÉCURITÉ +# Voir nginx.conf.example pour le détail de chaque directive. +# ----------------------------------------------------------------------------- + + # N'activer HSTS qu'une fois le HTTPS vérifié : irréversible côté serveur. + Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" + + Header always set Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests" + + Header always set X-Content-Type-Options "nosniff" + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), interest-cohort=()" + Header always set X-Frame-Options "DENY" + + # Retire la signature du serveur des en-têtes de réponse + Header unset X-Powered-By + + +# ----------------------------------------------------------------------------- +# COMPRESSION +# ----------------------------------------------------------------------------- + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css + AddOutputFilterByType DEFLATE application/javascript application/json + AddOutputFilterByType DEFLATE application/manifest+json image/svg+xml + + + + AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript image/svg+xml + + +# ----------------------------------------------------------------------------- +# CACHE +# ----------------------------------------------------------------------------- + + ExpiresActive On + + # HTML : revalidation systématique, sinon les visiteurs restent sur une + # version périmée après chaque publication. + ExpiresByType text/html "access plus 0 seconds" + + ExpiresByType text/css "access plus 30 days" + ExpiresByType application/javascript "access plus 30 days" + ExpiresByType image/png "access plus 90 days" + ExpiresByType image/svg+xml "access plus 90 days" + ExpiresByType image/x-icon "access plus 90 days" + ExpiresByType font/woff2 "access plus 1 year" + ExpiresByType application/manifest+json "access plus 1 day" + + +# ----------------------------------------------------------------------------- +# TYPES MIME +# ----------------------------------------------------------------------------- + + AddType application/manifest+json .webmanifest + AddType image/svg+xml .svg + AddType font/woff2 .woff2 + + # Encodage explicite : évite les accents cassés dans llms.txt et humans.txt + AddDefaultCharset UTF-8 + AddCharset UTF-8 .html .css .js .txt .xml .json .webmanifest + + +# ----------------------------------------------------------------------------- +# PROTECTION DES FICHIERS +# ----------------------------------------------------------------------------- + +# Interdit l'accès direct à ce fichier et aux fichiers de configuration + + Require all denied + + +# Désactive le listage des répertoires sans index +Options -Indexes + +# Page d'erreur +ErrorDocument 404 /index.html diff --git a/.well-known/security.txt b/.well-known/security.txt new file mode 100644 index 0000000..cbed74f --- /dev/null +++ b/.well-known/security.txt @@ -0,0 +1,8 @@ +# Politique de divulgation de vulnérabilités — RFC 9116 +# Site personnel statique : la surface d'attaque est minimale, mais déclarer +# un canal de contact reste une bonne pratique. + +Contact: mailto:m@tthieu.be +Expires: 2027-08-04T00:00:00.000Z +Preferred-Languages: fr, en +Canonical: https://www.courcellematthi.eu/.well-known/security.txt diff --git a/DEPLOIEMENT-COOLIFY.md b/DEPLOIEMENT-COOLIFY.md new file mode 100644 index 0000000..e33143a --- /dev/null +++ b/DEPLOIEMENT-COOLIFY.md @@ -0,0 +1,379 @@ +# Déploiement sur Coolify + +Guide complet pour mettre `courcellematthi.eu` en ligne depuis Coolify. + +--- + +## Résumé + +| | | +|---|---| +| Type d'application | Dockerfile (recommandé) ou Docker Compose | +| Port interne | 80 | +| Image finale | ~50 Mo | +| Build | Aucune compilation — copie de fichiers statiques | +| TLS | Géré par Traefik, **pas** par le conteneur | + +--- + +## Étape 1 — Préparer le dépôt Git + +Coolify déploie depuis un dépôt. Sur ton Gitea auto-hébergé : + +```bash +cd courcellematthi +git init +git add . +git commit -m "Site personnel — version initiale" +git branch -M main +git remote add origin https://git.ghostinthemachine.fr/matthieu/courcellematthi.git +git push -u origin main +``` + +Vérifie que `.htaccess` et `.well-known/` sont bien poussés — Git les suit +normalement, mais un `.gitignore` trop large les fait parfois disparaître : + +```bash +git ls-files | grep -E "htaccess|well-known" +``` + +--- + +## Étape 2 — Configurer le DNS + +Avant de créer l'application, les enregistrements doivent pointer vers le +serveur, sinon Let's Encrypt échouera à émettre le certificat. + +| Type | Nom | Valeur | +|---|---|---| +| A | `www` | IP de ton serveur | +| A | `@` | IP de ton serveur | + +Si le domaine est chez Cloudflare, **désactive le proxy (nuage orange → gris)** +pendant l'émission du certificat. Le proxy Cloudflare intercepte le challenge +HTTP-01 et fait échouer la validation ACME. Tu pourras le réactiver ensuite. + +Vérification : + +```bash +dig +short www.courcellematthi.eu +dig +short courcellematthi.eu +``` + +--- + +## Étape 3 — Créer l'application dans Coolify + +**Interface Coolify → Projet → + New → Application → Public/Private Repository** + +Renseigne : + +- **Repository** : l'URL de ton dépôt Gitea +- **Branch** : `main` +- **Build Pack** : `Dockerfile` +- **Dockerfile Location** : `/Dockerfile` +- **Base Directory** : `/` +- **Port Exposes** : `80` + +> **Ne choisis pas Nixpacks.** Il tenterait de détecter un projet Node, +> échouerait faute de `package.json`, ou pire, installerait une chaîne de +> build inutile. Le Dockerfile fourni est explicite et reproductible. + +--- + +## Étape 4 — Domaines + +Dans **Configuration → General → Domains**, saisis : + +``` +https://www.courcellematthi.eu +``` + +Coolify génère alors automatiquement les étiquettes Traefik et demande le +certificat Let's Encrypt. + +### Redirection du domaine nu + +Ajoute `https://courcellematthi.eu` comme second domaine, puis active +**Redirect to primary domain** (ou l'option *www ↔ non-www* selon ta version +de Coolify). + +Ce point n'est pas cosmétique : le HTML déclare +``. Si le domaine +nu répond aussi en 200 au lieu de rediriger, les moteurs voient deux URL pour +le même contenu et diluent le référencement. + +**Si tu préfères le domaine nu**, il faut modifier trois endroits pour rester +cohérent : + +1. `index.html` — la balise `canonical`, les `og:url`, les `hreflang`, et les + URL absolues du JSON-LD +2. `sitemap.xml` — la balise `` +3. `robots.txt` — la ligne `Sitemap:` + +--- + +## Étape 5 — Déployer + +Bouton **Deploy**. Le build prend une trentaine de secondes : il n'y a rien à +compiler, seulement à copier. + +Suis les journaux dans l'onglet **Logs**. Une fin de build normale ressemble à : + +``` +naming to docker.io/library/xxxxx done +Container started +``` + +--- + +## Étape 6 — Vérifications post-déploiement + +```bash +# Le site répond +curl -I https://www.courcellematthi.eu + +# La sonde de santé +curl https://www.courcellematthi.eu/healthz # -> ok + +# Le domaine nu redirige (301 attendu, pas 200) +curl -I https://courcellematthi.eu + +# Les en-têtes de sécurité sont présents +curl -sI https://www.courcellematthi.eu | grep -iE "content-security|x-content-type|referrer|permissions" + +# La compression fonctionne +curl -sI -H "Accept-Encoding: gzip" https://www.courcellematthi.eu | grep -i encoding + +# Les fichiers de service sont servis +curl -s https://www.courcellematthi.eu/robots.txt | head -3 +curl -s https://www.courcellematthi.eu/llms.txt | head -3 +curl -sI https://www.courcellematthi.eu/.well-known/security.txt +``` + +--- + +## Les deux pannes classiques sur Coolify + +Ce sont les deux mêmes que tu as déjà rencontrées sur d'autres déploiements. + +### 1. Conteneur absent du réseau `coolify` + +**Symptôme** : Traefik renvoie 404, alors que le conteneur tourne et que ses +journaux sont vides. La requête ne l'atteint jamais. + +**Diagnostic** : + +```bash +docker inspect --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool +``` + +Si `coolify` n'apparaît pas dans la liste : + +```bash +docker network connect coolify +``` + +En mode Docker Compose, le fichier fourni déclare déjà le réseau. En mode +Dockerfile, Coolify le fait normalement seul — mais pas toujours après une +mise à jour de Coolify. + +### 2. Port du load balancer non déclaré + +**Symptôme** : `502 Bad Gateway`, ou `service not found` dans le tableau de +bord Traefik. + +Traefik ne devine le port que si le conteneur n'en expose qu'un seul, et cette +détection échoue régulièrement. Il faut donc le déclarer explicitement. + +**En mode Docker Compose**, l'étiquette est déjà présente : + +```yaml +- traefik.http.services.courcellematthi.loadbalancer.server.port=80 +``` + +**En mode Dockerfile**, va dans **Configuration → Advanced → Custom Labels** +et ajoute : + +``` +traefik.http.services..loadbalancer.server.port=80 +``` + +Récupère le nom généré dans les étiquettes existantes du conteneur : + +```bash +docker inspect --format '{{json .Config.Labels}}' | grep -o 'traefik[^"]*' +``` + +--- + +## HSTS — à traiter en dernier, et prudemment + +Le HSTS n'est **pas** émis par le conteneur. C'est un choix délibéré : le +conteneur est redéployé souvent, et un réglage irréversible n'a rien à faire +dans une couche jetable. + +C'est Traefik qui doit l'émettre, une fois le HTTPS confirmé fonctionnel. + +> **Pourquoi cette prudence.** `Strict-Transport-Security` avec +> `max-age=63072000` indique au navigateur de refuser toute connexion HTTP à ce +> domaine pendant deux ans. Si le certificat casse ensuite, le site devient +> inaccessible — et **tu ne peux rien faire côté serveur** pour l'annuler : la +> directive est mémorisée par le navigateur du visiteur. + +### Procédure recommandée + +**Phase 1 — validation.** Configure `max-age=300` (cinq minutes). En mode +Docker Compose, c'est déjà la valeur du fichier fourni. En mode Dockerfile, +ajoute dans **Custom Labels** : + +``` +traefik.http.middlewares.hsts-test.headers.stsSeconds=300 +traefik.http.middlewares.hsts-test.headers.forceSTSHeader=true +traefik.http.routers..middlewares=hsts-test +``` + +Vérifie sur plusieurs jours que le certificat se renouvelle bien. + +**Phase 2 — activation.** Passe `stsSeconds` à `63072000`. + +N'inscris le domaine sur la liste de préchargement HSTS (hstspreload.org) que +si tu es certain de ne jamais revenir en arrière : la radiation prend des mois. + +--- + +## Redéploiement + +### Automatique + +Dans **Configuration → General**, active **Auto Deploy**. Coolify crée un +webhook dans Gitea : chaque `git push` sur `main` déclenche un déploiement. + +C'est pertinent ici — un site statique sans test à faire tourner ne risque +rien à se redéployer automatiquement. + +### Manuel + +Bouton **Redeploy**. + +### Après une modification de contenu + +Pense à mettre à jour la date dans `sitemap.xml` : + +```xml +2026-08-05 +``` + +--- + +## Test local avant déploiement + +Si tu veux vérifier l'image sur ta machine : + +```bash +docker build -t courcellematthi . +docker run --rm -p 8080:80 courcellematthi +# puis http://localhost:8080 +``` + +Contrôles utiles : + +```bash +# Poids de l'image +docker images courcellematthi + +# Le dossier deploy/ ne doit PAS être dans l'image +docker run --rm courcellematthi ls /usr/share/nginx/html + +# La configuration Nginx est syntaxiquement correcte +docker run --rm courcellematthi nginx -t +``` + +--- + +## Une fois en ligne + +1. **Google Search Console** — ajoute la propriété et soumets + `https://www.courcellematthi.eu/sitemap.xml` +2. **Test des résultats enrichis** — vérifie le JSON-LD sur + `search.google.com/test/rich-results` +3. **Débogueur LinkedIn** — force le rafraîchissement de l'aperçu de partage + sur `linkedin.com/post-inspector` (LinkedIn met les aperçus en cache + longtemps : sans cette étape, un ancien aperçu peut persister des semaines) +4. **securityheaders.com** — la note attendue est A ou A+ +5. **PageSpeed Insights** — contrôle les Core Web Vitals + +--- + +## Si quelque chose ne va pas + +| Symptôme | Cause probable | +|---|---| +| `failed to read dockerfile: open Dockerfile: no such file or directory` avec `transferring dockerfile: 2B` | `Dockerfile` listé dans `.dockerignore` — voir ci-dessous | +| `failed to read dockerfile` sans la ligne `2B` | Fichier réellement absent du dépôt, ou mauvais **Dockerfile Location** dans Coolify | +| 404 Traefik, conteneur sain | Conteneur hors du réseau `coolify` | +| 502 Bad Gateway | Étiquette `loadbalancer.server.port` absente | +| Certificat non émis | DNS non propagé, ou proxy Cloudflare actif | +| Page blanche, CSS absent | Chemins `/assets/...` — vérifier que la racine servie est bien `/usr/share/nginx/html` | +| Polices non chargées | CSP trop stricte — `fonts.googleapis.com` et `fonts.gstatic.com` doivent être autorisés | +| Ancienne version affichée | Cache navigateur sur `index.html` — vérifier l'en-tête `Cache-Control: must-revalidate` | +| Boucle de redirection | Redirection HTTPS dupliquée : Traefik **et** Nginx. Le `deploy/nginx.conf` fourni n'en contient pas — vérifier qu'on n'a pas utilisé `nginx.conf.example` par erreur | + +### Le Dockerfile filtré par `.dockerignore` + +Journal caractéristique : + +``` +#1 transferring dockerfile: 2B done +ERROR: failed to solve: failed to read dockerfile: + open Dockerfile: no such file or directory +``` + +La ligne **`2B`** est le signal décisif : BuildKit a bien reçu un contexte, mais +le Dockerfile qu'il en extrait est vide. Le fichier existe donc dans le dépôt — +c'est `.dockerignore` qui l'a rendu invisible. + +BuildKit, utilisé par Coolify et par Docker depuis la version 23, applique les +exclusions **avant** de lire le Dockerfile. Le builder classique tolérait ce +cas ; BuildKit non. + +Vérification : + +```bash +grep -nE '^\s*(Dockerfile|\.dockerignore)\s*$' .dockerignore +``` + +Si l'une des deux lignes ressort, supprime-la et repousse. Ces fichiers n'ont +pas besoin d'y figurer : le `RUN rm` du Dockerfile les retire déjà de l'image +finale, donc ils ne sont jamais servis au public. + +### Vérifier que le fichier est bien dans le dépôt + +Si la ligne `2B` **n'apparaît pas**, le fichier est réellement absent : + +```bash +git ls-files | grep -E '^Dockerfile$' +``` + +Sans résultat, il n'a pas été commité : + +```bash +git add Dockerfile .dockerignore deploy/ +git commit -m "Ajout des fichiers de déploiement" +git push +``` + +Contrôle également, dans **Configuration → Build** de Coolify : + +- **Base Directory** : `/` +- **Dockerfile Location** : `/Dockerfile` + +Si le dépôt contient le site dans un sous-dossier, ces deux chemins doivent +être ajustés en conséquence. + +Journaux : + +```bash +docker logs -f # Conteneur du site +docker logs -f coolify-proxy # Traefik +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6c00022 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# ============================================================================= +# Dockerfile — Site statique courcellematthi.eu +# ----------------------------------------------------------------------------- +# Image mono-étape : le site n'a aucune dépendance et aucune compilation. +# Une étape de build (Node, npm install…) n'apporterait ici que du temps +# de déploiement et de la surface d'attaque en plus. +# +# Image finale : environ 50 Mo, dont 48 Mo pour Nginx Alpine. +# ============================================================================= + +# Version épinglée plutôt que « latest » : un redéploiement six mois plus tard +# doit produire exactement la même image. « latest » rend les builds +# non reproductibles et peut casser la production sans qu'on ait rien changé. +FROM nginx:1.27-alpine + +# ----------------------------------------------------------------------------- +# MÉTADONNÉES (spécification OCI) +# Visibles via « docker inspect », utiles pour l'inventaire et le débogage. +# ----------------------------------------------------------------------------- +LABEL org.opencontainers.image.title="courcellematthi.eu" +LABEL org.opencontainers.image.description="Site personnel et CV de Matthieu Courcelle" +LABEL org.opencontainers.image.authors="Matthieu Courcelle " +LABEL org.opencontainers.image.url="https://www.courcellematthi.eu" +LABEL org.opencontainers.image.licenses="UNLICENSED" + +# ----------------------------------------------------------------------------- +# CONFIGURATION NGINX +# On remplace le fichier par défaut du site, pas nginx.conf global : la +# configuration de base de l'image (workers, types MIME, limites) est saine. +# ----------------------------------------------------------------------------- +RUN rm /etc/nginx/conf.d/default.conf +COPY deploy/nginx.conf /etc/nginx/conf.d/site.conf + +# ----------------------------------------------------------------------------- +# CONTENU DU SITE +# .dockerignore exclut le superflu (README, configs alternatives, .git). +# ----------------------------------------------------------------------------- +COPY . /usr/share/nginx/html/ + +# Retire du conteneur les fichiers qui n'ont rien à y faire : documentation, +# configurations serveur alternatives, sources de déploiement. Ils sont utiles +# dans le dépôt, pas dans l'image servie au public. +RUN rm -rf /usr/share/nginx/html/deploy \ + /usr/share/nginx/html/Dockerfile \ + /usr/share/nginx/html/.dockerignore \ + /usr/share/nginx/html/docker-compose.yaml \ + /usr/share/nginx/html/README.md \ + /usr/share/nginx/html/DEPLOIEMENT-COOLIFY.md \ + /usr/share/nginx/html/nginx.conf.example \ + /usr/share/nginx/html/.htaccess \ + /usr/share/nginx/html/verifier-depot.sh + +# ----------------------------------------------------------------------------- +# PERMISSIONS +# Lecture seule pour tous, aucun fichier exécutable : rien dans un site +# statique n'a besoin du bit d'exécution, sauf les répertoires (parcours). +# ----------------------------------------------------------------------------- +RUN chmod -R a=r,u+w /usr/share/nginx/html && \ + find /usr/share/nginx/html -type d -exec chmod a+x {} \; + +# ----------------------------------------------------------------------------- +# RÉSEAU +# EXPOSE est purement déclaratif : il documente le port et permet à Coolify +# de le détecter automatiquement. Il n'ouvre rien par lui-même. +# ----------------------------------------------------------------------------- +EXPOSE 80 + +# ----------------------------------------------------------------------------- +# SONDE DE SANTÉ +# Coolify affiche l'état du conteneur et peut le redémarrer s'il ne répond +# plus. wget est présent dans l'image Alpine, inutile d'installer curl. +# ----------------------------------------------------------------------------- +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/healthz || exit 1 + +# L'image officielle définit déjà le bon CMD ; on le réécrit pour rendre +# explicite le fonctionnement au premier plan (daemon off), indispensable +# pour que Docker suive correctement le cycle de vie du processus. +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae75f86 --- /dev/null +++ b/README.md @@ -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 `` +dans le `` 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 ``. 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 + +` + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ profil + + +

Matthieu Courcelle

+ +

+ Professeur d'informatique & développeur d'applications de gestion +

+ + + + +
+ + + Me contacter + + + + +
+
+ + +
+

+ Enseigner, transmettre, construire des outils qui servent +

+ +

+ Professeur d'informatique depuis 2018, j'enseigne aujourd'hui à la fois dans le + secondaire technique (IPES Ath) et dans le supérieur pour adultes à l'Institut + Gabrielle Petit — développement d'applications, administration réseau, sécurisation + d'infrastructures. La transmission est au cœur de ce que je fais : structurer une + matière complexe, la rendre accessible, accompagner un public en formation jusqu'à + l'autonomie. +

+ +

+ En parallèle des cours, j'ai développé des outils concrets pour les + établissements où j'interviens — une application de gestion d'événements + scolaires, des systèmes d'aide à l'administration — et j'ai construit, en autodidacte, + une vraie maîtrise de l'intelligence artificielle appliquée : skills Claude et + assistants Gemini pour aider mes collègues à préparer cours et évaluations, pour + accompagner des directions d'école sur l'administratif, la communication avec les + familles ou le RGPD + scolaire. Je gère aussi ma propre infrastructure serveur (Docker, déploiement, + monitoring) pour héberger ces outils. +

+ +

+ Depuis janvier 2026, je développe également en indépendant complémentaire + (underscorelab) des outils de gestion pour des + ASBL, notamment dans le secteur + du handicap. +

+ +

+ Cette double compétence — pédagogue et développeur formé à l'IA appliquée — ouvre + plusieurs directions : continuer à enseigner et former, accompagner des équipes dans + l'intégration d'outils IA, ou concevoir des solutions sur mesure pour des structures + qui en ont besoin. +

+
+ + +
+

Enseignement

+ +
+ +

Professeur d'informatique — Enseignement supérieur pour adultes

+

Institut Gabrielle Petit (ex-IPEPS Tournai-Leuze)

+ +
+ Développement d'applications orientées gestion — Projet SGBD (Bac 3) +

+ Conception et développement d'un système de gestion de données complet — de la + modélisation (MCD/MLD, normalisation) à une application 3-tiers connectée en base, + intégrant transactions ACID, gestion d'erreurs, déploiement Cloud/DevOps + (Git, CI/CD) et soutenance technique devant jury. +

+
+ +
+ Administration et sécurisation des réseaux et services (Bac 3) +

+ Formation à l'administration autonome d'une infrastructure réseau complète : + services (DHCP, DNS, Web, messagerie), virtualisation, adressage IPv4/IPv6, + et sécurisation avancée (pare-feu, DMZ, IDS/IPS, Active Directory/LDAP, + VLAN, GPO, VPN). +

+
+ +
+ Base d'administration réseaux (Bac 1) +

+ Introduction aux modèles OSI/TCP-IP, à l'adressage IP et au subnetting, au câblage + et aux équipements actifs (switch, routeur), avec diagnostic autonome + (ping, traceroute, sniffers) et mise en œuvre d'un petit réseau interconnecté. +

+
+ +
+ Initiation à l'informatique (Complément CESS) +

+ Apprentissage des bases de l'outil informatique (Windows, navigation, bureautique) + pour un public adulte en reprise d'études. +

+
+
+ +
+ +

Professeur d'informatique — Secondaire technique

+

IPES Ath · Technique de transition & de qualification (DI et DS)

+ +
+ Traitements de Problèmes Techniques +

+ Apprentissage méthodique du diagnostic et de la résolution de pannes matérielles + et logicielles, pour développer une démarche rigoureuse de dépannage informatique. +

+
+ +
+ Laboratoire Informatique +

+ Mise en pratique des notions théoriques via des ateliers concrets de montage, + configuration et manipulation de systèmes informatiques. +

+
+ +
+ Informatique Appliquée +

+ Application des concepts informatiques à des cas pratiques, faisant le lien entre + théorie algorithmique/logicielle et mise en œuvre réelle. +

+
+ +
+ Laboratoire de Maintenance et de Télématique +

+ Travaux pratiques sur la maintenance des postes de travail et les bases des réseaux + et télécommunications. +

+
+ +
+ Informatique de Gestion (Office 365) +

+ Maîtrise des outils bureautiques et collaboratifs de gestion appliqués aux besoins + administratifs et professionnels. +

+
+
+ +
+ +

Professeur d'informatique de gestion

+

IPES Tournai · Technique de qualification Gestion (DI)

+

Informatique de gestion appliquée à la suite Office 365.

+
+ +
+ +

Formateur en entreprise

+

ORDITECH SA

+

+ Formation professionnelle d'adultes sur les environnements Office 365 et les + systèmes d'exploitation. +

+
+
+ + +
+

Coordination pédagogique

+ +
+ +

Développement d'outils de gestion interne

+

École des Frères — Tournai

+ +
    +
  • Application de gestion des remédiations : suivi des élèves, planification et affectation des séances.
  • +
  • Application de gestion des activités complémentaires.
  • +
  • Système de valve électronique pour la diffusion d'annonces aux élèves.
  • +
+
+
+ + +
+

Projets

+ +
+
+

Festifun

+ Institut du Sacré-Cœur Mouscron +
+

+ Application web progressive de gestion d'événement scolaire, conçue et déployée + pour l'établissement. +

+
    +
  • PWA
  • JavaScript
  • Node.js
  • +
+
+ +
+
+

Outils IA d'aide à la direction

+ Institut du Sacré-Cœur Mouscron +
+

+ Assistants IA sur mesure destinés aux équipes de direction : maîtrise des outils + informatiques administratifs, communication avec les familles, application du RGPD + en contexte scolaire. +

+
    +
  • Claude
  • Gemini
  • NotebookLM
  • +
+
+ +
+
+

Bibliothèque de skills Claude & Gems Gemini

+ Pédagogie · Administration +
+

+ Assistants IA spécialisés développés pour l'équipe éducative : rédaction de cours, + création d'exercices et d'exemples, conception d'évaluations et aide à la correction. +

+

+ Déclinaison à destination des directions d'établissement sur les volets + administratifs, la communication institutionnelle et la conformité RGPD. +

+
+ +
+
+

underscorelab

+ Indépendant complémentaire · depuis janvier 2026 +
+

+ Développement d'outils de gestion sur mesure pour le secteur associatif, + principalement des ASBL d'aide aux personnes en situation de handicap. +

+
    +
  • Node.js
  • PHP
  • Python
  • PostgreSQL
  • Vue
  • +
+
+ +
+
+

Écosystème d'agents automatisés

+ + en cours de développement +
+

+ Architecture d'agents IA orchestrés via n8n sur infrastructure auto-hébergée, + pour l'automatisation de flux de travail administratifs et pédagogiques. +

+
    +
  • n8n
  • Docker
  • API
  • +
+
+
+ + +
+

Compétences

+ +
+ +
+

+ + Développement +

+
    +
  • PHP, Python, JavaScript (Node.js)
  • +
  • React, Vue
  • +
  • PostgreSQL, SQL, modélisation MCD/MLD
  • +
  • Applications de gestion, PWA
  • +
  • VBA / Excel avancé
  • +
+
+ +
+

+ + Systèmes & réseaux +

+
    +
  • Administration et sécurisation réseau
  • +
  • Active Directory, LDAP, GPO
  • +
  • Pare-feu, DMZ, VPN, VLAN
  • +
  • Windows, Linux, macOS
  • +
  • Montage, configuration, audit
  • +
+
+ +
+

+ + Infrastructure & déploiement +

+
    +
  • Docker, conteneurisation
  • +
  • Reverse proxy, SSL/TLS
  • +
  • Déploiement continu (Git, CI/CD)
  • +
  • Monitoring et sauvegarde
  • +
  • Auto-hébergement de services
  • +
+
+ +
+

+ + Intelligence artificielle appliquée +

+
    +
  • Conception de skills Claude
  • +
  • Création de Gems Gemini
  • +
  • Structuration documentaire (NotebookLM)
  • +
  • Automatisation de flux de travail
  • +
  • Accompagnement et formation d'équipes
  • +
+
+ +
+

+ + Pédagogie +

+
    +
  • Enseignement secondaire technique
  • +
  • Enseignement supérieur pour adultes
  • +
  • Conception de séquences et d'évaluations
  • +
  • Vulgarisation technique
  • +
  • Accompagnement en reprise d'études
  • +
+
+ +
+

+ + Langues +

+
    +
  • Français — langue maternelle
  • +
  • Anglais — lecture technique courante
  • +
+
+ +
+
+ + +
+

Formation

+ +
+
+ +

Certificat d'Aptitudes Pédagogiques

+

Institut de Promotion Sociale Saint Henri

+
+ +
+ +

Bachelier en informatique de gestion

+

IEPSCF

+
+
+
+ + +
+

Contact

+ +
+

+ Une question, un projet, une opportunité d'enseignement ou de collaboration — + écrivez-moi, je réponds. +

+ +
+ + + m@tthieu.be + + + + +
+
+ + +
+
+ +
+
+ + + + + + + + + diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..5b266ed --- /dev/null +++ b/llms.txt @@ -0,0 +1,85 @@ +# Matthieu Courcelle + +> Professeur d'informatique en enseignement secondaire technique et en +> enseignement supérieur pour adultes à Tournai (Belgique, Fédération +> Wallonie-Bruxelles). Développeur d'applications de gestion et praticien +> de l'intelligence artificielle appliquée à la pédagogie et à +> l'administration scolaire. + +Ce fichier suit la proposition llms.txt : il fournit aux agents +conversationnels un résumé structuré et fiable du site, en texte brut, +sans avoir à interpréter la mise en page HTML. + +Contact : m@tthieu.be +Site : https://www.courcellematthi.eu/ +Localisation : région de Tournai, Hainaut, Belgique +Langues : français (maternelle), anglais (lecture technique) + +## Fonctions d'enseignement actuelles + +- **Institut Gabrielle Petit** (anciennement IPEPS Tournai-Leuze), depuis 2023 — + enseignement supérieur pour adultes. Cours : Développement d'applications + orientées gestion / Projet SGBD (Bac 3), Administration et sécurisation des + réseaux et services (Bac 3), Base d'administration réseaux (Bac 1), + Initiation à l'informatique (Complément CESS). + +- **IPES Ath**, depuis 2020 — enseignement secondaire technique de transition + et de qualification. Cours : Traitements de Problèmes Techniques, Laboratoire + Informatique, Informatique Appliquée, Laboratoire de Maintenance et de + Télématique, Informatique de Gestion. + +## Fonctions antérieures + +- **IPES Tournai**, 2020-2022 — professeur d'informatique de gestion. +- **ORDITECH SA**, 2018-2020 — formateur en entreprise (Office 365, systèmes + d'exploitation). + +## Coordination pédagogique + +- **École des Frères, Tournai** — développement d'outils de gestion interne dans + le cadre d'heures de coordination : gestion des remédiations, gestion des + activités complémentaires, valve électronique d'annonces. + +## Projets + +- **Festifun** — application web progressive de gestion d'événement scolaire + (Institut du Sacré-Cœur Mouscron). +- **Outils IA d'aide à la direction** — assistants sur mesure pour équipes de + direction : outils administratifs, communication avec les familles, RGPD + scolaire (Institut du Sacré-Cœur Mouscron). +- **Bibliothèque de skills Claude et Gems Gemini** — assistants IA pour la + rédaction de cours, d'exercices, d'évaluations et l'aide à la correction ; + déclinaison à destination des directions d'établissement. +- **underscorelab** — activité d'indépendant complémentaire lancée en janvier + 2026 : outils de gestion sur mesure pour ASBL, principalement dans le secteur + de l'aide aux personnes en situation de handicap. +- **Écosystème d'agents automatisés** — en cours de développement : agents IA + orchestrés via n8n sur infrastructure auto-hébergée. + +## Compétences techniques + +- Développement : PHP, Python, JavaScript, Node.js, React, Vue, PostgreSQL, + SQL, modélisation MCD/MLD, PWA, VBA/Excel. +- Systèmes et réseaux : administration et sécurisation réseau, Active Directory, + LDAP, GPO, pare-feu, DMZ, VPN, VLAN, Windows, Linux, macOS. +- Infrastructure : Docker, reverse proxy, SSL/TLS, Git, CI/CD, monitoring, + sauvegarde, auto-hébergement. +- Intelligence artificielle appliquée : conception de skills Claude, création de + Gems Gemini, structuration documentaire avec NotebookLM, automatisation de + flux de travail, formation d'équipes. +- Pédagogie : enseignement secondaire technique et supérieur pour adultes, + conception de séquences et d'évaluations, vulgarisation technique. + +## Formation + +- Certificat d'Aptitudes Pédagogiques — Institut de Promotion Sociale + Saint Henri (2021-2024). +- Bachelier en informatique de gestion — IEPSCF (2008-2011). + +## Notes à l'attention des agents + +- Le nom de l'établissement a changé : « IPEPS Tournai-Leuze » et « Institut + Gabrielle Petit » désignent la même structure. +- « underscorelab » s'écrit en minuscules, en un seul mot. +- Ne pas confondre le poste d'enseignant (fonction officielle) avec les projets + développés pour des établissements tiers (réalisations, sans titre officiel). diff --git a/nginx.conf.example b/nginx.conf.example new file mode 100644 index 0000000..87942d7 --- /dev/null +++ b/nginx.conf.example @@ -0,0 +1,212 @@ +# ============================================================================= +# nginx.conf.example — Configuration serveur pour courcellematthi.eu +# ----------------------------------------------------------------------------- +# À adapter puis inclure dans la configuration Nginx. +# Couvre : redirections canoniques, en-têtes de sécurité, cache, compression. +# +# Sans ces réglages, un site statique parfaitement écrit reste noté médiocre +# par les outils d'audit (Lighthouse, Mozilla Observatory, securityheaders.com) +# et sert des fichiers non compressés. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# REDIRECTION HTTP -> HTTPS +# Le certificat est supposé géré par Certbot ou acme.sh. +# ----------------------------------------------------------------------------- +server { + listen 80; + listen [::]:80; + server_name courcellematthi.eu www.courcellematthi.eu; + + # Laisse passer la validation ACME avant de rediriger + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://www.courcellematthi.eu$request_uri; + } +} + +# ----------------------------------------------------------------------------- +# REDIRECTION DU DOMAINE NU VERS WWW +# Une seule URL canonique évite la dilution du référencement et le contenu +# dupliqué. Le choix www / sans-www est indifférent, la constance ne l'est pas. +# La valeur retenue ici doit correspondre à dans le HTML. +# ----------------------------------------------------------------------------- +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name courcellematthi.eu; + + ssl_certificate /etc/letsencrypt/live/courcellematthi.eu/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/courcellematthi.eu/privkey.pem; + + return 301 https://www.courcellematthi.eu$request_uri; +} + +# ----------------------------------------------------------------------------- +# SERVEUR PRINCIPAL +# ----------------------------------------------------------------------------- +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_name www.courcellematthi.eu; + + root /var/www/courcellematthi.eu; + index index.html; + charset utf-8; + + # ------------------------------------------------------------------------- + # TLS + # TLS 1.2 minimum : les versions antérieures sont obsolètes et vulnérables. + # ------------------------------------------------------------------------- + ssl_certificate /etc/letsencrypt/live/courcellematthi.eu/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/courcellematthi.eu/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; # Laisse le client choisir : recommandé + # depuis TLS 1.3 + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + # Agrafage OCSP : le serveur fournit lui-même la preuve de non-révocation + # du certificat, ce qui évite au navigateur une requête vers l'autorité. + ssl_stapling on; + ssl_stapling_verify on; + resolver 1.1.1.1 8.8.8.8 valid=300s; + + # ========================================================================= + # EN-TÊTES DE SÉCURITÉ + # ========================================================================= + + # HSTS — force HTTPS pendant deux ans, y compris sur les sous-domaines. + # ATTENTION : ne l'activer qu'une fois le HTTPS confirmé fonctionnel + # partout. Un HSTS émis par erreur rend le site inaccessible jusqu'à + # expiration, sans possibilité d'annulation côté serveur. + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + + # CSP — liste blanche des sources autorisées. Défense principale contre + # l'injection de scripts. + # default-src 'self' : tout provient du domaine par défaut + # style-src ... 'unsafe-inline' : requis par le CSS injecté de Google Fonts + # font-src fonts.gstatic.com : fichiers de police + # script-src 'self' 'unsafe-inline' : 'unsafe-inline' n'est nécessaire + # que pour l'attribut onclick du bouton d'impression. Le retirer si + # ce bouton est déplacé dans un module. + # frame-ancestors 'none' : interdit l'inclusion en iframe (clickjacking) + # base-uri 'self' : empêche la réécriture des URL relatives + # form-action 'self' : limite les cibles de soumission + add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; upgrade-insecure-requests" always; + + # Empêche le navigateur de deviner le type MIME d'un fichier, technique + # utilisée pour faire exécuter une image comme du script. + add_header X-Content-Type-Options "nosniff" always; + + # Ne transmet le référent complet qu'en navigation interne ; vers + # l'extérieur, seul le domaine est envoyé. Compromis usuel entre mesure + # d'audience et confidentialité. + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Désactive des API sensibles jamais utilisées par ce site. + add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=(), interest-cohort=()" always; + + # Redondant avec frame-ancestors, conservé pour les navigateurs anciens. + add_header X-Frame-Options "DENY" always; + + # ========================================================================= + # COMPRESSION + # Gzip partout ; Brotli en plus si le module est disponible (taux + # supérieur de 15 à 20 % sur du texte). + # ========================================================================= + gzip on; + gzip_vary on; # Ajoute « Vary: Accept-Encoding » : sans lui, + # un proxy peut servir la version compressée + # à un client qui ne la supporte pas. + gzip_comp_level 6; # Au-delà, le gain marginal ne compense plus + # le coût CPU. + gzip_min_length 256; # En dessous, l'en-tête coûte plus que le gain. + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/json + application/manifest+json + application/xml + image/svg+xml; + + # Décommenter si le module ngx_brotli est compilé + # brotli on; + # brotli_comp_level 6; + # brotli_types text/plain text/css application/javascript application/json image/svg+xml; + + # ========================================================================= + # CACHE + # Stratégie en deux niveaux : + # - Les ressources d'assets changent rarement : cache long. + # - Le HTML change à chaque mise à jour de contenu : jamais mis en cache + # durablement, sinon les visiteurs voient une version périmée. + # + # NOTE : « immutable » suppose des noms de fichiers versionnés + # (style.a1b2c3.css). Sans versionnement, garder une durée modérée + # pour pouvoir publier un correctif. + # ========================================================================= + + location ~* \.(css|js|woff2?|ttf|otf|eot)$ { + expires 30d; + add_header Cache-Control "public, max-age=2592000"; + access_log off; + } + + location ~* \.(png|jpe?g|gif|svg|webp|avif|ico)$ { + expires 90d; + add_header Cache-Control "public, max-age=7776000"; + access_log off; + } + + location = /index.html { + add_header Cache-Control "public, max-age=0, must-revalidate"; + } + + location ~* \.(txt|xml|webmanifest)$ { + expires 1d; + add_header Cache-Control "public, max-age=86400"; + } + + # ========================================================================= + # TYPES MIME PARTICULIERS + # ========================================================================= + location = /site.webmanifest { + types { } default_type application/manifest+json; + } + + location = /llms.txt { + types { } default_type text/plain; + charset utf-8; + } + + location = /.well-known/security.txt { + types { } default_type text/plain; + charset utf-8; + } + + # ========================================================================= + # ROUTAGE + # ========================================================================= + location / { + try_files $uri $uri/ /index.html; + } + + # Refuse l'accès aux fichiers cachés, sauf .well-known (RFC 8615) + location ~ /\.(?!well-known) { + deny all; + return 404; + } + + # Page d'erreur : renvoyer vers l'accueil plutôt qu'une page vide + error_page 404 /index.html; +} diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..bcdc673 --- /dev/null +++ b/robots.txt @@ -0,0 +1,50 @@ +# ============================================================================= +# robots.txt — courcellematthi.eu +# ============================================================================= +# Site personnel entièrement public : rien à masquer, tout à indexer. +# Un robots.txt permissif mais explicite vaut mieux qu'un fichier absent : +# il évite les 404 répétées dans les journaux et déclare le sitemap. +# ============================================================================= + +# --- Robots d'indexation classiques ----------------------------------------- +User-agent: * +Allow: / + +# --- Robots des moteurs de réponse et agents IA ------------------------------ +# Autorisés explicitement : sur un CV, être cité par un agent conversationnel +# lors d'une recherche de profil est un bénéfice, pas une fuite. +# Retirer ces blocs (ou les passer en Disallow) si l'arbitrage change. +User-agent: GPTBot +Allow: / + +User-agent: OAI-SearchBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: Claude-Web +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: Applebot-Extended +Allow: / + +# --- Aspirateurs de site ---------------------------------------------------- +# Bloqués : consommation de bande passante sans contrepartie. +User-agent: HTTrack +Disallow: / + +User-agent: WebZIP +Disallow: / + +# --- Plan du site ----------------------------------------------------------- +Sitemap: https://www.courcellematthi.eu/sitemap.xml diff --git a/site.webmanifest b/site.webmanifest new file mode 100644 index 0000000..41353b8 --- /dev/null +++ b/site.webmanifest @@ -0,0 +1,40 @@ +{ + "$comment": "Manifeste d'application web. Permet l'ajout à l'écran d'accueil sur mobile et fournit les icônes aux systèmes d'exploitation. display=browser : ce site est un document, pas une application — le masquer dans une coque sans barre d'adresse priverait l'utilisateur du partage et de la navigation.", + "name": "Matthieu Courcelle — Professeur d'informatique et développeur", + "short_name": "M. Courcelle", + "description": "Professeur d'informatique en secondaire technique et en supérieur pour adultes, développeur d'applications de gestion.", + "lang": "fr-BE", + "dir": "ltr", + "start_url": "/", + "scope": "/", + "display": "browser", + "background_color": "#f7f8f8", + "theme_color": "#f7f8f8", + "categories": ["education", "business"], + "icons": [ + { + "src": "/favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/assets/img/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/img/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/img/icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..3d56c42 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,17 @@ + + + + + https://www.courcellematthi.eu/ + 2026-08-04 + monthly + 1.0 + + + + diff --git a/verifier-depot.sh b/verifier-depot.sh new file mode 100644 index 0000000..e6195cb --- /dev/null +++ b/verifier-depot.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# ============================================================================= +# verifier-depot.sh — Contrôle avant push +# ----------------------------------------------------------------------------- +# Vérifie que tous les fichiers nécessaires au build Docker sont bien suivis +# par Git. Les fichiers cachés (.dockerignore, .well-known/) sont ceux qui +# disparaissent le plus facilement lors d'une copie manuelle ou d'une +# décompression. +# +# Usage : bash verifier-depot.sh +# ============================================================================= + +set -u + +REQUIS=( + # --- Build --- + "Dockerfile" + ".dockerignore" + "deploy/nginx.conf" + # --- Site --- + "index.html" + "favicon.svg" + "favicon.ico" + "robots.txt" + "sitemap.xml" + "llms.txt" + "humans.txt" + "site.webmanifest" + ".well-known/security.txt" + # --- Styles --- + "assets/css/tokens.css" + "assets/css/base.css" + "assets/css/layout.css" + "assets/css/components.css" + "assets/css/utilities.css" + "assets/css/print.css" + # --- Scripts --- + "assets/js/main.js" + "assets/js/modules/navigation.js" + "assets/js/modules/scroll-spy.js" + "assets/js/modules/back-to-top.js" + "assets/js/modules/copy-email.js" + # --- Images --- + "assets/img/og-image.png" + "assets/img/apple-touch-icon.png" + "assets/img/icon-192.png" + "assets/img/icon-512.png" + "assets/img/icon-maskable-512.png" + "assets/img/safari-pinned-tab.svg" + "assets/img/favicon-16x16.png" + "assets/img/favicon-32x32.png" + "assets/img/favicon-48x48.png" +) + +echo "== Fichiers suivis par Git ==" +manquants=0 +for f in "${REQUIS[@]}"; do + if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then + printf " OK %s\n" "$f" + else + printf " MANQUANT %s\n" "$f" + manquants=$((manquants + 1)) + fi +done + +echo +echo "== Pièges connus ==" + +# Le Dockerfile ne doit jamais être exclu : BuildKit applique .dockerignore +# AVANT de lire le Dockerfile, contrairement au builder classique. +if grep -qE '^[[:space:]]*(Dockerfile|\.dockerignore)[[:space:]]*$' .dockerignore 2>/dev/null; then + echo " ERREUR .dockerignore exclut Dockerfile ou .dockerignore" + echo " -> le build echouera avec « transferring dockerfile: 2B »" + manquants=$((manquants + 1)) +else + echo " OK .dockerignore n'exclut pas le Dockerfile" +fi + +# deploy/ doit rester dans le contexte : le COPY du Dockerfile en depend. +if grep -qE '^[[:space:]]*deploy' .dockerignore 2>/dev/null; then + echo " ERREUR .dockerignore exclut deploy/" + echo " -> COPY deploy/nginx.conf echouera" + manquants=$((manquants + 1)) +else + echo " OK deploy/ reste dans le contexte de build" +fi + +echo +if [ "$manquants" -eq 0 ]; then + echo "Tout est en ordre. Le push peut etre effectue." + exit 0 +else + echo "$manquants probleme(s). Corriger avant de pousser :" + echo " git add -A && git commit -m 'Ajout des fichiers manquants' && git push" + exit 1 +fi