Moderniser un blog Pelican en 2026 — Architecture, Stack & CI/CD
Posted on Sun 29 March 2026 in Développement
Moderniser un blog Pelican en 2026
Ce blog existe depuis 2020. Il a été construit à l'époque avec une stack qui fonctionnait, mais qui n'a pas bien vieilli : requirements.txt sans versions pinées, 6 scripts shell, un Makefile généré par Pelican, des plugins tiers abandonnés, un thème datant de 2013, et un déploiement entièrement manuel.
Voici le retour d'expérience sur sa modernisation complète.
L'état des lieux (avant)
Le projet souffrait de plusieurs problèmes :
| Problème | Détail |
|---|---|
| Dépendances | requirements.txt avec 3 lignes (pelican, Markdown, punch.py), aucune version pinée |
| Scripts | 6 scripts shell (setup.sh, deploy.sh, publish.sh, release.sh, add_extras_*.sh) |
| Structure | Contenu imbriqué dans pelican/content/, config dans pelican/ |
| Plugins | pelican-css et pelican-js (repos tiers sur notabug.org, plus maintenus) |
| Thème | blueidea-custom — CSS de 2013, pas responsive, largeur fixe, police Trebuchet MS |
| Déploiement | Manuel : make publish → copie dans un dossier deploy/ → git push |
| Versioning | punch.py pour gérer un numéro de version (inutile pour un blog) |
| Workflow | git-flow (overkill pour un blog personnel) |
La nouvelle stack
Python : uv au lieu de pip/poetry
uv est un gestionnaire de paquets Python écrit en Rust, rapide et tout-en-un. Il remplace pip, pip-tools, virtualenv et poetry avec un seul outil.
Le fichier pyproject.toml minimal :
[project]
name = "blog-source"
version = "1.0.0"
description = "Bloggy le Blog - Blog technique avec Pelican"
requires-python = ">=3.10"
dependencies = [
"pelican[markdown]>=4.9",
"ghp-import>=2.1",
]
Installation en une commande :
uv sync # Crée le venv, résout les dépendances, installe tout
Sur un runner GitHub Actions, uv sync prend ~2 secondes contre 15-30s avec pip.
Task runner : Justfile au lieu de Make + scripts shell
just est un command runner moderne (écrit en Rust) qui remplace make pour les tâches de projet. Contrairement à Make, il n'a pas de système de dépendances de fichiers — c'est juste un lanceur de commandes avec une syntaxe propre.
Le Justfile complet du projet :
set dotenv-load
set positional-arguments
# Show available recipes
default:
@just --list
# Install dependencies and setup theme
setup:
uv sync
git submodule update --init --recursive
# Start dev server with live-reload
dev port="8000":
uv run pelican content -o output -s pelicanconf.py -lr -p {{ port }}
# Build for production
publish:
uv run pelican content -o output -s publishconf.py
# Deploy to GitHub Pages
deploy: publish
uv run ghp-import output -b main -r deploy -p -f
L'avantage par rapport à 6 scripts shell indépendants : un seul point d'entrée, auto-documenté (just --list), avec des paramètres nommés et des valeurs par défaut.
Thème : Flex
Flex est le thème Pelican le plus populaire et maintenu. Il apporte :
- Responsive mobile-first
- Dark mode automatique (suit
prefers-color-schemede l'OS) - Syntax highlighting avec Pygments (thème light/dark séparé)
- SEO : balises OpenGraph, meta description
- Config via des variables Python dans
pelicanconf.py:
THEME = "themes/Flex"
# Dark mode auto-detect
THEME_COLOR_AUTO_DETECT_BROWSER_PREFERENCE = True
THEME_COLOR_ENABLE_USER_OVERRIDE = True
# Syntax highlighting
PYGMENTS_STYLE = "github" # thème clair
PYGMENTS_STYLE_DARK = "monokai" # thème sombre
Le thème est intégré comme git submodule, ce qui permet de le mettre à jour indépendamment et de garder un historique propre.
Plugin local : css_js_injector
Les anciens plugins pelican-css et pelican-js (repos tiers) utilisaient des hacks fragiles avec des constantes magiques pour injecter du CSS/JS dans les templates. Ils ne sont plus maintenus.
Le remplacement tient en ~40 lignes de Python :
import re
from pelican import signals
def inject_css_js(path, context):
if not path.endswith(".html"):
return
article = context.get("article") or context.get("page")
if article is None:
return
siteurl = context.get("SITEURL", "")
css_meta = getattr(article, "css", None)
js_meta = getattr(article, "js", None)
if not css_meta and not js_meta:
return
with open(path, "r", encoding="utf-8") as f:
content = f.read()
if css_meta:
for name in css_meta.split(","):
tag = f'<link rel="stylesheet" href="{siteurl}/css/{name.strip()}" type="text/css">'
content = content.replace("</head>", f"{tag}\n</head>", 1)
if js_meta:
for entry in js_meta.split(","):
entry = entry.strip()
if "(top)" in entry:
fname = entry.replace("(top)", "").strip()
tag = f'<script src="{siteurl}/js/{fname}"></script>'
content = re.sub(r"(<body[^>]*>)", rf"\1\n{tag}", content, count=1)
else:
fname = entry.replace("(bottom)", "").strip()
tag = f'<script src="{siteurl}/js/{fname}"></script>'
content = content.replace("</body>", f"{tag}\n</body>", 1)
with open(path, "w", encoding="utf-8") as f:
f.write(content)
def register():
signals.content_written.connect(inject_css_js)
Le principe : Pelican émet un signal content_written après avoir généré chaque fichier HTML. Le plugin intercepte ce signal, lit les metadata CSS et JS de l'article, et injecte les balises <link> / <script> aux bons endroits dans le HTML.
Utilisation dans un article :
---
title: Mon article
CSS: asciinema-player.css
JS: asciinema-player.js (top)
---
Un point technique à noter : le remplacement de <body> nécessite une regex (<body[^>]*>) car les thèmes ajoutent des attributs (class, id) sur la balise.
CI/CD : GitHub Actions
Déploiement automatique
Chaque push sur master déclenche un build + deploy :
name: Deploy Blog
on:
push:
branches: [master, main]
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: astral-sh/setup-uv@v4
- run: uv sync
- run: uv run pelican content -o output -s publishconf.py
- uses: JamesIves/github-pages-deploy-action@v4
with:
repository-name: yoyonel/yoyonel.github.io
branch: master
folder: output
ssh-key: ${{ secrets.DEPLOY_KEY }}
Le temps total du pipeline : ~25 secondes (dont ~15s pour le checkout des submodules).
Preview de PR sur surge.sh
Chaque Pull Request génère automatiquement une preview sur surge.sh avec un commentaire bot contenant l'URL :
🚀 Preview deployed!
https://blog-source-pr-1.surge.sh
Le workflow utilise une variable d'environnement PELICAN_SITEURL pour overrider le SITEURL de production :
# publishconf.py
SITEURL = os.environ.get("PELICAN_SITEURL", "https://yoyonel.github.io")
Cela permet au même publishconf.py de servir à la fois pour la production et pour les previews, sans duplication de configuration.
Structure finale du projet
├── .github/workflows/
│ ├── deploy.yml # Deploy sur push master
│ └── preview.yml # Preview PR via surge.sh
├── content/
│ ├── css/ # CSS custom (assets statiques)
│ ├── js/ # JS custom
│ └── *.md # Articles en Markdown
├── plugins/
│ └── css_js_injector.py # Plugin CSS/JS local
├── themes/
│ └── Flex/ # Thème (git submodule)
├── Justfile # Point d'entrée unique
├── pelicanconf.py # Config dev
├── publishconf.py # Config production
└── pyproject.toml # Dépendances Python
Comparé à avant : 11 fichiers de config/scripts réduits à 4 (pyproject.toml, pelicanconf.py, publishconf.py, Justfile).
Ce que ça donne au quotidien
Écrire un article :
just new-post "Mon Super Article"
just dev
# → http://localhost:8000 avec live-reload
Publier :
git add content/mon-super-article.md
git commit -m "article: mon super article"
git push
# → GitHub Actions build + deploy automatiquement
C'est tout. Pas de make publish && cd deploy && git add . && git commit && git push. Pas de virtualenv à activer manuellement. Pas de thème à installer dans le venv. Le blog se déploie en 25 secondes sur chaque push.
Reproduire cette stack
Pour construire un blog similaire from scratch :
- Pelican + Markdown : le moteur de blog statique
- uv : gestion des dépendances Python
- just : task runner
- Flex : thème responsive avec dark mode
- GitHub Actions + JamesIves/github-pages-deploy-action : CI/CD
- surge.sh : preview de PR
Le tout est open source : yoyonel/blog_source.