From c9e26244665ceb5b0bdcd8a1b32032f81382c02d Mon Sep 17 00:00:00 2001 From: Alpinux Date: Wed, 23 Sep 2026 12:42:39 +0200 Subject: [PATCH] Publier aussi l'inventaire complet des pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit derniers-articles.json s'arrête à vingt entrées : ce qu'il faut pour la section de l'accueil, trop peu pour la page des mises à jour d'alpinux.org, qui veut montrer tout ce que le wiki publie. Le build écrit donc un second fichier, toutes-les-pages.json, sans plafond et avec la rubrique de chaque page — la section de premier niveau de la navigation, celle à laquelle un lecteur range les choses. Un seul parcours de l'historique git produit les deux : le second est la liste entière, le premier son début. derniers-articles.json ne bouge pas d'un octet dans sa forme — mêmes quatre clés, même ordre — pour que l'accueil continue de le lire sans rien savoir de tout ceci. L'en-tête CORS couvre maintenant les deux fichiers, et seulement eux ; le bloc n'est écrit que pour ceux qui manquent au .htaccess, de sorte qu'un second build ne le duplique pas. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JYfYUZRcJmmEWJwAxZb4o2 --- docs/technique/deploiement-wiki.md | 29 +++++--- hooks/derniers_articles.py | 112 ++++++++++++++++++++--------- 2 files changed, 97 insertions(+), 44 deletions(-) diff --git a/docs/technique/deploiement-wiki.md b/docs/technique/deploiement-wiki.md index d617755..e148113 100644 --- a/docs/technique/deploiement-wiki.md +++ b/docs/technique/deploiement-wiki.md @@ -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 -vingt dernières pages créées ou modifiées, avec leur titre, leur adresse et la date du -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 ». +Chaque build écrit deux fichiers à la racine du site, avec pour chaque page son titre, +son adresse et la date du commit qui a touché son contenu : -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 : - **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 dans la liste. - **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 construit avec `--strict`, où un seul avertissement suffit à interrompre le build, et donc la publication. -Le même hook pose l'en-tête `Access-Control-Allow-Origin` sur ce seul fichier, au -moyen d'un `.htaccess` déposé à la racine du site construit. Sans lui, le navigateur -refuserait à alpinux.org le droit de lire une réponse venue de wiki.alpinux.org. +Le même hook pose l'en-tête `Access-Control-Allow-Origin` sur ces deux fichiers, et +sur eux seuls, au moyen d'un `.htaccess` déposé à la racine du site construit. Sans +lui, le navigateur refuserait à alpinux.org le droit de lire une réponse venue de +wiki.alpinux.org. !!! warning "Deux conditions" **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 # 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/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 | | 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 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` | diff --git a/hooks/derniers_articles.py b/hooks/derniers_articles.py index af02d61..c4f997a 100644 --- a/hooks/derniers_articles.py +++ b/hooks/derniers_articles.py @@ -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 -mis à jour ». Il est donc écrit à chaque build, à côté des pages, et servi avec -l'en-tête CORS sans lequel un autre domaine ne peut pas le lire. +Deux fichiers, écrits à chaque build à côté des pages et servis avec l'en-tête CORS +sans lequel un autre domaine ne peut pas les 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 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 -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 -# sert aussi à qui voudrait en faire autre chose. +# Ce que le premier fichier porte au plus. L'accueil n'en affiche que six, mais le +# fichier sert aussi à qui voudrait en faire autre chose. Le second ne plafonne pas. NB_MAX = 20 # 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]] = {} _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): - """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 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.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): """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 -def _autoriser_la_lecture_croisee(site_dir: Path) -> None: - """Pose l'en-tête CORS sur le seul fichier qui en a besoin. +def _autoriser_la_lecture_croisee(site_dir: Path, fichiers: list[str]) -> None: + """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à - 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 = ( - "\n" - f' \n' - ' Header set Access-Control-Allow-Origin "*"\n' - " \n" - "\n" - ) fichier = site_dir / ".htaccess" 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 + + bloc = "\n" + for nom in manquants: + bloc += ( + f' \n' + ' Header set Access-Control-Allow-Origin "*"\n' + " \n" + ) + bloc += "\n" + separateur = "\n" if existant and not existant.endswith("\n") else "" 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") seuil = datetime.now(timezone.utc) - timedelta(days=JOURS_NOUVEAU) - articles = [] + pages = [] for chemin, modifiee in modifiees.items(): prefixe = docs_dir.name + "/" @@ -162,30 +193,43 @@ def on_post_build(config): except ValueError: nouveau = False - articles.append({ - "titre": page["titre"], - "url": page["url"], - "date": creee if nouveau else modifiee, - "nouveau": nouveau, + pages.append({ + "titre": page["titre"], + "url": page["url"], + "date": creee if nouveau else modifiee, + "nouveau": nouveau, + "rubrique": _rubriques.get(src_uri, ""), }) - if not articles: + if not pages: log.info("derniers-articles : aucune page datée, fichier non écrit") return - articles.sort(key=lambda article: article["date"], reverse=True) - articles = articles[:NB_MAX] + pages.sort(key=lambda page: page["date"], reverse=True) 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: - (site_dir / FICHIER).write_text(contenu, encoding="utf-8") - _autoriser_la_lecture_croisee(site_dir) + for nom, contenu in ecrits.items(): + 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: log.info("derniers-articles : écriture impossible (%s), fichier non écrit", erreur) 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)