Merge pull request 'Publier aussi l'inventaire complet des pages' (#10) from flux-toutes-les-pages into main

Reviewed-on: #10
This commit is contained in:
Cédric A5L 2026-09-23 11:22:42 +00:00
commit 99fde2aef2
2 changed files with 97 additions and 44 deletions

View file

@ -181,14 +181,20 @@ Une exécution réussie encadre le build par une ligne `Deploy started` et une l
--- ---
## Ce que le build publie pour la page d'accueil ## Ce que le build publie pour alpinux.org
Chaque build écrit, à la racine du site, un fichier **`derniers-articles.json`** : les Chaque build écrit deux fichiers à la racine du site, avec pour chaque page son titre,
vingt dernières pages créées ou modifiées, avec leur titre, leur adresse et la date du son adresse et la date du commit qui a touché son contenu :
commit qui a touché leur contenu. C'est ce fichier que lit la page d'accueil
d'alpinux.org pour afficher « Le wiki, fraîchement mis à jour ».
Il est produit par le hook `hooks/derniers_articles.py`, déclaré dans `mkdocs.yml`. | Fichier | Ce qu'il porte | Qui le lit |
|---|---|---|
| `derniers-articles.json` | Les vingt dernières pages créées ou modifiées | L'accueil d'alpinux.org, section « Le wiki, fraîchement mis à jour » |
| `toutes-les-pages.json` | Toutes les pages, sans limite, avec leur rubrique | La page des mises à jour d'alpinux.org |
Le second est la liste entière, le premier son début : un seul parcours de l'historique
git les produit tous les deux, et ils portent la même date de génération.
Ils sont produits par le hook `hooks/derniers_articles.py`, déclaré dans `mkdocs.yml`.
Trois choses à savoir : Trois choses à savoir :
- **Un déplacement n'est pas une mise à jour.** Les dates viennent de `git log` avec - **Un déplacement n'est pas une mise à jour.** Les dates viennent de `git log` avec
@ -198,14 +204,15 @@ Trois choses à savoir :
typiquement la redirection laissée derrière un article déplacé — n'apparaît jamais typiquement la redirection laissée derrière un article déplacé — n'apparaît jamais
dans la liste. dans la liste.
- **Le hook n'échoue jamais.** Sans historique git, il n'écrit rien et le build - **Le hook n'échoue jamais.** Sans historique git, il n'écrit rien et le build
continue ; l'accueil d'alpinux.org masque alors simplement sa section wiki. Ses continue ; alpinux.org masque alors simplement ce qu'il ne peut pas remplir. Ses
messages sont pour cela de niveau *info* et jamais *warning* : le déploiement messages sont pour cela de niveau *info* et jamais *warning* : le déploiement
construit avec `--strict`, où un seul avertissement suffit à interrompre le build, construit avec `--strict`, où un seul avertissement suffit à interrompre le build,
et donc la publication. et donc la publication.
Le même hook pose l'en-tête `Access-Control-Allow-Origin` sur ce seul fichier, au Le même hook pose l'en-tête `Access-Control-Allow-Origin` sur ces deux fichiers, et
moyen d'un `.htaccess` déposé à la racine du site construit. Sans lui, le navigateur sur eux seuls, au moyen d'un `.htaccess` déposé à la racine du site construit. Sans
refuserait à alpinux.org le droit de lire une réponse venue de wiki.alpinux.org. lui, le navigateur refuserait à alpinux.org le droit de lire une réponse venue de
wiki.alpinux.org.
!!! warning "Deux conditions" !!! warning "Deux conditions"
**Le clone doit avoir son historique.** Un clone `--depth 1` ne porte qu'un **Le clone doit avoir son historique.** Un clone `--depth 1` ne porte qu'un
@ -220,6 +227,7 @@ refuserait à alpinux.org le droit de lire une réponse venue de wiki.alpinux.or
```bash ```bash
# vérifier ce que le dernier build a publié # vérifier ce que le dernier build a publié
curl -s https://wiki.alpinux.org/derniers-articles.json | head -20 curl -s https://wiki.alpinux.org/derniers-articles.json | head -20
curl -s https://wiki.alpinux.org/toutes-les-pages.json | head -20
``` ```
--- ---
@ -256,4 +264,5 @@ Ouvrez [http://localhost:8000](http://localhost:8000) — MkDocs recharge automa
| Déployer à la main (webhook en panne) | `deploy-wiki.sh`, sur le serveur | | Déployer à la main (webhook en panne) | `deploy-wiki.sh`, sur le serveur |
| Voir le journal des déploiements | `tail -20 /var/log/wiki-deploy.log` | | Voir le journal des déploiements | `tail -20 /var/log/wiki-deploy.log` |
| Vérifier la liste envoyée à l'accueil | `curl -s https://wiki.alpinux.org/derniers-articles.json` | | Vérifier la liste envoyée à l'accueil | `curl -s https://wiki.alpinux.org/derniers-articles.json` |
| Vérifier l'inventaire complet publié | `curl -s https://wiki.alpinux.org/toutes-les-pages.json` |
| Régénérer les fichiers du logo (poste local) | `python3 scripts/build-assets.py` | | Régénérer les fichiers du logo (poste local) | `python3 scripts/build-assets.py` |

View file

@ -1,8 +1,15 @@
"""Publie `derniers-articles.json` : ce que le wiki a écrit ou repris récemment. """Publie ce que le wiki a écrit ou repris, pour qui l'affiche ailleurs.
La page d'accueil d'alpinux.org lit ce fichier pour afficher « Le wiki, fraîchement Deux fichiers, écrits à chaque build à côté des pages et servis avec l'en-tête CORS
mis à jour ». Il est donc écrit à chaque build, à côté des pages, et servi avec sans lequel un autre domaine ne peut pas les lire :
l'en-tête CORS sans lequel un autre domaine ne peut pas le lire.
- `derniers-articles.json` — les vingt dernières pages touchées, que la page d'accueil
d'alpinux.org lit pour sa section « Le wiki, fraîchement mis à jour » ;
- `toutes-les-pages.json` — le même inventaire, sans limite de nombre et avec la
rubrique de chaque page, pour la page des mises à jour d'alpinux.org.
Les deux sortent du même calcul : le second est la liste entière, le premier son
début.
Les dates viennent de git, pas du système de fichiers : un `git clone` repose tous les Les dates viennent de git, pas du système de fichiers : un `git clone` repose tous les
fichiers à la même seconde, et une page recopiée depuis Obsidian aurait l'air neuve. fichiers à la même seconde, et une page recopiée depuis Obsidian aurait l'air neuve.
@ -20,10 +27,11 @@ from urllib.parse import urljoin
from mkdocs.plugins import log from mkdocs.plugins import log
FICHIER = "derniers-articles.json" FICHIER_RECENTS = "derniers-articles.json"
FICHIER_TOUTES = "toutes-les-pages.json"
# Ce que le fichier porte au plus. L'accueil n'en affiche que six, mais le fichier # Ce que le premier fichier porte au plus. L'accueil n'en affiche que six, mais le
# sert aussi à qui voudrait en faire autre chose. # fichier sert aussi à qui voudrait en faire autre chose. Le second ne plafonne pas.
NB_MAX = 20 NB_MAX = 20
# En deçà, un article est annoncé comme « publié » plutôt que « mis à jour ». # En deçà, un article est annoncé comme « publié » plutôt que « mis à jour ».
@ -34,10 +42,24 @@ EXCLUS = {"index.md"}
_pages: dict[str, dict[str, str]] = {} _pages: dict[str, dict[str, str]] = {}
_dans_la_nav: set[str] = set() _dans_la_nav: set[str] = set()
_rubriques: dict[str, str] = {}
def _rubrique_par_page(items, rubrique=""):
"""Associe chaque page de la navigation au titre de la section qui la porte.
On garde la section de premier niveau — « Guides », « Contribuer » — plutôt que la
plus proche : c'est l'échelle à laquelle un lecteur range les pages.
"""
for item in items:
if item.is_section:
yield from _rubrique_par_page(item.children, rubrique or item.title)
elif item.is_page:
yield item.file.src_uri, rubrique
def on_nav(nav, config, files): def on_nav(nav, config, files):
"""Retient les pages de la navigation. """Retient les pages de la navigation, et sous quelle rubrique elles vivent.
Ce qui en est absent — les redirections laissées derrière un article déplacé, par Ce qui en est absent — les redirections laissées derrière un article déplacé, par
exemple — n'a pas à être annoncé comme une nouveauté. exemple — n'a pas à être annoncé comme une nouveauté.
@ -45,6 +67,9 @@ def on_nav(nav, config, files):
_dans_la_nav.clear() _dans_la_nav.clear()
_dans_la_nav.update(page.file.src_uri for page in nav.pages) _dans_la_nav.update(page.file.src_uri for page in nav.pages)
_rubriques.clear()
_rubriques.update(_rubrique_par_page(nav.items))
def on_page_context(context, page, config, nav): def on_page_context(context, page, config, nav):
"""Retient le titre et l'URL de chaque page rendue.""" """Retient le titre et l'URL de chaque page rendue."""
@ -106,24 +131,30 @@ def _historique(depot: Path, dossier: str) -> tuple[dict, dict]:
return modifiees, creees return modifiees, creees
def _autoriser_la_lecture_croisee(site_dir: Path) -> None: def _autoriser_la_lecture_croisee(site_dir: Path, fichiers: list[str]) -> None:
"""Pose l'en-tête CORS sur le seul fichier qui en a besoin. """Pose l'en-tête CORS sur les seuls fichiers qui en ont besoin.
Le fichier est lu depuis alpinux.org : sans cet en-tête, le navigateur refuse la Ils sont lus depuis alpinux.org : sans cet en-tête, le navigateur refuse la
réponse et la section reste masquée. Le bloc s'ajoute à un `.htaccess` déjà là réponse et la section reste masquée. Le bloc s'ajoute à un `.htaccess` déjà là
plutôt que de le remplacer — on ne sait pas ce qu'il porte d'autre. plutôt que de le remplacer — on ne sait pas ce qu'il porte d'autre — et n'est
écrit que pour les fichiers qui n'y figurent pas encore.
""" """
bloc = (
"<IfModule mod_headers.c>\n"
f' <Files "{FICHIER}">\n'
' Header set Access-Control-Allow-Origin "*"\n'
" </Files>\n"
"</IfModule>\n"
)
fichier = site_dir / ".htaccess" fichier = site_dir / ".htaccess"
existant = fichier.read_text(encoding="utf-8") if fichier.exists() else "" existant = fichier.read_text(encoding="utf-8") if fichier.exists() else ""
if FICHIER in existant:
manquants = [nom for nom in fichiers if nom not in existant]
if not manquants:
return return
bloc = "<IfModule mod_headers.c>\n"
for nom in manquants:
bloc += (
f' <Files "{nom}">\n'
' Header set Access-Control-Allow-Origin "*"\n'
" </Files>\n"
)
bloc += "</IfModule>\n"
separateur = "\n" if existant and not existant.endswith("\n") else "" separateur = "\n" if existant and not existant.endswith("\n") else ""
fichier.write_text(existant + separateur + bloc, encoding="utf-8") fichier.write_text(existant + separateur + bloc, encoding="utf-8")
@ -144,7 +175,7 @@ def on_post_build(config):
log.info("derniers-articles : clone superficiel, les dates ne sont pas fiables") log.info("derniers-articles : clone superficiel, les dates ne sont pas fiables")
seuil = datetime.now(timezone.utc) - timedelta(days=JOURS_NOUVEAU) seuil = datetime.now(timezone.utc) - timedelta(days=JOURS_NOUVEAU)
articles = [] pages = []
for chemin, modifiee in modifiees.items(): for chemin, modifiee in modifiees.items():
prefixe = docs_dir.name + "/" prefixe = docs_dir.name + "/"
@ -162,30 +193,43 @@ def on_post_build(config):
except ValueError: except ValueError:
nouveau = False nouveau = False
articles.append({ pages.append({
"titre": page["titre"], "titre": page["titre"],
"url": page["url"], "url": page["url"],
"date": creee if nouveau else modifiee, "date": creee if nouveau else modifiee,
"nouveau": nouveau, "nouveau": nouveau,
"rubrique": _rubriques.get(src_uri, ""),
}) })
if not articles: if not pages:
log.info("derniers-articles : aucune page datée, fichier non écrit") log.info("derniers-articles : aucune page datée, fichier non écrit")
return return
articles.sort(key=lambda article: article["date"], reverse=True) pages.sort(key=lambda page: page["date"], reverse=True)
articles = articles[:NB_MAX]
genere = datetime.now(timezone.utc).replace(microsecond=0).isoformat() genere = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
contenu = json.dumps({"genere": genere, "articles": articles},
ensure_ascii=False, indent=2) + "\n" # L'accueil lit ce fichier depuis longtemps : il garde ses quatre clés, sans la
# rubrique dont il n'a que faire.
recents = [
{clef: page[clef] for clef in ("titre", "url", "date", "nouveau")}
for page in pages[:NB_MAX]
]
ecrits = {
FICHIER_RECENTS: {"genere": genere, "articles": recents},
FICHIER_TOUTES: {"genere": genere, "pages": pages},
}
try: try:
(site_dir / FICHIER).write_text(contenu, encoding="utf-8") for nom, contenu in ecrits.items():
_autoriser_la_lecture_croisee(site_dir) texte = json.dumps(contenu, ensure_ascii=False, indent=2) + "\n"
(site_dir / nom).write_text(texte, encoding="utf-8")
_autoriser_la_lecture_croisee(site_dir, list(ecrits))
except OSError as erreur: except OSError as erreur:
log.info("derniers-articles : écriture impossible (%s), fichier non écrit", log.info("derniers-articles : écriture impossible (%s), fichier non écrit",
erreur) erreur)
return return
log.info("derniers-articles : %d article(s) publié(s) dans %s", len(articles), FICHIER) log.info("derniers-articles : %d page(s) publiée(s) dans %s, %d dans %s",
len(pages), FICHIER_TOUTES, len(recents), FICHIER_RECENTS)