Publier aussi l'inventaire complet des pages

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYfYUZRcJmmEWJwAxZb4o2
This commit is contained in:
Alpinux 2026-09-23 12:42:39 +02:00
parent cf92993ec2
commit c9e2624466
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
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` |

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
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 = (
"<IfModule mod_headers.c>\n"
f' <Files "{FICHIER}">\n'
' Header set Access-Control-Allow-Origin "*"\n'
" </Files>\n"
"</IfModule>\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 = "<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 ""
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)