From 58bcc189ab6b13cfc51702ef7f6fbb63c8a05094 Mon Sep 17 00:00:00 2001 From: julien Date: Wed, 30 Sep 2026 10:30:36 +0200 Subject: [PATCH] Nouvelle version de Zola et petites corrections --- .env.example | 1 + .gitignore | 2 ++ Caddyfile.example | 3 ++ Dockerfile | 12 ++++---- GUIDE_REDACTION.md | 11 +++---- README.md | 30 ++++++++++++++----- compose.yml | 4 +-- scripts/check-content.py | 46 ++++++++++++++++++++++++++++++ src/templates/taxonomy_single.html | 5 ++-- 9 files changed, 92 insertions(+), 22 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a670517 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +FRONTEND_PORT=8002 diff --git a/.gitignore b/.gitignore index 983ce89..751f0eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.env + # Site généré par Zola src/public/ diff --git a/Caddyfile.example b/Caddyfile.example index d02afac..1c10012 100644 --- a/Caddyfile.example +++ b/Caddyfile.example @@ -1,4 +1,6 @@ lescarnetsdakkadien.netig.net { + encode zstd gzip + @assets path /main.css /giallo.css /images/* /fonts/* @documents { not path /main.css /giallo.css /images/* /fonts/* @@ -7,6 +9,7 @@ lescarnetsdakkadien.netig.net { header { Content-Security-Policy "default-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'" Referrer-Policy "strict-origin-when-cross-origin" + Permissions-Policy "camera=(), geolocation=(), microphone=()" Strict-Transport-Security "max-age=31536000" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" diff --git a/Dockerfile b/Dockerfile index e71d7e0..71f743a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,13 +4,13 @@ COPY scripts/check-content.py scripts/_schema.py ./scripts/ COPY src ./src RUN python3 scripts/check-content.py -FROM ghcr.io/getzola/zola:v0.23.1 AS builder -WORKDIR /project -COPY --from=validator /project/src ./ -RUN ["/zola", "build"] +FROM ghcr.io/getzola/zola:v0.23.6 AS build +WORKDIR /site +COPY --from=validator /project/src /site +RUN ["zola", "build", "--output-dir", "/public"] -FROM ghcr.io/static-web-server/static-web-server:2 +FROM ghcr.io/static-web-server/static-web-server:2.44.0 +COPY --from=build /public /public ENV SERVER_ROOT=/public \ SERVER_CACHE_CONTROL_HEADERS=false \ SERVER_SECURITY_HEADERS=false -COPY --from=builder /project/public /public diff --git a/GUIDE_REDACTION.md b/GUIDE_REDACTION.md index 4685721..31b223f 100644 --- a/GUIDE_REDACTION.md +++ b/GUIDE_REDACTION.md @@ -29,7 +29,7 @@ Les commandes publiques sont : - `new-content.py` : création d’un brouillon ; - `update-content.py` : mise à jour de l’horodatage `updated` ; -- `check-content.py` : validation structurelle des fiches. +- `check-content.py` : validation des noms et de la structure des fiches. `_schema.py` est leur module commun et ne se lance pas directement. @@ -424,6 +424,9 @@ Zola génère `giallo.css`, tandis que le HTML rendu utilise les classes corresp Pour chaque fiche, il vérifie : +- le nom de fichier en Unicode NFC ; +- les identifiants en minuscules avec lettres Unicode, chiffres et tirets ; +- la forme `mzl-NNN.md` pour les signes ; - la lecture en UTF-8 ; - la présence d’un front matter délimité par `+++` ; - la validité du TOML ; @@ -453,11 +456,9 @@ Le validateur accepte les chaînes et tableaux vides. Il ne contrôle pas : - la qualité ou l’exactitude linguistique des valeurs ; - les champs supplémentaires ; -- la forme des noms de fichiers ; -- la correspondance entre le numéro MZL et le nom du fichier ; -- la normalisation Unicode des fichiers existants. +- la correspondance entre le numéro MZL du front matter et le nom du fichier. -Zola reste responsable des liens, taxonomies et templates. Le générateur reste responsable de produire des noms de fichiers valides et des squelettes complets. +Zola reste responsable des liens, taxonomies et templates. Le générateur et le validateur appliquent les mêmes conventions de nommage ; le générateur reste responsable de produire des squelettes complets. ## Vérification avant publication diff --git a/README.md b/README.md index 9ed4c88..9bc9dea 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Site public : -Le site est généré avec **Zola 0.23.1** et **Tera 2** à partir de fichiers Markdown. Il est entièrement statique : aucune base de données, aucune interface d’administration et aucune exécution côté serveur ne sont nécessaires. +Le site est généré avec **Zola 0.23.6** et **Tera 2** à partir de fichiers Markdown. Il est entièrement statique : aucune base de données, aucune interface d’administration et aucun code applicatif côté serveur ne sont nécessaires. ## Contenu @@ -21,7 +21,7 @@ La page d’accueil présente le dernier article et les trois fiches les plus r Pour le développement local : - Python 3.11 ou ultérieur ; -- Zola 0.23.1 ; +- Zola 0.23.6 ; - Git, si le dépôt est versionné avec Git. Pour le déploiement décrit ici : @@ -83,7 +83,7 @@ Les contenus sont stockés dans `src/content/`. Trois commandes accompagnent leu - `new-content.py` crée un brouillon complet pour une section donnée ; - `update-content.py` actualise son horodatage `updated` ; -- `check-content.py` vérifie la présence et le type des champs prévus. +- `check-content.py` vérifie les noms de fichiers ainsi que la présence et le type des champs prévus. Le module `_schema.py` centralise les modèles utilisés par le générateur et le validateur ; il n’est pas exécuté directement. @@ -93,6 +93,7 @@ Les commandes, conventions et modèles de front matter sont détaillés dans le ```text . +├── .env.example ├── Caddyfile.example ├── Dockerfile ├── compose.yml @@ -128,8 +129,20 @@ Les commandes, conventions et modèles de front matter sont détaillés dans le L’image est construite en trois étapes : 1. validation des fiches avec Python ; -2. génération du site avec Zola 0.23.1 ; -3. copie des seuls fichiers publics dans un serveur HTTP statique. +2. génération du site avec Zola 0.23.6 ; +3. copie des seuls fichiers publics dans static-web-server 2.44.0. + +Créer la configuration locale à partir de l’exemple : + +```sh +cp .env.example .env +``` + +Le seul réglage nécessaire est le port HTTP publié sur l’hôte : + +```dotenv +FRONTEND_PORT=8002 +``` Vérifier puis démarrer le service : @@ -138,12 +151,14 @@ docker compose config docker compose up -d --build ``` -Le conteneur est exposé uniquement sur l’interface locale de l’hôte : +Par défaut, le conteneur est exposé uniquement sur l’interface locale de l’hôte : ```text http://127.0.0.1:8002 ``` +Le port peut être changé dans `.env`. Si Caddy est utilisé, adapter alors aussi le port de `reverse_proxy` dans `Caddyfile.example` ou dans la configuration de l’hôte. + Pour mettre à jour le déploiement : ```sh @@ -163,10 +178,11 @@ Caddy prend en charge le domaine public, HTTPS, les en-têtes de sécurité et l [`Caddyfile.example`](Caddyfile.example) fournit le bloc correspondant au port publié par `compose.yml`. Il applique : +- la compression `zstd` ou `gzip` selon le client ; - `Cache-Control: no-cache` aux documents ; - un cache navigateur d’une journée aux feuilles de style, images et polices ; - une CSP compatible avec les ressources locales du site ; -- HSTS, `Referrer-Policy`, `X-Content-Type-Options` et `X-Frame-Options`. +- HSTS, `Referrer-Policy`, `Permissions-Policy`, `X-Content-Type-Options` et `X-Frame-Options`. Le fichier peut être importé au niveau principal du Caddyfile de l’hôte ou servir de modèle pour un bloc existant. Sa syntaxe peut être contrôlée avec : diff --git a/compose.yml b/compose.yml index 114e216..0af7fc9 100644 --- a/compose.yml +++ b/compose.yml @@ -3,6 +3,6 @@ name: les-carnets-d-akkadien services: zola: build: . - ports: - - "127.0.0.1:8002:80" restart: unless-stopped + ports: + - "127.0.0.1:${FRONTEND_PORT:-8002}:80" diff --git a/scripts/check-content.py b/scripts/check-content.py index ef99307..a6a8cbb 100755 --- a/scripts/check-content.py +++ b/scripts/check-content.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 """Vérifie que chaque fiche contient les champs prévus pour son modèle.""" +import re import sys import tomllib +import unicodedata from datetime import datetime from pathlib import Path from typing import Any @@ -25,6 +27,8 @@ COMMON_FIELDS = { "draft": bool, } +MZL_FILENAME_RE = re.compile(r"mzl-([0-9]{3})") + class Validator: def __init__(self) -> None: @@ -68,6 +72,46 @@ def read_front_matter( return None +def check_filename( + path: Path, + section: str, + validator: Validator, +) -> None: + stem = path.stem + normalized = unicodedata.normalize("NFC", stem) + if stem != normalized: + validator.error(path, "nom de fichier non normalisé en Unicode NFC") + + if section == "signes": + match = MZL_FILENAME_RE.fullmatch(stem) + if match is None or not 1 <= int(match.group(1)) <= 999: + validator.error(path, "un signe doit être nommé mzl-NNN.md (001 à 999)") + return + + allow_leading_hyphen = section == "vocabulaire" + body = stem[1:] if stem.startswith("-") else stem + if stem.startswith("-") and not allow_leading_hyphen: + validator.error( + path, + "seul le vocabulaire accepte un nom commençant par un tiret", + ) + return + + if ( + not body + or stem in {".", "..", "_index"} + or stem != stem.lower() + or stem.endswith("-") + or "--" in stem + or not all(character.isalnum() or character == "-" for character in body) + ): + validator.error( + path, + "nom de fichier invalide : utiliser des lettres Unicode minuscules, " + "des chiffres et des tirets", + ) + + def check_type( value: Any, expected_type: type, @@ -189,6 +233,8 @@ def main() -> int: validator.error(path, f"section inconnue : {section}") continue + check_filename(path, section, validator) + result = read_front_matter(path, validator) if result is None: continue diff --git a/src/templates/taxonomy_single.html b/src/templates/taxonomy_single.html index 7975af3..a3782d4 100644 --- a/src/templates/taxonomy_single.html +++ b/src/templates/taxonomy_single.html @@ -1,7 +1,8 @@ {% extends "base.html" %} -{% set term_label = term.name | replace(from="-", to=" ") if taxonomy.name != "lectures" else term.name %} -{% block title %}{% if taxonomy.name == "natures" %}Mots : {{ term_label }}{% elif taxonomy.name == "lectures" %}Lecture « {{ term_label }} »{% elif taxonomy.name == "genres" %}Textes : {{ term_label }}{% elif taxonomy.name == "themes" %}Grammaire : {{ term_label }}{% else %}{{ term_label }}{% endif %} · {{ config.title }}{% endblock title %} +{% block title %} +{% set term_label = term.name | replace(from="-", to=" ") if taxonomy.name != "lectures" else term.name %}{% if taxonomy.name == "natures" %}Mots : {{ term_label }}{% elif taxonomy.name == "lectures" %}Lecture « {{ term_label }} »{% elif taxonomy.name == "genres" %}Textes : {{ term_label }}{% elif taxonomy.name == "themes" %}Grammaire : {{ term_label }}{% else %}{{ term_label }}{% endif %} · {{ config.title }}{% endblock title %} {% block content %} +{% set term_label = term.name | replace(from="-", to=" ") if taxonomy.name != "lectures" else term.name %} {% set heading = term_label %}{% set description = "Entrées classées dans cet index." %}{% set back_url = get_url(path=taxonomy.name, trailing_slash=true) %}{% set back_label = "Retour à l’index" %} {% if taxonomy.name == "natures" %} {% set heading = "Mots : " ~ term_label %}{% set description = "Fiches de vocabulaire classées comme « " ~ term_label ~ " »." %}{% set back_label = "Toutes les natures grammaticales" %}