Colophon
Ce blog existe en double. Le même contenu, deux stacks, deux expériences de lecture presque identiques. C'est voulu.
Pourquoi Astro
J'avais besoin d'un blog rapide, sans runtime JS inutile. Astro coche toutes les cases : rendu HTML à la compilation, zéro framework imposé, îles interactives au besoin. La version 7 est stable et le DX est agréable, les composants .astro ressemblent à du HTML qu'on aurait rendu raisonnable.
Markdoc plutôt que MDX pour les articles. MDX mélange JSX dans du Markdown, ce qui transforme chaque article en fichier de code. Markdoc garde une syntaxe déclarative ({% callout %}...{% /callout %}) et les composants restent côté Astro, pas dans le contenu. Plus propre à long terme.
Keystatic en production : le problème résolu
Keystatic monte un éditeur visuel sur /keystatic/. Il écrit directement dans les fichiers .mdoc : pas de base de données, pas de CMS hébergé, pas d'API tierce. La source de vérité c'est git, pas une table wp_posts.
Sauf que Keystatic est présenté comme un outil de développement : il nécessite les sources du projet pour fonctionner, et injecte des routes SSR incompatibles avec un build purement statique. En mode output: 'static' sans adapter, astro build échoue avec NoAdapterInstalled.
La solution : mode hybride. Astro permet de mélanger pages statiques prérendues et routes SSR dans le même projet. Les articles du blog sont compilés en HTML pur au build, sans JavaScript serveur à l'exécution. Les routes admin (/keystatic/*, /admin/ et les endpoints d'API internes) sont SSR et servies par un adapter Node.
L'autre contrainte : pour que Keystatic puisse écrire les .mdoc sur le disque du serveur, les sources doivent être présentes sur le VPS, pas seulement le dist/. Le CI/CD rsync donc les sources, puis lance bun run build sur place.
L'architecture complète
┌────────────────────────────────────────────────────────────────┐
│ VPS Hetzner (Helsinki) │
│ │
│ src/ ← sources Astro + articles .mdoc │
│ │ └── content/blog/*.mdoc ← écrits par Keystatic │
│ dist/ ← généré par bun run build │
│ ├── client/ ← pages blog statiques (HTML pur) │
│ └── server/ ← routes SSR (entry.mjs) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ systemd astro-blog (Restart=always) │ │
│ │ process Node → port local │ │
│ └──────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────▼───────────────────────────────┐ │
│ │ Caddy │ │
│ │ astro-blog.interstice.work → HTTPS + TLS auto │ │
│ │ │ │
│ │ /keystatic* basic_auth ─┐ │ │
│ │ /admin* basic_auth ─┼──→ reverse_proxy │ │
│ │ endpoints API basic_auth ─┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
Flux éditorial :
1. Ouvrir /keystatic/ → éditeur visuel → sauvegarder
└─ Keystatic écrit le .mdoc dans src/content/blog/
2. Aller sur /admin/ → bouton Rebuild
└─ endpoint authentifié → bun run build (~10s) → redémarrage
3. systemd détecte la sortie → redémarre le process
└─ le nouveau dist/ sert les pages régénéréesLe mécanisme de rebuild
Keystatic sauvegarde le contenu dans les sources. Mais les pages du blog sont statiques : elles ne sont générées qu'au build. Un article sauvegardé n'est pas visible tant que le site n'a pas été reconstruit.
La solution : un bouton Rebuild sur le tableau de bord /admin/. Il envoie une requête à un endpoint d'API interne authentifié, qui lance bun run build en arrière-plan puis appelle process.exit(0). systemd, configuré en Restart=always, redémarre automatiquement le process avec le nouveau dist/. Le build prend une dizaine de secondes, le redémarrage moins d'une seconde.
C'est intentionnellement brutal : pas de logique de restart interne, pas de PM2. systemd fait le travail.
Mode hybride : comment ça fonctionne
Dans astro.config.mjs, output: 'static' reste la valeur par défaut. C'est au niveau de chaque route SSR qu'on opt-out du prérendu :
// src/pages/admin/index.astro export const prerender = false; // ← SSR, pas de build statique
Les pages blog (src/pages/[category]/[...slug].astro, src/pages/index.astro) ne portent pas cette directive : elles sont compilées en HTML pur au build, sans JavaScript serveur. Résultat : les pages blog sont du HTML pur, les routes admin restent dynamiques.
Pourquoi pas le mode GitHub de Keystatic
Keystatic propose un mode alternatif où les sauvegardes transitent par l'API GitHub (commits directs dans le dépôt). Cela supprimerait le besoin de sources sur le VPS et du mécanisme de rebuild.
Problème : GitHub est hébergé sur infrastructure Microsoft/Azure, hors Europe. Toute l'infrastructure The Interstice est sur VPS Hetzner (Helsinki). Faire passer les articles par GitHub pour les réécrire sur le VPS serait incohérent avec cette contrainte. Le mode local, malgré sa complexité, reste 100 % EU.
Le système miroir avec le blog WordPress
Le même contenu est publié en parallèle sur wp-blog.interstice.work (WordPress) et ici (Astro). C'est une comparaison active : temps de chargement, DX d'écriture, référencement, maintenabilité.
Les deux blogs partagent le même switch WP / Astro dans le header et la même newsletter (Brevo, via un proxy PHP côté WP). Depuis le thème Sprite, les cartes de listing sont identiques des deux côtés (hero, catégorie, date, titre) : plus d'extrait automatique nulle part.
Le pendant WordPress de cette page : colophon côté WP, avec le point de vue inverse — pourquoi WordPress reste en ligne malgré son coût en perf/hébergement.
Le thème Sprite
L'identité visuelle des deux blogs a changé le 22 et 23 juillet 2026 : palette terre et parallélogrammes miroirs remplacés par Sprite, une esthétique pixel-art 8-bit/16-bit. Chaque blog a sa variante : Cartridge (nuit/or) ici, Manual (papier/encre) côté WordPress. Les deux partagent la même structure de composants et le même handoff de design, seule la palette s'inverse.
Contrairement à la palette terre précédente, les tokens Sprite ne sont pas synchronisés avec le reste du système The Interstice (vitrine, portfolio, landing) via le script de propagation sync-tokens.py : c'est une identité propre à la famille blog, assumée comme disjointe.
Le cube The Interstice
The Interstice est organisé comme un cube isométrique : trois faces, trois sites.
- Face dessus (crème) : vitrine freelance
- Face gauche (encre) : portfolio technique
- Face droite (terre) : les deux blogs
Les deux blogs restent rattachés à la face droite du cube par leur position dans le hub, mais leur identité visuelle (Sprite) s'en est désormais détachée : plus de palette terre partagée, plus de tokens synchronisés avec les deux autres faces.
Stack complète
- Astro 7 : mode hybride (static + SSR)
- Markdoc : format des articles (
.mdoc) - Keystatic : backoffice en production sur
/keystatic/ - @astrojs/node : adapter standalone, sert le SSR
- Newsreader : serif display (Google Fonts)
- IBM Plex Mono : monospace UI
- Press Start 2P : titres pixel (badge build, sections)
- Brevo : newsletter (proxy PHP via le blog WP)
- VPS Hetzner CX23 (Helsinki) : hébergement EU
- Caddy : reverse proxy, TLS automatique, Basic Auth
- systemd : supervision du process Node, restart automatique
Le code source n'est pas public pour l'instant. Si ça change, ce sera sur github.com/OnyxynO.