Systeam, blog, Comment ce site est construit

samedi 26 septembre 2026

Nom

build - un dossier de Markdown, deux sorties, zéro dépendance

Théau Trovaoutillage

Résumé

Tout ce que vous lisez ici sort d'un dossier, content/, écrit dans un Markdown restreint. Quelques centaines de lignes de Python, sans aucune dépendance, en tirent deux choses : la page HTML que vous lisez, et un miroir en texte brut pour le terminal. Cet article montre chaque élément du format, avec la source puis le rendu.

Ce générateur s'appelle désormais tilder. Il est libre, sous licence MIT : github.com/THOSTED/tilder (site externe, nouvel onglet).

Essayez dès maintenant : curl systeam.sh/blog/2026-09-26-comment-ce-site-est-construit affiche cet article en texte, en couleur.

Ce sommaire est une seule ligne, [TOC], seule dans la source : il liste les sections de la page, avec un lien vers chacune.

Le principe

Une page, c'est un fichier. content/events.md devient /events, content/blog/index.md devient /blog/. Le script lit chaque fichier et produit :

  • la page HTML ;
  • sa version texte, en ASCII pur, repliée à 75 colonnes ;
  • la même version texte, colorée par des séquences ANSI ;
  • les flux RSS et iCalendar, et le plan du site.
Schéma : content/ passe par builder/build.py, qui produit la page HTML, le texte, le texte coloré et les flux
Le chemin d'une page, de la source au site.

Rien de tout cela n'est commité. Au démarrage, un conteneur construit le site, puis surveille les sources : un fichier modifié, et le site est reconstruit dans la seconde. Le serveur, Caddy, ne fait que servir des fichiers.

Pourquoi pas un générateur existant ? Parce que chaque élément du format doit exister deux fois : en HTML et en texte pour le terminal. Un format réduit à ce que les deux sorties savent rendre tient dans un seul fichier.

En-tête

Chaque fichier commence par quelques lignes entre deux ---. Pour un article, elles donnent le titre, la description, l'auteur et une étiquette ; la date vient du nom du fichier :

---
title: Comment ce site est construit
description: Une phrase, une seule.
author: Théau Trova
tag: outillage
---

Le reste de la page - filets, navigation, logo, pied, et la fiche de l'article sous son titre - vient du script et de content/site.toml. Le fichier ne contient que le contenu. La liste des articles, sur le blog, est construite de la même façon : il n'y a rien à y ajouter à la main.

Sections

Un titre de niveau deux ouvre une section : son nom dans la marge de gauche, le contenu décalé à droite, comme dans une page de manuel. Des marqueurs entre accolades s'ajoutent en fin de titre :

## Sections {#sections}
## Membres {grid}
## Pour le terminal {text}
## Pour la page {html}

{#sections} donne une ancre : cette section se lie avec le lien vers #sections. {grid}, {text} et {html} sont montrés plus bas, {members} aussi.

Entrées

Un titre de niveau trois est une entrée : un événement, un membre, un article. La liste qui le suit immédiatement est sa ligne de métadonnées. Une date au format AAAA-MM-JJ | date lisible devient une balise time, un texte entre accents graves devient une étiquette, le reste s'affiche tel quel. Le corps de l'entrée est décalé de deux espaces.

### Exemple d'entrée {next}

  - 2026-11-21 | samedi 21 novembre 2026
  - I-Factory, campus de la Doua, Villeurbanne
  - `exemple`

  Le corps de l'entrée, décalé de deux espaces.

Rendu :

Exemple d'entrée

I-Factory, campus de la Doua, Villeurbanneexemple

Le corps de l'entrée, décalé de deux espaces. {next} met son étiquette dans la couleur d'accent : c'est ainsi que la prochaine rencontre se repère, sur /events comme sur l'accueil.

Entrée complète

complet

{full} marque un mentor qui a atteint sa capacité. L'étiquette passe dans la couleur d'avertissement.

Dans la version texte, l'étiquette s'aligne à droite du titre, entre crochets : [ exemple ].

Blocs

Les blocs sont séparés par une ligne vide. Un paragraphe peut s'écrire sur plusieurs lignes : elles sont rejointes.

Des classes en fin de paragraphe changent son apparence dans la page :

Un paragraphe discret. {small muted}

Un paragraphe en petit.

Un paragraphe atténué.

Un paragraphe très atténué.

Un paragraphe en chasse fixe.

Un paragraphe d'avertissement.

Un paragraphe entièrement entre astérisques est un état vide, la manière du site de dire que quelque chose n'existe pas encore :

*Aucun article publié pour l'instant.*

Aucun article publié pour l'instant.

Une liste, une ligne par élément. Un tiret pour une liste simple, un nombre suivi d'un point pour une liste numérotée ; deux espaces de retrait de plus imbriquent une liste dans l'élément du dessus :

1. Cloner le dépôt
2. Lancer la pile
   - `docker compose up -d`
   - vérifier avec `curl`
3. Écrire du contenu
  1. Cloner le dépôt
  2. Lancer la pile
    • docker compose up -d
    • vérifier avec curl
  3. Écrire du contenu

Une case entre crochets fait une liste de tâches ; un x la coche :

- [x] migrer le blog en dossiers
- [ ] écrire le premier compte rendu
  • migrer le blog en dossiers
  • écrire le premier compte rendu

Dans le terminal, les cases restent [x] et [ ], et les listes gardent leurs numéros et leur retrait.

Trois tirets seuls sur une ligne tracent un filet :

---

Un encadré, chaque ligne commençant par un chevron :

> **Note :** un encadré, pour ce qui doit ressortir.

Note : un encadré, pour ce qui doit ressortir.

Enfin, un commentaire HTML passe dans la page et disparaît de la version texte. Il sert aux notes pour les rédacteurs :

<!-- TO FILL: le nom de l'hébergeur. -->

Code

Un bloc de code s'ouvre et se ferme par trois accents graves. Le nom d'un langage, juste après, active la coloration syntaxique ; elle est calculée à la construction, sans JavaScript, et le langage s'affiche dans le coin du bloc :

```python
def plier(texte, largeur=75):
    return textwrap.wrap(texte, largeur)
```

Rendu :

import textwrap

# Replier un paragraphe pour un terminal de 80 colonnes.
def plier(texte: str, largeur: int = 75) -> list[str]:
    """Une ligne par élément, jamais plus large que `largeur`."""
    if not texte:
        return [""]
    return textwrap.wrap(texte, largeur, break_long_words=False)

console distingue l'invite de la commande et de sa sortie :

$ curl -sI systeam.sh | grep -i content-security
Content-Security-Policy: default-src 'none'; style-src 'self'; ...

diff colore ce qui entre et ce qui sort :

@@ -1,3 +1,3 @@
 services:
-  image: caddy:2
+  image: caddy:2-alpine

Une configuration, ici un Caddyfile :

(base) {
	log {
		output discard  # aucun journal d'accès
	}
	header -Server
}

Langages reconnus : sh, console, python, js (et ts), c, go, rust, sql, json, yaml, toml et ini, caddy et nginx, dockerfile, html et xml, css, make, diff, text. Un langage inconnu laisse le bloc en texte simple.

Chaque bloc a un bouton « copier », qui copie le code sans la coloration. Une ligne trop longue ne casse pas la mise en page : le bloc défile horizontalement.

docker compose logs -f build | grep --line-buffered -E 'built|failed' | while read -r l; do printf '%s\n' "$l"; done

Dans la version texte, cette ligne est coupée à la 75e colonne, la coupure marquée d'une barre oblique inverse - qu'un shell lit comme une continuation.

Tableaux

Un tableau s'écrit avec des barres verticales. La ligne de tirets sépare l'en-tête ; un deux-points règle l'alignement :

| Sortie   | Format     | Largeur |
|:---------|:----------:|--------:|
| page     | HTML       |   libre |
| miroir   | ASCII      |      75 |

Rendu :

Sortie Format Largeur
pageHTMLlibre
miroirASCII75
miroir coloréASCII + ANSI75
fluxRSS, iCalendar-

Dans le terminal, le tableau devient des colonnes alignées, comme column -t. S'il ne tient pas en 75 colonnes, chaque ligne devient une fiche En-tête: valeur. Un tableau trop large pour la page défile, sans élargir le reste.

Bandeaux

Un encadré dont la première ligne est [!INFO], [!WARNING] ou [!ERROR] devient un bandeau. La syntaxe est celle de GitHub ; le libellé affiché est en français :

> [!WARNING]
> Ne modifiez jamais public/ : il est reconstruit à chaque changement.

INFO

Le site est reconstruit dans la seconde qui suit une modification de content/.

ATTENTION

Ne modifiez jamais public/ à la main : il est reconstruit à chaque changement.

Une ligne qui ne contient qu'un chevron sépare deux paragraphes.

ERREUR

Une construction ratée est journalisée ; la dernière version correcte reste servie.

Dans le terminal, un bandeau devient une boîte, colorée selon son type.

En ligne

Six éléments, sans imbrication :

[un lien](events), `du code`, **du gras**, *de l'italique*, ~~du barré~~, ++du souligné++

Rendu : un lien, du code, du gras, de l'italique, du barré, du souligné.

Le souligné est en pointillés : sur le web, un soulignement plein se confond avec un lien.

L'italique s'écrit aussi _ainsi_, mais seulement autour d'un mot entier : snake_case reste tel quel. Dans le terminal, l'italique devient du texte simple ; le barré garde ses ~~, parce que le retirer changerait le sens.

Les liens internes s'écrivent depuis la racine du site, sans barre initiale et sans .html : events, blog/, members/theau-trova, ./#contribuer. Le script les rend relatifs à la page qui les contient. Un lien externe porte une flèche : le lab (site externe).

Dans la version texte, un lien interne garde son libellé seul - on le visite avec curl, on ne le recopie pas. Un lien externe affiche son adresse entre parenthèses.

Une grille

événements

calendrier, flux iCalendar et RSSpage

blog

comptes rendus techniquespage

membres

mentors et membrespage

Images

Un article ou un événement peut être un fichier, ou un dossier du même nom contenant index.md et ses images. Une image s'écrit seule sur sa ligne ; son chemin part du dossier de l'article, et le texte entre guillemets devient la légende :

content/blog/2026-09-26-comment-ce-site-est-construit/
  index.md
  pipeline.svg

![Schéma du chemin d'une page](pipeline.svg "Le chemin d'une page.")

Le schéma du début de cet article est écrit ainsi. Le texte alternatif est obligatoire : c'est lui que lit un lecteur d'écran, et c'est lui qu'affiche le terminal, avec l'adresse de l'image.

Membres

La liste des membres n'est pas écrite à la main. Chaque membre a son fichier, et ses informations - prénom, nom, nom d'usage, pronoms, catégorie - sont dans son en-tête. Une section marquée ainsi se remplit toute seule d'une fiche par membre :

## Membres {members}

C'est aussi la seule page du site qui charge un script : une recherche par nom et un filtre par catégorie. Sans JavaScript, la liste complète reste affichée.

Pour la page

Cette section porte {html} : elle n'existe que dans la page. Dans le terminal, elle n'apparaît pas.

Deux sorties

La version texte n'est pas une copie tenue à la main : elle sort du même fichier, au même moment. Elle ne peut donc pas diverger de la page.

curl systeam.sh              en couleur, depuis le site
curl systeam.sh/events?plain ASCII nu, pour un fichier
curl man.systeam.sh          ASCII nu, sans détection

La conversion ne va que dans un sens. Le texte perd les accents et les liens : on ne retrouve pas « ingénierie » à partir de « ingenierie ». C'est pourquoi la source est le Markdown accentué, et jamais le texte.

Ce qui n'existe pas

Volontairement : pas de titres au-delà du niveau trois, pas de notes de bas de page, pas d'images dans une phrase, pas de HTML en ligne. Le JavaScript se limite à deux petits scripts : la recherche des membres, et le bouton « copier » des blocs de code. Chaque ajout se paie deux fois, une fois par sortie.

La référence complète du format est dans le dépôt, builder/docs/markdown.md.

Voir aussi

tous les articles · curl systeam.sh/blog/2026-09-26-comment-ce-site-est-construit

↑ haut de page