FastAPI + HTMX : le full-stack sans build
# Créez des applications web prêtes pour la production sans React ni webpack : FastAPI, HTMX, Alpine.js, Jinja2, CSS pur, modèles Bootstrap, i18n, déploiement, SEO et performances.
En bref : FastAPI + HTMX + Alpine.js + Jinja2 + du CSS simple produisent des applications web de production sans aucun outil de build, sans aucun
node_modules/, avec des scores Lighthouse parfaits. Ce guide couvre tout le système, de l’architecture au déploiement, en utilisant blakecrosley.com comme référence en production : 210 articles de blog, des composants JavaScript interactifs, 11 guides fondamentaux, 48 études de design, ainsi que l’anglais et 9 langues traduites, sans le moindre bundler, compilateur ou transpilateur.1
Le stack moderne de développement web part du principe que vous avez besoin de React, webpack, TypeScript et d’un pipeline de build. Pour une grande catégorie d’applications — sites axés sur le contenu, outils internes, applications CRUD, sites portfolio, plateformes de documentation — cette hypothèse est fausse. Le stack décrit dans ce guide élimine toute la chaîne d’outils de build frontend, tout en produisant des sites qui obtiennent 100/100/100/100 sur Lighthouse.2
Ce n’est pas du militantisme. C’est une mesure. L’architecture décrite ici fonctionne en production, sert de vrais utilisateurs dans dix langues, et les chiffres sont vérifiables.
Points clés
- Le rendu HTML côté serveur élimine trois catégories entières de problèmes : la gestion d’état côté client, les frontières de sérialisation JSON et les incohérences d’hydratation. HTMX fait des réponses du serveur la sortie finale — pas d’étape de rendu côté client.
- Zéro outil de build signifie zéro échec de build. Pas de conflits de dépendances entre pairs lors de
npm install, pas d’erreurs du compilateur TypeScript dans des fichiers que vous n’avez pas touchés, pas de PR Dependabot pour des dépendances transitives que vous n’avez jamais importées. Le pipeline de déploiement se résume àgit push. - Alpine.js gère l’état purement client que HTMX ne peut pas gérer. Les menus déroulants, les modales, les bascules de navigation mobile et tout état d’interface qui existe uniquement dans le navigateur relèvent d’Alpine.js. La frontière est claire : si l’état a besoin du serveur, utilisez HTMX. Sinon, utilisez Alpine.js.
- Le CSS pur avec propriétés personnalisées remplace Sass et Tailwind. Les propriétés personnalisées CSS se propagent en cascade, s’héritent et répondent aux media queries à l’exécution. Les variables de préprocesseur se compilent en valeurs statiques et disparaissent. Le navigateur lit directement les propriétés personnalisées — sans étape de compilation.
- Cette approche a des limites claires. Elle est inadaptée aux grandes équipes qui partagent des interfaces de composants, aux produits SaaS dotés d’un état client complexe, et aux applications qui dépendent des bibliothèques de l’écosystème npm. Le cadre de décision de la section 15 identifie précisément cette frontière.
- blakecrosley.com en est la preuve. Les patterns fondamentaux de ce guide (HTMX, Alpine.js, Jinja2, CSS pur) tournent en production sur blakecrosley.com. Les sections Bootstrap et SQLAlchemy couvrent des patterns standards de la stack qui ne sont pas utilisés sur ce site précis. Chaque affirmation s’appuie sur un chemin de fichier, un bloc de configuration ou un audit Lighthouse que vous pouvez vérifier vous-même sur PageSpeed Insights.2
Comment utiliser ce guide
Il s’agit d’une référence exhaustive. Commencez là où votre niveau d’expérience correspond :
| Expérience | Commencez ici | Puis explorez |
|---|---|---|
| Développeur Python, nouveau venu sur HTMX | La thèse no-build → Vue d’ensemble de l’architecture → HTMX en profondeur | Patterns Alpine.js, Sécurité |
| Développeur React/Vue évaluant des alternatives | La thèse no-build → Cadre de décision | Vue d’ensemble de l’architecture, Performance |
| Développeur FastAPI ajoutant de l’interactivité | HTMX en profondeur → Patterns Alpine.js | i18n et localisation, Déploiement |
| Développeur full-stack partant de zéro | Lisez de manière séquentielle à partir de Vue d’ensemble de l’architecture | Carte de référence rapide pour un usage continu |
Utilisez Ctrl+F / Cmd+F pour rechercher des patterns ou des attributs spécifiques. La Carte de référence rapide à la fin fournit un résumé parcourable.
La thèse no-build
La thèse est étroite et précise : pour les sites pilotés par le contenu avec un développeur solo ou une petite équipe, les outils de build résolvent des problèmes que vous n’avez pas tout en en créant que vous avez.
Voici les véritables métriques de blakecrosley.com :
| Métrique | blakecrosley.com (no-build) | Projet Next.js typique3 |
|---|---|---|
| Dépendances | 17 paquets Python | 311+ paquets npm |
| Fichiers de configuration de build | 0 | 5-8 (next.config, tsconfig, postcss, tailwind, etc.) |
Taille de node_modules/ |
N’existe pas | 187 Mo de base, 250-400 Mo avec ajouts |
| Temps d’installation | pip install : 8 secondes |
npm install : 30-90 secondes |
| Étape de build | Aucune | next build : 15-60 secondes |
| Pipeline de déploiement | git push → en ligne en ~40 secondes |
Install → build → deploy : 2-5 minutes |
| Performance Lighthouse | 100 | 70-90 sans optimisation explicite4 |
Les 17 paquets Python incluent FastAPI, Jinja2, Pydantic, uvicorn, nh3, et 12 autres. Aucun n’est un outil de build. Aucun n’est un compilateur. Aucun n’est un bundler.5
Ce à quoi vous renoncez
L’honnêteté impose de lister les véritables coûts :
Pas de TypeScript. Chaque fichier .js est du JavaScript vanilla. Les erreurs de type sont attrapées par les tests et l’analyse de code, pas par un compilateur. Cela fonctionne pour un développeur solo. Cela ne fonctionnerait pas pour une équipe de 10 personnes partageant des interfaces de composants.
Pas de Hot Module Replacement. Les modifications CSS nécessitent un rafraîchissement manuel du navigateur. Le hx-boost d’HTMX rend la navigation suffisamment rapide pour que les rafraîchissements complets soient tolérables, mais lors de cycles d’itération visuelle serrés, le HMR fait gagner du temps.
Pas de Tree Shaking. Chaque octet de JavaScript que vous écrivez est livré au navigateur. Cette contrainte impose de la discipline : des fichiers petits et ciblés plutôt que de gros modules utilitaires.
Pas de bibliothèques de composants npm. Pas de Radix, pas de shadcn/ui, pas de Headless UI. Chaque élément interactif est construit à la main ou utilise les composants intégrés de Bootstrap 5.
Pas de design tokens issus de npm. Le système de design vit dans les propriétés personnalisées CSS. Il ne peut pas être importé comme paquet dans un autre projet.
Ces compromis sont acceptables pour un site piloté par le contenu avec un à trois développeurs. Ils seraient inacceptables pour un produit SaaS avec une équipe d’ingénierie de 15 personnes. La section 15 fournit le cadre de décision.
Ce que vous gagnez
Zéro échec de build. Aucun npm install ne peut échouer à cause de conflits de dépendances entre pairs. Aucun next build ne peut échouer à cause d’une erreur TypeScript dans un fichier que vous n’avez pas touché.6
Debug via View Source. Le JavaScript qui s’exécute dans le navigateur est le JavaScript que vous avez écrit. Pas besoin de source maps.
Démarrage local instantané. uvicorn app.main:app --reload démarre en moins de 2 secondes.
Cascade de requêtes concrète. Une première visite charge : un document HTML (~15 Ko gzippés), un fichier CSS (~8 Ko), HTMX (~16 Ko, mis en cache), Alpine.js (~15 Ko, mis en cache) et le JS interactif de la page (~4-8 Ko). Total : environ 55-65 Ko à la première visite.1
Frontend pérenne. Le code côté client utilise HTML, CSS et JavaScript — des standards qui ont maintenu une rétrocompatibilité pendant 30 ans.7 Pas de migration Webpack 4 → 5, pas de dépréciation de Create React App, pas de migration vers le App Router de Next.js.
Comparaison de stacks
Comment la stack no-build se compare aux alternatives courantes sur des dimensions mesurables :
| Dimension | FastAPI+HTMX (ce guide) | Next.js (React) | Astro | 11ty |
|---|---|---|---|---|
| JS livré au navigateur | 35-40 Ko (HTMX+Alpine+petits scripts de page) | 85-250 Ko+ (runtime React) | 0 Ko par défaut, îlots opt-in | 0 Ko par défaut |
| Étape de build | Aucune | Requise (webpack/turbopack) | Requise (Vite) | Requise (personnalisée) |
| Fichiers de configuration | 0 | 5-8 (next.config, tsconfig, etc.) | 1-3 (astro.config, tsconfig) | 1-2 (.eleventy.js) |
| Pipeline de déploiement | git push (40 s) |
Install+build+deploy (2-5 min) | Install+build+deploy (1-3 min) | Install+build+deploy (1-2 min) |
| Interactivité côté serveur | Native (HTMX) | Routes API + fetch client | Limitée (actions de formulaire) | Aucune (sortie statique) |
| Gestion d’état client | Alpine.js (15 Ko) | State/context/Redux React | Îlots de framework | JS manuel |
| Langage backend | Python | JavaScript/TypeScript | JavaScript/TypeScript | JavaScript |
| Approche i18n | Côté serveur (middleware) | next-intl ou paquet similaire | @astrojs/i18n | Manuelle |
| Performance Lighthouse | 100 (mesurée) | 70-90 typique4 | 95-100 typique | 95-100 typique |
| Idéal pour | Sites de contenu, CRUD, tableaux de bord | SPA complexes, grandes équipes | Sites de contenu, marketing | Blogs statiques, docs |
Astro et 11ty sont les concurrents les plus proches pour les sites de contenu. Tous deux produisent une excellente sortie statique mais nécessitent une étape de build et une chaîne d’outils JavaScript. La stack FastAPI+HTMX échange la performance des sites statiques contre une interactivité côté serveur (filtrage par catégorie, gestion de formulaires, recherche en temps réel) sans ajouter d’étape de build. Si votre site est purement statique sans interactions serveur, Astro ou 11ty seront peut-être le meilleur choix.
Vue d’ensemble de l’architecture
Flux des requêtes
Chaque requête suit un chemin unique à travers quatre couches :
Browser FastAPI Jinja2 HTMX/Alpine
| | | |
|--- GET /about ------>| | |
| |-- render template ->| |
| | |-- base.html ------->|
| | | + about.html |
| |<-- full HTML -------| |
|<--- HTML response ---| | |
| |
|--- hx-get /search ------------------------------------------------>|
| |<-- HTMX request ----| |
| |-- render partial -->| |
| | |-- _results.html |
| |<-- HTML fragment ---| |
|<--- HTML fragment ---| | |
|--- DOM swap -------------------------------------------------------->|
Les chargements de page complets renvoient des documents HTML entiers (template de base + template de page). Les requêtes HTMX renvoient des fragments HTML (partiels). Le serveur décide quoi afficher en fonction du type de requête. Alpine.js gère l’état côté client qui ne touche jamais le serveur.
Rôles des composants
| Composant | Rôle | Portée |
|---|---|---|
| FastAPI | Routage, logique métier, accès aux données, validation | Serveur |
| Jinja2 | Rendu des templates, héritage, macros | Serveur |
| HTMX | Interactivité pilotée par le serveur (formulaires, pagination, recherche) | Client ↔ Serveur |
| Alpine.js | État côté client uniquement (menus déroulants, modales, bascules) | Client uniquement |
| Bootstrap 5 | Système de grille, classes utilitaires, mise en page responsive | Client (CSS) |
| CSS pur | Propriétés personnalisées, styles de composants, design tokens | Client (CSS) |
| Pydantic | Validation des requêtes/réponses, paramètres | Serveur |
Structure du projet
app/
├── main.py # FastAPI app, middleware, templates
├── config.py # Pydantic settings management
├── routes/
│ ├── pages.py # Page routes (HTML responses)
│ └── api.py # API routes (JSON/HTML fragment responses)
├── content.py # Markdown loading, blog post parsing
├── security/
│ ├── headers.py # CSP, HSTS, security headers middleware
│ ├── csrf.py # HMAC-signed CSRF tokens
│ ├── rate_limit.py # 3-tier rate limiting
│ └── logging.py # Security event logging
├── i18n/
│ ├── config.py # Supported locales, mappings
│ ├── middleware.py # URL-based locale detection
│ ├── jinja.py # Translation functions for templates
│ └── d1_client.py # Cloudflare D1 translation storage
├── cache_assets.py # Content-hash asset versioning
└── templates/
├── base.html # Base layout with Alpine.js state
├── components/ # Reusable partials (_language_switcher.html, etc.)
└── pages/ # Page templates (home.html, about.html, etc.)
content/
├── blog/ # Markdown blog posts with YAML frontmatter
└── guides/ # Multi-section guide markdown
static/
├── css/ # Plain CSS (no preprocessors)
├── js/ # Vanilla JavaScript (no bundlers)
│ └── vendor/ # Self-hosted HTMX, Alpine.js
└── images/ # Optimized images with WebP srcset
Cette structure repose sur un principe unique : chaque répertoire contient un seul type d’élément. Les routes se trouvent dans routes/. Les templates dans templates/. Les ressources statiques dans static/. Aucune étape de build ne transforme l’un en l’autre.
Comparaison avec l’architecture SPA
Dans un projet React + Next.js, la structure équivalente inclurait :
src/
├── components/ # React components (JSX)
├── pages/ # Route handlers (also JSX)
├── api/ # API routes (also in pages/)
├── hooks/ # Custom React hooks
├── context/ # React context providers
├── lib/ # Utility functions
├── styles/ # CSS modules or Tailwind config
└── types/ # TypeScript type definitions
# Plus build configuration
next.config.js
tsconfig.json
postcss.config.js
tailwind.config.js
eslint.config.js
package.json
package-lock.json
node_modules/ # 187+ MB of dependencies
L’architecture SPA nécessite une coordination au moment du build entre ces répertoires. TypeScript compile les fichiers .tsx en JavaScript. PostCSS transforme les directives Tailwind en CSS. Webpack (ou Turbopack) regroupe le résultat en chunks. Chaque étape peut échouer indépendamment.
L’architecture sans build ne nécessite aucune coordination. Le template référence un fichier CSS. Ce fichier CSS existe dans static/css/. Le navigateur le charge directement. Si vous renommez un fichier, la référence dans le template échoue au moment de l’exécution — pas au moment du build. Cela déplace les erreurs de la compilation vers l’exécution, ce qui constitue un véritable compromis. Pour un développeur solo utilisant uvicorn --reload pendant le développement, les erreurs d’exécution apparaissent immédiatement dans le navigateur. Pour une grande équipe, les erreurs de compilation détectées par TypeScript préviennent une catégorie de bugs que les erreurs d’exécution ne peuvent pas intercepter.
Patterns FastAPI
Configuration de l’application
L’application s’initialise dans main.py avec un ordre explicite des middleware :
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.gzip import GZipMiddleware
app = FastAPI(
title="Blake Crosley",
docs_url=None, # Disable docs in production
redoc_url=None,
openapi_url=None, # Prevent /openapi.json exposure
)
# Middleware order matters: last added = first executed
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(GZipMiddleware, minimum_size=500)
app.add_middleware(LocaleMiddleware)
app.add_middleware(RateLimitMiddleware)
app.add_middleware(SecurityLogMiddleware, site_name="blakecrosley.com")
# Static files
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# Templates
templates = Jinja2Templates(directory=TEMPLATES_DIR)
Trois décisions de conception comptent ici. Premièrement, docs_url=None et openapi_url=None désactivent les points de terminaison de documentation API automatiques. Un site de contenu public n’a pas besoin d’exposer /docs ou /openapi.json sur internet.8 Deuxièmement, l’ordre des middleware est important — la journalisation de sécurité s’exécute en premier (car elle est ajoutée en dernier) afin de capturer chaque requête, y compris celles rejetées par la limitation de débit. Troisièmement, GZipMiddleware compresse les réponses de plus de 500 octets, ce qui réduit généralement de 70 à 80 % la taille de transfert HTML. Depuis Starlette 1.5.0, il ne compresse plus tout : une liste d’exclusion par défaut ignore désormais les charges utiles binaires et déjà compressées (archives gzip et zip, PNG, JPEG, WebP, GIF, AVIF, audio/*, video/*, polices WOFF et WOFF2, ainsi que text/event-stream), ce qui est souhaitable — recompresser un PNG consomme du CPU pour le rendre légèrement plus volumineux. Notez que la liste exclut délibérément image/*, de sorte que image/svg+xml reste compressé. Remplacez-la avec le paramètre réservé aux mots-clés exclude_content_types.28
Routage
Les routes se répartissent en deux catégories : les routes de page renvoient des documents HTML complets, tandis que les routes API renvoient des fragments JSON ou HTML.
# routes/pages.py — full HTML responses
from fastapi import APIRouter, Request
router = APIRouter()
@router.get("/about")
async def about(request: Request):
templates = request.app.state.templates
return templates.TemplateResponse("pages/about.html", {
"request": request,
"page_title": "About — Blake Crosley",
"page_description": "Designer, developer, dad.",
})
# routes/api.py — JSON or HTML fragment responses
@router.get("/api/quiz/{quiz_id}/step")
async def quiz_step(request: Request, quiz_id: str, answers: str = ""):
# Parse answers, compute next question or result
question = get_next_question(quiz_id, answers)
templates = request.app.state.templates
return templates.TemplateResponse("components/_quiz_step.html", {
"request": request,
"question": question,
"answers": answers,
"step": len(answers.split(",")) if answers else 0,
})
La distinction est importante pour HTMX. Les routes de page complète renvoient des documents qui étendent base.html. Les routes API renvoient des fragments HTML que HTMX insère dans des éléments DOM existants. Le même moteur de templates Jinja2 rend les deux — sans couche API séparée.
Injection de dépendances
Le système Depends() de FastAPI assure une séparation nette entre les gestionnaires de routes et la logique partagée :
from fastapi import Depends, Request
def get_templates(request: Request):
"""Get templates from app state."""
return request.app.state.templates
def get_current_locale(request: Request) -> str:
"""Get locale from middleware-set request state."""
return getattr(request.state, "locale", "en")
@router.get("/blog/{slug}")
async def blog_post(
request: Request,
slug: str,
templates=Depends(get_templates),
locale: str = Depends(get_current_locale),
):
post = load_post_by_slug(slug)
if not post:
raise HTTPException(404, "Post not found")
return templates.TemplateResponse("pages/blog/post.html", {
"request": request,
"post": post,
"locale": locale,
})
Les dépendances se composent. Une dépendance get_db peut dépendre de get_current_locale, qui dépend de la requête. FastAPI résout automatiquement la chaîne.
Paramètres Pydantic
La configuration utilise BaseSettings de Pydantic avec une priorité accordée aux variables d’environnement :
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
D1_WORKER_URL: str = ""
D1_AUTH_SECRET: str = ""
CLOUDFLARE_ACCOUNT_ID: str = ""
CLOUDFLARE_API_TOKEN: str = ""
ANALYTICS_PASSKEY: str = ""
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
Les variables d’environnement remplacent les valeurs du fichier .env. En production (Railway), les secrets sont définis comme variables d’environnement. En local, un fichier .env fournit des valeurs par défaut. La classe Settings valide les types au démarrage — un champ requis manquant échoue immédiatement plutôt qu’à l’exécution.
Modèles async
Les routes FastAPI sont async par défaut. Pour les opérations liées aux E/S (requêtes de base de données, requêtes HTTP, lectures de fichiers), async évite de bloquer la boucle d’événements :
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load translations into memory cache at startup
async with httpx.AsyncClient() as client:
for locale in SUPPORTED_LOCALES:
resp = await client.post(f"{D1_URL}/query", ...)
TRANSLATIONS[locale] = resp.json()["results"]
yield
# Cleanup on shutdown (if needed)
app = FastAPI(lifespan=lifespan)
Lifespan est désormais l’unique chemin de démarrage et d’arrêt. Starlette a atteint sa première version stable, 1.0, en mars 2026 (1.6.0 au 8 août) et a supprimé les hooks on_event, on_startup et on_shutdown, dépréciés depuis longtemps — lifespan (ci-dessus) est le seul mécanisme, et @app.route() / @app.websocket_route() ont cédé la place à Route / WebSocketRoute dans la liste routes. FastAPI 0.137.0 (14 juin 2026) refactorise ses propres composants internes de routage : router.routes n’est plus une liste plate d’objets APIRoute, mais un arbre de nœuds intermédiaires ; traitez-le donc comme un détail interne plutôt que comme un élément à parcourir. L’avantage est que les routes ajoutées à un routeur après include_router() sont désormais reflétées en direct, et qu’un sous-routeur peut être inclus avant que ses routes ne soient définies. FastAPI lui-même ne contraint pas Starlette à la branche 1.x : son exigence d’exécution est un simple plancher, starlette>=0.46.0, depuis la version 0.136.3 et reste inchangée jusqu’à 0.140.7 — aucune limite supérieure, et Starlette 0.4x la satisfait toujours. Les numéros de version 1.x dans les notes de version 0.137.0 sont des mises à jour dependabot du fichier de verrouillage des tests du dépôt lui-même, et non une contrainte d’exécution pour votre application.24 Rien de tout cela ne change les modèles de ce guide — il utilise lifespan et la déclaration de route standard partout — mais si vous maintenez des outils qui parcourent router.routes, ou si vous exécutez encore d’anciens gestionnaires @app.on_event, 0.137.0 / Starlette 1.0 introduisent des ruptures. FastAPI 0.137.2 (18 juin 2026) poursuit avec iter_route_contexts(), la méthode prise en charge pour énumérer les routes maintenant que router.routes est interne. FastAPI 0.138.0 (20 juin 2026) ajoute ensuite app.frontend("/", directory="dist") / router.frontend(...) pour servir un frontend statique compilé — utile si vous livrez une compilation SPA distincte, mais sans rapport avec l’approche de ce guide, sans compilation et rendue côté serveur (elle monte un répertoire dist/ au lieu de rendre HTML sur le serveur).25 FastAPI 0.139.0 (1er juillet 2026) l’étend avec la prise en charge des dépendances dans app.frontend() — par exemple, l’authentification automatique par cookie pour le frontend servi — en apportant au montage de frontend statique le même mécanisme Depends() que vous utilisez sur les routes API.26 FastAPI 0.141.0 (29 juillet 2026) ajoute app.frontend(check_dir="auto"), ce qui empêche fastapi dev d’échouer lorsque le répertoire de compilation n’existe pas encore — le cas courant où vous démarrez le serveur avant d’exécuter la compilation du frontend. FastAPI 0.141.1, publié le même jour, corrige des dépendances dans app.frontend() qui abandonnaient silencieusement les tâches d’arrière-plan et les en-têtes de réponse : une dépendance qui définissait un cookie ou planifiait une BackgroundTask voyait ce travail écarté sur le montage de frontend, alors qu’il fonctionnait normalement sur les routes API. Si vous avez adopté la prise en charge des dépendances de 0.139.0, la version 0.141.1 est celle qui la fait fonctionner comme le reste de l’application.29
FastAPI 0.140.0 met fin à une régression mémoire présente dans chaque version depuis novembre 2025 — mettez à niveau. La version du 24 juillet 2026 consiste en un unique refactoring aux effets disproportionnés. Dependant, l’objet interne que FastAPI construit pour chaque nœud du graphe de dépendances de chaque route, avait accumulé des attributs functools.cached_property à partir de 0.121.0 (3 novembre 2025) — ils étaient au nombre de dix dans 0.139.2. Une propriété mise en cache nécessite un __dict__ par instance pour y écrire son résultat ; le coût se multipliait donc dans chaque nœud de chaque graphe de l’application. La PR #16049 déplace cette logique hors de la classe vers des assistants au niveau du module (_get_cache_key(), _get_oauth_scopes(), _uses_scopes()) et déclare Dependant comme @dataclass(slots=True), en le laissant comme simple conteneur de données. L’exécution CodSpeed de FastAPI sur la PR fusionnée mesure le benchmark mémoire test_dependency_graph à 17,5 Mo → 1,1 Mo, soit une réduction de ×16 ; le rapport ayant déclenché ce travail décrivait un service de production qui restait sous environ 400 Mo avec 0.120.4 et rencontrait une erreur OOM avec 0.121.3. Toutes les versions recommandées par ce guide depuis lors — 0.137.x, 0.138.0, 0.139.2 — l’intégraient. Si votre application possède un arbre de dépendances profond ou étendu (Depends() imbriqués, schémas de sécurité, nombreux routeurs inclus), 0.140.0 offre un gain mémoire gratuit sans modifier le code de l’application.27
0.140.0 était le premier pas, pas l’ensemble du correctif — verrouillez 0.140.7 ou une version ultérieure. Trois jours après cette publication, le 27 juillet 2026, FastAPI a livré sept versions supplémentaires en cinq heures et demie : 0.140.1 à 0.140.7, chacune étant un refactoring du même mécanisme de dépendances. Le travail se divise en deux parties. Premièrement, l’arbre de dépendances aplati : FastAPI construisait et conservait auparavant une copie aplatie du graphe de dépendances de chaque route, et 0.140.2 cesse de la conserver ; 0.140.3, 0.140.5, 0.140.6 et 0.140.7 suppriment les derniers emplacements qui en reconstruisaient une — génération OpenAPI, champs de corps, paramètres de requête, puis OpenAPI à nouveau. La version 0.140.4 supprime une gestion administrative qui suivait des dépendances répétées que rien ne lisait. Deuxièmement, et seul changement ayant un seuil visible : 0.140.1 augmente le lru_cache des assistants de classification des appelables dans fastapi/dependencies/models.py, de 1 024 à 4 096 entrées, derrière une constante nommée _CALLABLE_CLASSIFICATION_CACHE_SIZE, car des utilisateurs ont signalé que les applications comptant plus de 1 024 dépendances distinctes faisaient constamment évincer le cache. Rien ici ne change une API que vous appelez, la mise à niveau se résume donc à une hausse de version. Deux réserves méritent d’être formulées clairement : le rythme signifie que cette branche évolue encore ; lisez donc les notes de version plutôt que de supposer que 0.140.7 en est l’aboutissement. Par ailleurs, FastAPI a ajouté, durant la même période, les benchmarks de dépendances OpenAPI qui mesurent ce travail (PR #16075) ; les chiffres publiés couvrent donc les dernières versions et non l’ensemble de cette séquence de sept versions.30
Les opérations liées au CPU (rendu Markdown, extraction CSS) peuvent utiliser des fonctions synchrones. FastAPI les exécute automatiquement dans un pool de threads lorsque le gestionnaire de route n’est pas déclaré async :
# Sync function — FastAPI runs it in a thread pool
@router.get("/blog/{slug}")
def blog_post(slug: str):
post = load_post_by_slug(slug) # CPU-bound Markdown parsing
return templates.TemplateResponse(...)
La règle : si la fonction attend des E/S, rendez-la async. Si elle effectue du travail CPU, laissez-la synchrone. Ne mélangez pas await et des appels bloquants dans la même fonction.9
Templates Jinja2
Héritage de templates
Le système d’héritage de Jinja2 remplace la composition par composants de React avec un modèle plus simple. Un template de base définit le squelette de la page. Les templates enfants remplissent des blocs nommés :
<!-- base.html — the skeleton -->
<!DOCTYPE html>
<html lang="{{ lang_attr() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title | default("Blake Crosley") }}</title>
<meta name="description" content="{{ page_description | default('...') }}">
<!-- CSS — single file, no preprocessor -->
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
<!-- JSON-LD structured data -->
<script type="application/ld+json">
{ "@context": "https://schema.org", "@graph": [...] }
</script>
{% block head %}{% endblock %}
</head>
<body>
<header class="header">...</header>
<main id="main" role="main">
{% block content %}{% endblock %}
</main>
<footer class="footer">...</footer>
<!-- Scripts deferred for performance -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
<!-- pages/about.html — fills the blocks -->
{% extends "base.html" %}
{% block head %}
<script type="application/ld+json">
{ "@type": "AboutPage", "name": "About Blake Crosley", ... }
</script>
{% endblock %}
{% block content %}
<section class="hero">
<h1>About</h1>
<p>Designer, developer, dad.</p>
</section>
{% endblock %}
La directive {% extends %} établit une relation parent-enfant. Le template enfant ne définit que les blocs qu’il souhaite remplacer. Tout le reste — le <head>, l’en-tête, le pied de page, les balises script — provient de la base. C’est une composition par soustraction plutôt que par construction.
Le global asset()
Les ressources statiques utilisent le versionnage par hash de contenu pour l’invalidation du cache :
# cache_assets.py
def build_asset_map(static_dir: Path) -> dict[str, str]:
"""Compute MD5 hashes of all static files at startup."""
asset_map = {}
for filepath in static_dir.rglob("*"):
if filepath.is_file():
rel_path = str(filepath.relative_to(static_dir))
content_hash = hashlib.md5(filepath.read_bytes()).hexdigest()[:10]
asset_map[rel_path] = content_hash
return asset_map
def make_asset_url(asset_map: dict, path: str) -> str:
"""Generate versioned URL: /static/css/styles.css?v=a3f8b2c1d0"""
clean_path = path.lstrip("/")
version = asset_map.get(clean_path, "0")
return f"/static/{clean_path}?v={version}"
Dans le template : {{ asset('css/styles.css') }} produit /static/css/styles.css?v=a3f8b2c1d0. Le hash change lorsque le fichier est modifié, invalidant ainsi le cache CDN. Cela remplace la stratégie de noms de fichiers [contenthash] de webpack par 30 lignes de Python calculées au démarrage.
Include pour les partiels réutilisables
Les composants qui se répètent sur plusieurs pages utilisent {% include %} :
<!-- base.html -->
{% include "components/_language_switcher.html" %}
<!-- components/_language_switcher.html -->
{%- set current = current_locale() -%}
{%- set locales = all_locales() -%}
<div class="language-switcher"
x-data="{ open: false }"
@click.away="open = false">
<button @click="open = !open" :aria-expanded="open">
{{ current_locale_native() }}
</button>
<ul class="language-switcher-menu"
:class="{ 'is-open': open }"
x-cloak>
{% for locale in locales %}
<li>
<a href="{{ locale_url(request.url.path, locale.code) }}"
hreflang="{{ locale.code }}">
{{ locale.native }}
</a>
</li>
{% endfor %}
</ul>
</div>
Le préfixe underscore (_language_switcher.html) est une convention indiquant un partiel — un fragment de template qui n’est pas destiné à être rendu de manière autonome. Ce composant utilise à la fois Alpine.js (pour le basculement du menu déroulant) et Jinja2 (pour la liste des locales). La frontière est nette : Alpine.js gère l’état ouvert/fermé, Jinja2 gère les données.
Macros pour les composants réutilisables
Les macros sont les fonctions de Jinja2 — des blocs de template réutilisables avec des paramètres :
<!-- components/_macros.html -->
{% macro card(title, description, href, badge=None) %}
<article class="card">
<a href="{{ href }}" class="card__link">
{% if badge %}
<span class="card__badge">{{ badge }}</span>
{% endif %}
<h3 class="card__title">{{ title }}</h3>
{% if description %}
<p class="card__description">{{ description }}</p>
{% endif %}
</a>
</article>
{% endmacro %}
{% macro optimized_image(image_config, loading="lazy") %}
{% if image_config.get("svg") %}
<img src="{{ image_config.svg }}"
width="{{ image_config.width }}"
height="{{ image_config.height }}"
alt="{{ image_config.alt }}">
{% else %}
<picture>
<source type="image/webp"
srcset="{{ image_config.webp_srcset }}"
sizes="(max-width: 768px) 100vw, 50vw">
<img src="{{ image_config.fallback }}"
width="{{ image_config.width }}"
height="{{ image_config.height }}"
alt="{{ image_config.alt }}"
loading="{{ loading }}">
</picture>
{% endif %}
{% endmacro %}
Importez et utilisez les macros dans les templates de page :
{% from "components/_macros.html" import card, optimized_image %}
<section class="projects">
{% for project in projects %}
{{ card(
title=project.title,
description=project.description,
href=project.link,
badge="New" if project.is_new else None
) }}
{% endfor %}
</section>
Les macros remplacent les composants React pour les modèles de présentation. Elles acceptent des paramètres, prennent en charge les valeurs par défaut et se composent avec d’autres macros. La différence : les macros s’exécutent une seule fois côté serveur et produisent du HTML statique. Les composants React s’exécutent côté client et maintiennent un état. Pour l’affichage de contenu, les macros sont l’outil approprié.
Contexte de template et globals
Les globals Jinja2 sont des fonctions disponibles dans chaque template sans passage explicite :
# In main.py — register globals
templates.env.globals["asset"] = lambda path: make_asset_url(_asset_map, path)
templates.env.globals["csrf_token"] = generate_csrf_token
templates.env.globals["analytics_script"] = analytics.tracking_script
Le global asset() génère des URL versionnées. Le global csrf_token() génère des jetons CSRF à la volée. Le global analytics_script() injecte le snippet de suivi. Ces fonctions sont appelables dans n’importe quel template sans que le gestionnaire de route ne les transmette explicitement.
Pour l’i18n, la mise en place est plus élaborée — les fonctions de traduction ont besoin d’accéder à la locale de la requête en cours :
# i18n/jinja.py
def setup_i18n_jinja(env):
"""Register translation functions as Jinja2 globals."""
env.globals["_"] = get_translation # _('ui.nav.about')
env.globals["locale_prefix"] = get_locale_prefix # '/ja' or ''
env.globals["current_locale"] = get_current_locale
env.globals["all_locales"] = get_all_locales
env.globals["alternate_urls"] = get_alternate_urls
env.globals["lang_attr"] = get_lang_attr # 'ja' for HTML lang
env.globals["og_locale"] = get_og_locale # 'ja_JP' for og:locale
env.globals["jsonld_lang"] = get_jsonld_lang # 'ja-JP' for JSON-LD
Chaque fonction lit la locale depuis la variable de contexte de requête définie par le middleware de locale. Le template appelle {{ _('ui.nav.about') }} et obtient la chaîne traduite pour la locale de la requête en cours, sans aucun paramètre de locale explicite.
Blocs conditionnels
Le système de blocs de Jinja2 prend en charge les remplacements conditionnels :
<!-- base.html -->
{% block head %}{% endblock %}
<!-- pages/blog/post.html -->
{% block head %}
<script type="application/ld+json">
{
"@type": "Article",
"headline": "{{ post.meta.title }}",
"author": { "@id": "https://blakecrosley.com/#person" },
"datePublished": "{{ post.meta.date.isoformat() }}",
"dateModified": "{{ post.meta.updated.isoformat() if post.meta.updated else post.meta.date.isoformat() }}"
}
</script>
{% if post.meta.scripts %}
{% for script in post.meta.scripts %}
<script defer src="{{ asset(script.lstrip('/static/')) }}"></script>
{% endfor %}
{% endif %}
{% if post.meta.styles %}
{% for style in post.meta.styles %}
<link rel="stylesheet" href="{{ asset(style.lstrip('/static/')) }}">
{% endfor %}
{% endif %}
{% endblock %}
Les articles de blog déclarent leurs dépendances dans le frontmatter YAML (scripts: ["/static/js/boids.js"]). Le template les inclut de manière conditionnelle. Les pages qui n’ont pas besoin de scripts ou de styles supplémentaires n’en embarquent aucun — pas de code mort, pas d’imports inutilisés.
Filtres personnalisés
Les filtres Jinja2 transforment les données lors du rendu. Le filtre sanitize prévient les attaques XSS dans le contenu généré par les utilisateurs :
import nh3
ALLOWED_TAGS = {"a", "b", "blockquote", "br", "code", "em", "h1", "h2",
"h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol",
"p", "pre", "span", "strong", "table", "td", "th", "tr", "ul"}
def sanitize_html(value: str) -> str:
"""Sanitize HTML to prevent XSS attacks."""
if not value:
return ""
return nh3.clean(
value,
tags=ALLOWED_TAGS,
attributes={"a": {"href", "title"}, "img": {"src", "alt"}},
link_rel="noopener noreferrer",
)
templates.env.filters["sanitize"] = sanitize_html
Dans les templates : {{ user_content | sanitize }}. La bibliothèque nh3 est un assainisseur HTML écrit en Rust — rapide et sûr. Elle supprime toute balise ou tout attribut absent de la liste autorisée, empêchant ainsi les attaques XSS stockées même si le contenu provient d’une source non fiable.10
Plongée dans HTMX
HTMX permet à n’importe quel élément HTML d’émettre des requêtes HTTP et d’injecter la réponse dans le DOM. L’idée architecturale clé est la suivante : le HTML rendu côté serveur constitue l’API. Le serveur renvoie la représentation finale. Pas de rendu côté client, pas de sérialisation JSON, pas d’hydratation.
Attributs fondamentaux
| Attribut | Rôle | Exemple |
|---|---|---|
hx-get |
Émettre une requête GET | hx-get="/search?q=term" |
hx-post |
Émettre une requête POST | hx-post="/contact" |
hx-target |
Où placer la réponse | hx-target="#results" |
hx-swap |
Comment insérer la réponse | hx-swap="innerHTML" (par défaut), outerHTML, beforeend |
hx-trigger |
Ce qui déclenche la requête | hx-trigger="click", keyup changed delay:300ms, load |
hx-indicator |
Élément à afficher pendant la requête | hx-indicator="#spinner" |
hx-push-url |
Mettre à jour l’URL du navigateur | hx-push-url="true" |
hx-replace-url |
Remplacer l’URL sans entrée dans l’historique | hx-replace-url="true" |
Pattern 1 : Quiz interactif (état serveur multi-étapes)
blakecrosley.com intègre un quiz interactif qui guide les utilisateurs dans le choix d’un outil. L’intégralité de l’état du quiz réside sur le serveur — aucune gestion d’état côté client :
<!-- _quiz_container.html — chargement initial -->
<div hx-get="/api/quiz/claude-vs-codex/step?answers="
hx-trigger="load"
hx-swap="innerHTML"
id="quiz-wrapper">
<p>Loading quiz...</p>
</div>
<!-- _quiz_step.html — chaque question -->
<div class="quiz-step" id="quiz-container">
<p>Question {{ step }} of {{ total }}</p>
<h3>{{ question.question }}</h3>
<div class="quiz-step__options">
{% for opt in question.options %}
<button class="quiz-step__btn"
hx-get="/api/quiz/claude-vs-codex/step?answers={{ answers }},{{ opt.value }}"
hx-target="#quiz-container"
hx-swap="outerHTML">
{{ opt.label }}
</button>
{% endfor %}
</div>
</div>
Chaque clic sur un bouton envoie les réponses accumulées sous forme de paramètre de requête. Le serveur calcule la question suivante ou le résultat final en fonction de l’historique des réponses. L’état s’accumule dans l’URL — ni cookies, ni sessions, ni JavaScript côté client. Le quiz progresse via des swaps outerHTML : chaque réponse remplace l’intégralité de l’élément d’étape du quiz.
Pattern 2 : Liste d’articles paginée
La page d’écriture utilise HTMX pour une pagination fluide qui met à jour l’URL :
<!-- Pagination link -->
<a href="/writing?page=2&category=Engineering"
hx-get="/writing?page=2&category=Engineering"
hx-target="#writing-content"
hx-swap="innerHTML"
hx-replace-url="true"
hx-indicator="#writing-loading"
aria-label="Go to page 2">
2
</a>
Quatre attributs travaillent de concert :
hx-getémet la requête vers la même URL que lehref(amélioration progressive — fonctionne sans JavaScript)hx-targetplace la réponse dans le conteneur#writing-contenthx-replace-url="true"met à jour l’URL du navigateur sans ajouter d’entrée dans l’historiquehx-indicatoraffiche un indicateur de chargement pendant la requête
Le serveur détecte les requêtes HTMX via l’en-tête HX-Request et renvoie uniquement le fragment de la liste d’articles au lieu de la page complète. C’est pourquoi le middleware des en-têtes de sécurité ajoute Vary: HX-Request — afin que les caches CDN stockent séparément la page complète et le fragment.11
Pattern 3 : Recherche avec temporisation
<input type="search" name="q"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
hx-indicator="#search-spinner" />
<div id="results"></div>
L’attribut hx-trigger combine trois modificateurs :
keyupse déclenche au relâchement de la touchechangedne se déclenche que si la valeur a réellement changé (évite les requêtes dupliquées dues aux touches de modification)delay:300msapplique une temporisation — attend 300 ms après le dernier keyup avant de déclencher la requête
Le serveur renvoie un fragment HTML rendu :
@router.get("/api/search")
async def search(request: Request, q: str = ""):
results = search_content(q)
return templates.TemplateResponse("components/_search_results.html", {
"request": request,
"results": results,
"query": q,
})
Aucun état côté client. Aucune bibliothèque de temporisation. Aucun useEffect. Le template rend les résultats, HTMX les injecte, et le serveur reste l’unique source de vérité.
Pattern 4 : Swaps hors bande (OOB)
Parfois, une seule action serveur doit mettre à jour plusieurs éléments du DOM. Le mécanisme de swap hors bande de HTMX gère cela sans orchestration côté client :
<!-- Server returns multiple elements in one response -->
<!-- Primary target: swapped normally via hx-target -->
<div id="cart-items">
<ul>
<li>Widget A — $29.99</li>
<li>Widget B — $14.99</li>
</ul>
</div>
<!-- OOB target: swapped independently via hx-swap-oob -->
<span id="cart-count" hx-swap-oob="true">2 items</span>
<span id="cart-total" hx-swap-oob="true">$44.98</span>
L’attribut hx-swap-oob="true" indique à HTMX de trouver l’élément par son id n’importe où dans le DOM et de le remplacer, indépendamment du hx-target. Cela remplace le pattern React « remonter l’état » — le serveur calcule tous les états dérivés et envoie le HTML final pour chaque élément en une seule réponse.
Un formulaire de contact illustre bien ce mécanisme : la soumission du formulaire pourrait remplacer le corps du formulaire par un message de succès tout en mettant simultanément à jour un badge de notification via un swap OOB.
Pattern 5 : Liens boostés
HTMX peut « booster » les liens de navigation standard pour utiliser AJAX au lieu de chargements de page complets :
<nav hx-boost="true">
<a href="/about">About</a>
<a href="/writing">Writing</a>
<a href="/guides">Guides</a>
</nav>
Avec hx-boost="true", cliquer sur un lien récupère la page via AJAX, remplace le contenu du <body> et met à jour l’URL — sans rechargement complet de la page. L’historique du navigateur fonctionne normalement (boutons précédent/suivant). Si JavaScript échoue, les liens fonctionnent comme une navigation standard.
L’avantage réside dans la performance perçue : la navigation boostée semble instantanée car le navigateur n’a pas besoin de ré-analyser le CSS, de réévaluer les scripts ni de recalculer la mise en page. Seul le contenu du <body> change. Les liens boostés conviennent parfaitement aux éléments de navigation principale, ce qui confère aux transitions de page l’apparence d’une application monopage sans l’architecture SPA.
Pattern 6 : En-têtes de requête HTMX
HTMX envoie des en-têtes personnalisés avec chaque requête :
| En-tête | Valeur | Cas d’utilisation |
|---|---|---|
HX-Request |
true |
Détecter les requêtes HTMX côté serveur |
HX-Target |
ID de l’élément | Savoir quel élément recevra la réponse |
HX-Trigger |
ID de l’élément | Savoir quel élément a déclenché la requête |
HX-Current-URL |
URL complète | Connaître la page actuelle de l’utilisateur |
Le serveur peut utiliser HX-Request pour renvoyer des réponses différentes :
@router.get("/writing")
async def writing(request: Request, page: int = 1, category: str = None):
posts = load_all_posts(page=page, category=category)
context = {"request": request, "posts": posts, "current_page": page}
# HTMX request: return only the post list fragment
if request.headers.get("HX-Request"):
return templates.TemplateResponse(
"pages/writing/_post_list.html", context
)
# Normal request: return the full page
return templates.TemplateResponse("pages/writing/index.html", context)
Ce pattern de double réponse est au cœur de l’architecture. Un chargement de page complet renvoie le document entier (template de base + contenu de la page). Une navigation HTMX renvoie uniquement le contenu modifié. C’est le serveur qui décide, pas le client.
Pattern 7 : Amélioration progressive
Chaque lien HTMX sur blakecrosley.com inclut un attribut href standard :
<a href="/writing?page=2"
hx-get="/writing?page=2"
hx-target="#writing-content"
hx-swap="innerHTML">
Next Page
</a>
Si JavaScript ne parvient pas à se charger, le href fonctionne comme un lien classique. Si HTMX se charge, il intercepte le clic et effectue un swap AJAX. C’est l’amélioration progressive : le site fonctionne sans JavaScript, et HTMX enrichit l’expérience lorsqu’il est disponible.
Pattern 8 : États de chargement
<button hx-post="/api/contact"
hx-target="#form-result"
hx-indicator="#submit-spinner">
<span id="submit-spinner" class="htmx-indicator">Sending...</span>
<span>Send Message</span>
</button>
HTMX ajoute la classe htmx-request à l’élément déclencheur pendant les requêtes. L’attribut hx-indicator pointe vers un élément qui devient visible pendant la requête. Le style se gère en CSS :
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline;
}
Aucune gestion d’état de chargement. Aucun useState(false). Aucun setLoading(true). CSS gère la visibilité, HTMX gère le basculement de classe.
Patterns Alpine.js
Alpine.js comble le vide laissé par HTMX : l’état côté client qui n’a jamais besoin de communiquer avec le serveur. Lorsqu’un utilisateur clique sur un menu déroulant et que celui-ci s’ouvre, cet état n’existe que dans le navigateur. Alpine.js le gère avec des attributs HTML.
La règle de frontière
La frontière entre HTMX et Alpine.js est précise :
| Type d’état | Outil | Exemple |
|---|---|---|
| Nécessite des données serveur | HTMX | Résultats de recherche, validation de formulaire, pagination |
| N’existe que dans le navigateur | Alpine.js | Ouverture/fermeture de menu déroulant, bascule du menu mobile, visibilité de modale |
| Combine les deux | Les deux | Sélecteur de langue (bascule Alpine.js, navigation de type HTMX) |
Navigation mobile
Le template de base enveloppe l’ensemble de l’en-tête dans un composant Alpine.js :
<div x-data="{ navOpen: false, langOpen: false }"
@keydown.escape.window="navOpen = false; langOpen = false">
<!-- Mobile hamburger button -->
<button @click="navOpen = !navOpen; langOpen = false"
:aria-expanded="navOpen"
:class="navOpen ? 'nav__toggle is-open' : 'nav__toggle'"
aria-label="Toggle navigation">
<span class="nav__toggle-icon">
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
</span>
</button>
<!-- Mobile menu panel -->
<div class="mobile-menu" x-show="navOpen" x-cloak>
<nav class="mobile-menu__nav">
<a href="/about" @click="navOpen = false">About</a>
<a href="/#work" @click="navOpen = false">Work</a>
<a href="/writing" @click="navOpen = false">Writing</a>
</nav>
</div>
</div>
Patterns Alpine.js clés :
x-datadéclare la portée du composant et l’état initialx-showbascule la visibilité selon l’état (utilise CSSdisplay: none)x-cloakmasque l’élément jusqu’à l’initialisation d’Alpine.js (évite le flash de contenu non stylisé)@clicklie des gestionnaires de clic avec des expressions:aria-expanded(raccourci pourx-bind:aria-expanded) définit dynamiquement les attributs@keydown.escape.windowécoute la touche Échap globalement pour fermer les panneaux
Composant menu déroulant
Le sélecteur de langue utilise Alpine.js pour l’état de bascule avec @click.away pour la fermeture au clic extérieur :
<div x-data="{ open: false }"
@click.away="open = false"
@keydown.escape.window="open = false">
<button @click="open = !open"
:aria-expanded="open"
aria-haspopup="listbox">
English
<svg :class="{ 'rotated': open }">...</svg>
</button>
<ul :class="{ 'is-open': open }"
:aria-hidden="!open"
role="listbox"
x-cloak>
<li role="option">
<a href="/ja/about">日本語</a>
</li>
<!-- more languages -->
</ul>
</div>
Le modificateur @click.away ferme le menu déroulant lors d’un clic à l’extérieur. Alpine.js gère cela avec un seul attribut — pas d’enregistrement d’écouteur d’événements, pas de nettoyage, pas de gestion de références.
Quand utiliser Alpine.js plutôt que du JavaScript natif
Alpine.js est approprié lorsque :
- L’état est limité à un seul élément DOM (menu déroulant, modale, bascule)
- Les interactions sont binaires ou simples (ouvrir/fermer, afficher/masquer, basculer)
- Plusieurs éléments doivent réagir au même changement d’état
- Les attributs d’accessibilité doivent rester synchronisés avec la visibilité
Le JavaScript natif est approprié lorsque :
- L’interaction implique des calculs complexes (visualisations, simulations)
- Le composant possède sa propre boucle de rendu (canvas, animation)
- La performance est critique (Alpine.js ajoute une surcharge par composant
x-data) - La logique dépasse 20-30 lignes d’expressions Alpine.js
blakecrosley.com utilise Alpine.js pour la navigation, le changement de langue et les bascules de contenu. Les 20 composants interactifs du blog (simulation de boids, visualiseur de code de Hamming, etc.) utilisent du JavaScript natif car ils nécessitent du rendu canvas et des machines à états complexes.
Exemple de bout en bout : filtrage par catégorie sur /writing
Cette section retrace une fonctionnalité réelle du code de production à travers chaque couche : route, template, interaction HTMX, sécurité, mise en cache et rendu final. La fonctionnalité : des onglets de catégories sur la page d’écriture qui filtrent les articles de blog sans rechargement complet de la page.
La route (app/routes/pages.py:508)
async def writing_listing(request: Request, page: int = 1, category: str | None = None):
"""Writing page — blog posts and external publications."""
templates = get_templates(request)
markdown_posts = load_all_posts(published_only=True)
all_posts = CUSTOM_BLOG_POSTS + markdown_posts
# Filter by category if specified
if category and category in CATEGORY_MAP:
display_name = CATEGORY_MAP[category]
all_posts = [
p for p in all_posts
if _get_post_category(p).lower() == display_name.lower()
]
# Pagination
total_pages = max(1, (len(all_posts) + POSTS_PER_PAGE - 1) // POSTS_PER_PAGE)
page = max(1, min(page, total_pages))
paginated = all_posts[(page - 1) * POSTS_PER_PAGE : page * POSTS_PER_PAGE]
template_context = {
"request": request,
"posts": paginated,
"categories": categories,
"current_category": category,
"current_page": page,
"total_pages": total_pages,
# ... SEO: canonical, prev/next URLs
}
# HTMX partial: return just the post list fragment
if request.headers.get("HX-Request"):
return templates.TemplateResponse(
"pages/writing/_post_list.html",
template_context,
)
# Full page for direct navigation
return templates.TemplateResponse(
"pages/writing/index.html",
template_context,
)
La vérification de l’en-tête HX-Request constitue le pattern central : même route, mêmes données, template différent. HTMX reçoit un fragment. Les navigateurs reçoivent la page complète.
Les onglets de catégorie (HTMX)
<!-- Category filter tabs -->
<nav class="writing-categories">
<a href="/writing"
hx-get="/writing"
hx-target="#post-list"
hx-push-url="true"
class="category-tab {% if not current_category %}active{% endif %}">
All ({{ total_posts }})
</a>
{% for cat in categories %}
<a href="/writing?category={{ cat.slug }}"
hx-get="/writing?category={{ cat.slug }}"
hx-target="#post-list"
hx-push-url="true"
class="category-tab {% if current_category == cat.slug %}active{% endif %}">
{{ cat.name }} ({{ cat.count }})
</a>
{% endfor %}
</nav>
<div id="post-list">
{% include "pages/writing/_post_list.html" %}
</div>
Chaque onglet possède à la fois un href (fonctionne sans JavaScript) et un hx-get (remplace uniquement la liste d’articles). hx-push-url met à jour l’URL du navigateur afin que la vue filtrée soit partageable et mémorisable dans les favoris.
Le partial (pages/writing/_post_list.html)
Le partial s’affiche de manière identique, qu’il soit inclus au chargement de la page ou injecté par HTMX :
{% for post in posts %}
<article class="post-card">
<a href="{{ locale_prefix() }}/blog/{{ post.meta.slug }}">
<h3>{{ post.meta.title }}</h3>
<p>{{ post.meta.description }}</p>
<time>{{ post.meta.date }}</time> · {{ post.reading_time }}m
</a>
</article>
{% endfor %}
Aucun balisage HTMX spécifique dans le partial. Aucune logique de rendu côté client. Le même HTML fonctionne pour le chargement initial de la page et pour chaque filtre ultérieur.
Sécurité
Les valeurs de catégorie sont validées par rapport à CATEGORY_MAP (un dictionnaire côté serveur) avant le filtrage. Les catégories invalides sont ignorées, jamais renvoyées en écho. Aucune entrée utilisateur n’est interpolée dans du SQL ou du HTML. L’en-tête CSP bloque les scripts en ligne.
Mise en cache
Les réponses de catégorie sont dynamiques (pas de cache CDN). En revanche, les ressources statiques (CSS, HTMX, Alpine.js) sont hashées par contenu et mises en cache indéfiniment après le premier chargement. Les changements de catégorie suivants ne transfèrent que le partial HTML (~3-5 Ko) — ni CSS, ni JS, ni images ne sont re-téléchargés.
Ce que cela démontre
Une fonctionnalité, du vrai code de production, zéro outil de build. Le serveur filtre et génère le HTML. HTMX remplace la liste d’articles. Alpine.js n’intervient pas (aucun état client nécessaire). L’URL se met à jour pour le partage. Amélioration progressive : les onglets fonctionnent comme de simples liens sans JavaScript. Total de JavaScript personnalisé pour cette fonctionnalité : zéro ligne.
Extensions optionnelles
Les sections suivantes couvrent des patterns qui complètent la stack principale mais ne sont pas utilisés sur blakecrosley.com. Ils sont inclus car ils représentent les ajouts les plus courants que les équipes adoptent avec cette architecture.
Bootstrap 5 sans Sass
Remarque : blakecrosley.com utilise du CSS brut avec des propriétés personnalisées — pas de Bootstrap. Cette section présente Bootstrap 5 comme option pour les équipes souhaitant un framework utilitaire sans étape de build. Le CSS compilé de Bootstrap peut être chargé depuis un CDN ou intégré à votre feuille de styles. Les patterns ci-dessous sont génériques et fonctionnent avec l’approche HTMX + Alpine.js décrite dans les sections précédentes.
Bootstrap 5 a abandonné jQuery comme dépendance et prend en charge l’utilisation autonome du CSS. Ni Sass, ni PostCSS, ni aucun outil de build ne sont nécessaires pour utiliser le système de grille et les classes utilitaires de Bootstrap.
Auto-hébergement sans CDN
blakecrosley.com auto-héberge toutes les bibliothèques tierces :
<!-- base.html — no CDN, no external requests -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
L’auto-hébergement élimine les dépendances externes, empêche les pannes de CDN de casser le site et permet un cache immuable avec des URLs à empreinte de contenu. Téléchargez le CSS compilé de Bootstrap (pas les sources Sass) et placez-le dans static/css/vendor/.
Système de grille
La grille Bootstrap fonctionne avec de simples classes HTML :
<div class="container">
<div class="row">
<div class="col-12 col-md-8">
<article>Main content</article>
</div>
<div class="col-12 col-md-4">
<aside>Sidebar</aside>
</div>
</div>
</div>
Pas de mixins Sass. Pas de @include make-col(). Le CSS compilé inclut les classes de grille responsive. Pour des breakpoints personnalisés au-delà des valeurs par défaut de Bootstrap, écrivez de simples media queries en CSS.
Surcharges en CSS brut
Surchargez les valeurs par défaut de Bootstrap avec des propriétés personnalisées CSS et des sélecteurs standard :
/* Custom design tokens — no Sass, no Tailwind */
:root {
--color-bg-dark: #000000;
--color-text-primary: #ffffff;
--color-text-secondary: rgba(255, 255, 255, 0.65);
--color-text-tertiary: rgba(255, 255, 255, 0.40);
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--spacing-lg: 2rem;
--gutter: 48px;
--font-size-lg: 1.25rem;
}
/* Responsive override — the browser reads this at runtime */
@media (max-width: 768px) {
:root {
--gutter: var(--spacing-md); /* 48px → 24px on mobile */
}
}
/* Override Bootstrap's default body styles */
body {
background: var(--color-bg-dark);
color: var(--color-text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
Les propriétés personnalisées CSS se propagent dans le DOM, héritent des éléments parents et réagissent aux media queries à l’exécution. Les variables Sass, elles, se compilent en valeurs statiques et disparaissent. Cette distinction est cruciale pour la gestion des thèmes : modifier une seule propriété personnalisée peut mettre à jour chaque valeur dérivée sans recompilation.12
Classes utilitaires vs. CSS de composants
Utilisez les classes utilitaires Bootstrap pour les espacements et mises en page ponctuels. Utilisez le CSS de composants pour les patterns récurrents :
<!-- Bootstrap utility for one-off spacing -->
<div class="mt-4 mb-3 px-2">One-off layout</div>
<!-- Component class for repeated patterns -->
<article class="writing__item">
<h3 class="writing__item-title">Post Title</h3>
<p class="writing__item-description">Description</p>
</article>
/* Component CSS — BEM naming, reusable */
.writing__item {
padding: var(--spacing-md);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.15s ease;
}
.writing__item:hover {
background: rgba(255, 255, 255, 0.03);
}
.writing__item-title {
font-size: var(--font-size-lg);
margin-bottom: 0.5rem;
}
Le principe : les utilitaires Bootstrap pour la mécanique de mise en page (marges, padding, flexbox). Le CSS personnalisé pour l’identité visuelle (couleurs, typographie, animations). Ne mélangez jamais classes utilitaires et styles de composants pour une même responsabilité.
i18n et localisation
blakecrosley.com propose du contenu en 10 langues : anglais, japonais, coréen, chinois simplifié, chinois traditionnel, allemand, français, espagnol, polonais et portugais (brésilien).
Routage de locale basé sur l’URL
La locale figure dans le chemin de l’URL : /about (anglais), /ja/about (japonais), /zh-Hans/about (chinois simplifié). L’anglais est la langue par défaut et ne comporte aucun préfixe.
# i18n/config.py
SUPPORTED_LOCALES = [
"en", "zh-Hans", "zh-Hant", "fr", "de", "ja", "ko", "pl", "pt-BR", "es"
]
DEFAULT_LOCALE = "en"
Le middleware de locale extrait la locale depuis le chemin de l’URL :
# i18n/middleware.py
class LocaleMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
path = request.url.path
# Check if path starts with a supported locale
for locale in SUPPORTED_LOCALES:
if path.startswith(f"/{locale}/") or path == f"/{locale}":
request.state.locale = locale
# Strip locale prefix for route matching
request.scope["path"] = path[len(f"/{locale}"):]
break
else:
request.state.locale = DEFAULT_LOCALE
response = await call_next(request)
return response
Le middleware supprime le préfixe de locale avant la résolution des routes. Les gestionnaires de routes n’ont donc pas besoin de chemins spécifiques à chaque locale — /about gère aussi bien l’anglais (/about) que le japonais (/ja/about), car le middleware normalise le chemin.
Fonctions de traduction dans les templates
Les variables globales Jinja2 fournissent des fonctions de traduction :
<!-- Template usage -->
<h3>{{ _('ui.footer.navigate') | default('Navigate') }}</h3>
<a href="{{ locale_prefix() }}/about">
{{ _('ui.nav.about') | default('About') }}
</a>
La fonction _() recherche une clé de traduction dans le cache mémoire. Le filtre | default() fournit le texte anglais de secours si la traduction est manquante. La fonction locale_prefix() renvoie le préfixe d’URL pour la locale courante ("" pour l’anglais, "/ja" pour le japonais).
Balises hreflang
Chaque page inclut des balises hreflang pour toutes les locales prises en charge :
<!-- Generated in base.html -->
{% for alt in alternate_urls(request.url.path) %}
<link rel="alternate" hreflang="{{ alt.hreflang }}" href="{{ alt.url }}">
{% endfor %}
Ce qui produit :
<link rel="alternate" hreflang="en" href="https://blakecrosley.com/about">
<link rel="alternate" hreflang="ja" href="https://blakecrosley.com/ja/about">
<link rel="alternate" hreflang="zh-Hans" href="https://blakecrosley.com/zh-Hans/about">
<!-- ... all 10 locales -->
<link rel="alternate" hreflang="x-default" href="https://blakecrosley.com/about">
Les moteurs de recherche utilisent hreflang pour afficher la bonne version linguistique dans les résultats. L’entrée x-default pointe vers la version anglaise comme solution de repli.13
Stockage des traductions et cache mémoire
Les traductions sont stockées dans Cloudflare D1 (SQLite en périphérie) et chargées dans un cache mémoire via le gestionnaire lifespan :
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load translations into memory at startup
for locale in SUPPORTED_LOCALES:
data = await fetch_translations(locale)
TRANSLATIONS[locale] = data
yield
app = FastAPI(lifespan=lifespan)
Le cache mémoire évite les requêtes à la base de données à chaque rendu de page. Les mises à jour de traduction nécessitent un rafraîchissement du cache (déclenché via un endpoint d’administration ou un déploiement). Cette architecture sacrifie la fraîcheur au profit de la performance — les traductions changent rarement, tandis que les rendus de page surviennent à chaque requête.
Surveillance de la santé
blakecrosley.com inclut un endpoint de vérification de santé i18n qui surveille la couverture de traduction par locale :
@app.get("/health/i18n")
async def health_i18n():
cache = get_translation_cache()
result = {
"status": "healthy",
"cache_loaded": cache.is_loaded,
"locales": {},
"alerts": [],
}
# Check coverage for each locale
for locale in SUPPORTED_LOCALES:
coverage = await calculate_coverage(locale, en_count)
result["locales"][locale] = {"coverage": round(coverage, 2)}
if coverage < 99.5:
result["alerts"].append(
f"{locale}: {coverage:.1f}% coverage (threshold: 99.5%)"
)
result["status"] = "warning"
return result
Le seuil de couverture à 99,5 % détecte les traductions manquantes avant que les utilisateurs ne rencontrent des chaînes non traduites. L’endpoint de santé s’intègre au système de surveillance de Railway pour alerter lorsque la couverture diminue — par exemple, après l’ajout de nouvelles chaînes d’interface qui n’ont pas encore été traduites.
Rendu de contenu adapté à la locale
Les articles de blog et les guides prennent en charge les traductions par locale des métadonnées et du contenu :
# In route handler
translated = get_blog_translation(post.meta.slug, locale)
return templates.TemplateResponse("pages/blog/post.html", {
"request": request,
"post": post,
"translated_title": translated.title if translated else post.meta.title,
"translated_description": translated.description if translated else post.meta.description,
})
<!-- In template -->
<h1>{{ translated_title }}</h1>
<p class="post__description">{{ translated_description }}</p>
<!-- Body content falls back to English if translation unavailable -->
{{ post.html | sanitize | safe }}
Le principe est constant : privilégier le contenu traduit, puis se rabattre sur l’anglais. Cela permet une traduction partielle — un utilisateur japonais voit les titres et descriptions traduits même si le corps complet de l’article reste en anglais. Le filtre Jinja2 | default() encode ce schéma en un seul pipe :
{{ translated.title if translated else post.meta.title }}
Traduction des données de locale
Le contenu statique comme les descriptions de projets et les libellés de navigation est traduit via des fonctions utilitaires qui conservent la même structure de données tout en substituant les chaînes spécifiques à la locale :
# i18n/data.py
def translate_projects(projects: list, locale: str) -> list:
"""Return projects with translated titles and descriptions."""
if locale == "en":
return projects
translated = []
for project in projects:
t = get_translation(f"project.{project['slug']}.title", locale)
d = get_translation(f"project.{project['slug']}.description", locale)
translated.append({
**project,
"title": t or project["title"],
"description": d or project["description"],
})
return translated
Cette approche maintient la couche de traduction séparée de la couche de données. Les routes transmettent la même liste projects quelle que soit la locale. Les fonctions de traduction enveloppent les données de manière transparente.
Sitemap avec alternates hreflang
Le sitemap dynamique inclut toutes les pages dans toutes les locales avec des références croisées :
@app.get("/sitemap.xml")
async def sitemap():
for page in static_pages:
for locale in SUPPORTED_LOCALES:
# Each URL entry includes alternates for all locales
locale_path = f"/{locale}{path}" if locale != "en" else path
xml_parts.append(f"<loc>{base_url}{locale_path}</loc>")
# Add xhtml:link alternates
for alt_locale in SUPPORTED_LOCALES:
alt_path = f"/{alt_locale}{path}" if alt_locale != "en" else path
hreflang = LOCALE_TO_HREFLANG[alt_locale]
xml_parts.append(
f'<xhtml:link rel="alternate" hreflang="{hreflang}" '
f'href="{base_url}{alt_path}"/>'
)
Cela produit 10 entrées d’URL par page (une par locale), chacune avec 11 liens alternatifs (10 locales + x-default). Pour un site de 50 pages, le sitemap contient 500 entrées d’URL avec 5 500 liens hreflang. Le sitemap est généré dynamiquement et mis en cache pendant une heure.
Patterns de base de données
Note : blakecrosley.com utilise Cloudflare D1 (SQLite serverless) via HTTP pour toutes les données persistantes, et non SQLAlchemy. Cette section couvre le pattern async SQLAlchemy standard pour les projets FastAPI qui nécessitent une base de données relationnelle — la configuration de production la plus courante pour cette stack.
SQLAlchemy 2.0 Async
Pour les applications qui nécessitent une base de données relationnelle, la prise en charge async de SQLAlchemy 2.0 s’intègre proprement avec FastAPI :
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
engine = create_async_engine("sqlite+aiosqlite:///./data.db")
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
Note d’installation (SQLAlchemy 2.0.50+) : depuis la version 2.0.50, la dépendance greenlet de la stack async ne s’installe plus par défaut. Utilisez l’extra asyncio pour qu’elle soit incluse, sans quoi le premier await exécuté sur l’engine échouera avec une erreur indiquant l’absence de greenlet :23
pip install "sqlalchemy[asyncio]" aiosqlite
SQLAlchemy 2.0.50 exige également Python 3.10+ (3.7–3.9 ne sont plus prises en charge) et ajoute des wheels free-threaded (3.13t).23
Injection de dépendances pour les sessions de base de données
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
@router.get("/users/{user_id}")
async def get_user(request: Request, user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "User not found")
return templates.TemplateResponse("pages/user.html", {
"request": request, "user": user
})
La dépendance get_db gère le cycle de vie de la session : elle ouvre une session, la fournit au gestionnaire de route, commit en cas de succès et effectue un rollback en cas d’exception. Chaque opération de base de données utilise des requêtes paramétrées — jamais d’interpolation de chaîne.
Intégration Pydantic
Les modèles Pydantic valident les entrées à la frontière API et sérialisent les sorties pour les templates :
from pydantic import BaseModel, EmailStr
class ContactForm(BaseModel):
name: str
email: EmailStr
message: str
@router.post("/contact")
async def submit_contact(request: Request, form: ContactForm):
# form.name, form.email, form.message are validated
await send_email(form)
return templates.TemplateResponse("components/_contact_success.html", {
"request": request
})
Pydantic valide les types, les formats (e-mail, URL) et les contraintes (longueur minimale/maximale) avant l’exécution du gestionnaire de route. Une entrée invalide renvoie automatiquement une réponse 422. Cela remplace les bibliothèques de validation de formulaire côté client : le serveur valide, puis HTMX insère soit le message de succès, soit le retour d’erreur.
Migrations avec Alembic
Alembic gère les changements de schéma de base de données :
# Generate a migration from model changes
alembic revision --autogenerate -m "add user preferences table"
# Apply migrations
alembic upgrade head
# Roll back one migration
alembic downgrade -1
La fonctionnalité autogenerate compare les modèles SQLAlchemy au schéma actuel de la base de données et génère des scripts de migration. Ces scripts sont des fichiers Python versionnés qui vivent dans le dépôt :
# alembic/versions/001_add_user_preferences.py
def upgrade():
op.create_table(
"user_preferences",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("locale", sa.String(10), default="en"),
sa.Column("theme", sa.String(20), default="dark"),
)
def downgrade():
op.drop_table("user_preferences")
Les migrations s’exécutent pendant le déploiement (avant le démarrage de l’application). Cela garantit que le schéma de base de données correspond au code de l’application. Pour blakecrosley.com, la plupart des données résident dans Cloudflare D1 (accessible via HTTP), donc les migrations Alembic s’appliquent à la base de données SQLite locale ou PostgreSQL utilisée pour les données de session et l’analytics.
Le pattern Cloudflare D1
blakecrosley.com utilise Cloudflare D1 comme base de données distante accessible via un proxy Cloudflare Worker :
class D1Client:
"""HTTP client for Cloudflare D1 via Worker proxy."""
def __init__(self, worker_url: str, auth_secret: str):
self.worker_url = worker_url
self.auth_secret = auth_secret
async def fetch_all(self, sql: str, params: list = None) -> list[dict]:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.worker_url}/query",
json={"sql": sql, "params": params or []},
headers={"Authorization": f"Bearer {self.auth_secret}"},
)
return response.json()["results"]
Ce pattern convient aux applications qui ont besoin d’une base de données sans vouloir gérer un serveur de base de données. D1 est SQLite à l’edge de Cloudflare, accessible via HTTP. Le proxy Worker gère l’authentification et la limitation de débit. Le compromis porte sur la latence : chaque requête est une requête HTTP (~50-100ms), contre une connexion à une base de données locale (~1-5ms). Le cache en mémoire au démarrage atténue cet effet pour les charges de travail à forte lecture, comme les traductions.
Sécurité
Limiter la taille du corps des requêtes
Starlette 1.6.0 a ajouté max_body_size, le contrôle qui manquait à cette stack : sans lui, un client peut envoyer un flux de données non borné à votre application et faire de la mémoire le point de défaillance. Définissez-le sur Starlette, Router, Mount ou une Route individuelle, ou encapsulez toute application ASGI dans RequestBodyLimitMiddleware. Les routes imbriquées peuvent augmenter ou réduire la limite à l’échelle de l’application, ce qui permet à un endpoint d’upload d’être permissif tandis que tout le reste reste strict.
app = FastAPI(lifespan=lifespan)
app.router.max_body_size = 2 * 1024 * 1024 # 2 MB default for the whole app
La limite compte les octets effectivement reçus du serveur ASGI, y compris les données de fichiers multipart, et n’utilise Content-Length que comme contrôle d’échec rapide — un en-tête absent ou sous-estimé ne peut pas la contourner. La valeur par défaut est None, ce qui signifie illimitée : cette protection est donc optionnelle.28
Middleware d’en-têtes de sécurité
blakecrosley.com applique des en-têtes de sécurité renforcés via un middleware personnalisé :
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
CSP_DIRECTIVES = {
"default-src": "'self'",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval'",
"style-src": "'self' 'unsafe-inline'",
"img-src": "'self' data: https:",
"connect-src": "'self'",
"frame-ancestors": "'self'",
"base-uri": "'self'",
"form-action": "'self'",
"upgrade-insecure-requests": "",
}
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
response.headers["Content-Security-Policy"] = self.csp
response.headers["Permissions-Policy"] = self.PERMISSIONS_POLICY
return response
La CSP inclut 'unsafe-inline' et 'unsafe-eval', car Alpine.js en a besoin pour évaluer les expressions. L’alternative est la build compatible CSP de Alpine.js, qui présente des limitations.14 Toutes les autres fonctionnalités sont verrouillées : frame-ancestors empêche le clickjacking, form-action limite les soumissions de formulaires à la même origine et upgrade-insecure-requests force HTTPS.
Sécurité du cache CDN avec HTMX
Le middleware d’en-têtes de sécurité ajoute Vary: HX-Request aux réponses HTMX :
if request.headers.get("HX-Request"):
existing_vary = response.headers.get("Vary", "")
if "HX-Request" not in existing_vary:
parts = [v.strip() for v in existing_vary.split(",") if v.strip()]
parts.append("HX-Request")
response.headers["Vary"] = ", ".join(parts)
Sans cet en-tête, un CDN pourrait mettre en cache une réponse de fragment HTMX et la servir comme page complète à une requête non-HTMX (ou inversement). L’en-tête Vary indique au CDN de stocker des entrées de cache distinctes selon la valeur de l’en-tête HX-Request.11
Protection CSRF
Les formulaires HTMX utilisent des tokens CSRF stateless signés avec HMAC :
# csrf.py
def generate_csrf_token() -> str:
"""Token format: timestamp:random:HMAC-SHA256-signature"""
timestamp = str(int(time.time()))
random_value = secrets.token_hex(16)
payload = f"{timestamp}:{random_value}"
signature = hmac.new(
CSRF_SECRET.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return f"{payload}:{signature}"
def validate_csrf_token(token: str) -> bool:
"""Verify signature and check expiration (1 hour)."""
timestamp, random_value, signature = token.split(":")
if int(time.time()) - int(timestamp) > 3600:
return False
expected = hmac.new(
CSRF_SECRET.encode(),
f"{timestamp}:{random_value}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Le token est généré dans le template via un global Jinja2 et inclus dans les requêtes de formulaire HTMX :
<form hx-post="/contact" hx-target="#form-result">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- form fields -->
</form>
Les tokens stateless éliminent le stockage de sessions côté serveur. La signature HMAC garantit que le token a été généré par le serveur. L’horodatage empêche les attaques par rejeu. hmac.compare_digest empêche les attaques temporelles.15
Assainissement de HTML
Le contenu généré par les utilisateurs passe par nh3 avant son rendu :
templates.env.filters["sanitize"] = sanitize_html
# In templates: {{ content | sanitize }}
La bibliothèque nh3 supprime les balises et attributs absents de la liste d’autorisation. Les liens reçoivent automatiquement rel="noopener noreferrer". Cette défense est indépendante de la CSP : elle empêche les XSS stockées au niveau de la couche de rendu, tandis que la CSP empêche les scripts injectés au niveau du navigateur. Défense en profondeur.
Validation des entrées
Les modèles Pydantic valident toutes les entrées à la frontière API :
from pydantic import BaseModel, Field, EmailStr
class ContactRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
message: str = Field(..., min_length=10, max_length=5000)
FastAPI renvoie automatiquement une erreur 422 Unprocessable Entity pour les entrées invalides. Combinée aux requêtes de base de données paramétrées (SQLAlchemy n’interpole jamais les chaînes), cette protection prévient les injections SQL et garantit la sûreté des types aux frontières.
Performances
Lighthouse 100/100/100/100
blakecrosley.com obtient un score de 100 dans les quatre catégories Lighthouse : Performance, Accessibilité, Bonnes pratiques et SEO. Vérifiez-le dans PageSpeed Insights.2
Les optimisations clés :
Stratégie de chargement de CSS
blakecrosley.com charge CSS avec une seule balise <link> et des URL hachées selon le contenu pour une mise en cache immuable :
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
Le helper asset() ajoute un hash de contenu (?v=a3b2c1d4) afin que le navigateur mette le fichier en cache indéfiniment, jusqu’à ce que son contenu change. Pas d’extraction critique de CSS, pas d’astuce avec les médias d’impression, pas de chargement basé sur JavaScript. Le fichier CSS pèse environ 8 KB compressé avec gzip — suffisamment peu pour que l’approche à requête unique obtienne 100 dans Lighthouse Performance sans acrobaties d’optimisation.
Compression GZip
app.add_middleware(GZipMiddleware, minimum_size=500)
Les réponses de plus de 500 octets sont compressées, à l’exception des exclusions par défaut de types de contenu introduites par Starlette 1.5.0 (archives, images, audio, vidéo, polices, SSE). HTML se compresse de 70 à 80 %, réduisant un document de 15 KB à 3-4 KB.28
Mise en cache immuable des assets statiques
# In security headers middleware
if request.url.path.startswith("/static/"):
if os.environ.get("RAILWAY_ENVIRONMENT"):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
Les assets statiques avec des URL hachées selon le contenu (?v=a3f8b2c1d0) sont mis en cache pendant un an avec immutable. Le hash change lorsque le fichier change, ce qui force les navigateurs et les CDN à récupérer la nouvelle version.
Chargement différé des scripts
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>
L’attribut defer télécharge les scripts en parallèle de l’analyse de HTML, mais les exécute après l’analyse du document. Cela évite de bloquer le rendu sans la complexité du chargement async et de la gestion de l’ordre d’exécution.
Optimisation des images
Les images utilisent WebP avec un srcset responsive et des dimensions explicites :
OPTIMIZED_IMAGES = {
"vision-sprint": {
"webp_srcset": (
"/static/images/optimized/vision-sprint-400w.webp 400w, "
"/static/images/optimized/vision-sprint-800w.webp 800w, "
"/static/images/optimized/vision-sprint-1200w.webp 1200w"
),
"fallback": "/static/images/optimized/vision-sprint-fallback.jpg",
"width": 1200,
"height": 1045,
},
}
<picture>
<source type="image/webp"
srcset="{{ image.webp_srcset }}"
sizes="(max-width: 768px) 100vw, 50vw">
<img src="{{ image.fallback }}"
width="{{ image.width }}"
height="{{ image.height }}"
alt="{{ image.alt }}"
loading="lazy">
</picture>
Les attributs explicites width et height empêchent le Cumulative Layout Shift (CLS). L’attribut loading="lazy" diffère le chargement des images hors écran. WebP offre des fichiers 25-35 % plus petits que JPEG à qualité équivalente.16
Early Hints
# In main.py
app.state.preload_links = [
f'<{make_asset_url(_asset_map, "css/styles.css")}>; rel=preload; as=style',
]
# In security headers middleware
if "text/html" in content_type:
preload_links = getattr(request.app.state, "preload_links", [])
if preload_links:
response.headers["Link"] = ", ".join(preload_links)
L’en-tête Link avec rel=preload indique à Cloudflare d’envoyer une réponse 103 Early Hints, ce qui permet au navigateur de commencer à récupérer CSS avant que le serveur ait fini de générer la réponse HTML.17
JavaScript minimal
L’empreinte totale de JavaScript :
| Bibliothèque | Taille (minifiée + compressée avec gzip) |
|---|---|
| HTMX | ~16 KB |
| Alpine.js | ~15 KB |
| JS spécifique à la page | 4-8 KB |
| Total | 35-39 KB |
Une application React typique envoie 100-300 KB de JavaScript de framework avant le code applicatif.18 L’approche sans build envoie moins de JavaScript, car il y a moins de JavaScript à envoyer.
Déploiement
Railway
blakecrosley.com est déployé sur Railway via git push :
# railway.toml
[build]
builder = "nixpacks"
[deploy]
startCommand = "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"
healthcheckPath = "/health"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
Le builder Nixpacks de Railway détecte le projet Python à partir de requirements.txt, installe les dépendances et exécute la commande de démarrage. Aucun fichier Docker n’est requis. Le point de terminaison de contrôle d’intégrité vérifie que l’application répond avant de recevoir du trafic :
@app.get("/health")
async def health():
return {"status": "healthy"}
Pipeline de déploiement
git push origin main
→ Railway detects push
→ Nixpacks installs Python + requirements.txt (cached)
→ uvicorn starts
→ Health check passes
→ Traffic routes to new deployment
→ ~40 seconds total
Pas de npm install. Pas de npm run build. Pas de compilation webpack. Pas de compilation TypeScript. La seule étape d’installation est pip install -r requirements.txt, qui est mise en cache entre les déploiements.
Procfile
web: uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
Le Procfile fournit une alternative compatible avec Heroku. Railway prend en charge railway.toml et Procfile. La syntaxe ${PORT:-8000} utilise le port fourni par la plateforme ou, par défaut, le port 8000 pour le développement local.
Configuration d’Uvicorn en production
Pour les déploiements à fort trafic, utilisez plusieurs workers :
uvicorn app.main:app \
--host 0.0.0.0 \
--port ${PORT:-8000} \
--workers 4 \
--loop uvloop \
--http httptools
--workers 4exécute quatre processus workers (règle générale : 2 * nombre de cœurs CPU + 1)--loop uvlooputilise la boucle d’événements uvloop, plus rapide (remplacement direct d’asyncio)--http httptoolsutilise le parseur HTTP httptools, plus rapide
Chaque worker est un processus distinct qui conserve sa propre copie de l’application ; la mémoire par processus est donc multipliée par le nombre de workers — c’est précisément là que le correctif du graphe de dépendances de FastAPI 0.140.0 porte ses fruits : sur une application comportant de nombreuses dépendances, quatre workers sous 0.139.2 subissent quatre fois l’ancien surcoût de Dependant.27
Pour le développement, --reload surveille les modifications apportées aux fichiers :
uvicorn app.main:app --reload --port 8000
Alternative avec Docker
Pour les plateformes qui nécessitent Docker :
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
L’image de base slim permet de conserver un conteneur léger. --no-cache-dir empêche pip de stocker les paquets téléchargés dans la couche de l’image.
CDN Cloudflare
blakecrosley.com utilise Cloudflare pour la mise en cache CDN, le DNS et les Workers :
# Cache headers for HTML pages (set in security middleware)
response.headers["Cache-Control"] = (
"public, max-age=300, s-maxage=3600, "
"stale-while-revalidate=86400"
)
max-age=300— mise en cache par le navigateur pendant 5 minutess-maxage=3600— mise en cache par le CDN pendant 1 heurestale-while-revalidate=86400— diffusion du contenu périmé pendant sa revalidation durant 24 heures
Les ressources statiques reçoivent max-age=31536000, immutable, car les URL contenant un hachage du contenu garantissent leur fraîcheur.
Cadre de décision
Avez-vous besoin d’outils de build ?
Répondez à quatre questions :
1. Plus de cinq développeurs partagent-ils des interfaces JavaScript ? Si oui, la vérification des types à la compilation de TypeScript évite des bugs d’intégration que les tests à l’exécution détectent trop tard. Ajoutez une étape de build.
2. Votre application gère-t-elle un état complexe côté client ? Si le glisser-déposer, la collaboration en temps réel ou les données disponibles hors ligne sont des fonctionnalités essentielles (et non de simples bonus), un framework comme React ou Svelte justifie sa complexité. Ajoutez une étape de build.
3. Plusieurs produits utilisent-ils une bibliothèque de composants commune ? Si oui, cette bibliothèque nécessite un paquet npm, un versionnage sémantique et du tree shaking. Ajoutez une étape de build.
4. Dépendez-vous de bibliothèques de l’écosystème npm qui supposent l’utilisation d’un bundler ? Si Radix, Framer Motion, TanStack Query ou des bibliothèques similaires sont au cœur du produit, un pipeline de build est indispensable.
Si les quatre réponses sont « non », l’approche sans build est viable. Si au moins une réponse est « oui », les outils de build résolvent un véritable problème. L’erreur consiste à ajouter des outils de build lorsque les quatre réponses sont « non » : vous résolvez alors des problèmes que vous n’avez pas, tout en créant une charge de gestion des dépendances bien réelle.1
Comparaison des stacks
| Catégorie | Sans build (ce guide) | React + outils de build |
|---|---|---|
| Idéal pour | Sites de contenu, portfolios, outils internes, applications CRUD | Produits SaaS, SPA complexes, applications utilisant un design system |
| Taille de l’équipe | 1 à 5 développeurs | 5 à 50 développeurs ou plus |
| Gestion de l’état | Serveur (HTMX) + client (Alpine.js) | Client (état React, Redux, Zustand) |
| Sûreté des types | À l’exécution (Pydantic côté serveur) | À la compilation (TypeScript) |
| Réutilisation des composants | Inclusions + macros Jinja2 | Paquets npm, bibliothèques partagées |
| SEO | Rendu côté serveur par défaut | Nécessite une configuration SSR/SSG |
| Niveau minimal de performance | Élevé (JS minimal, rendu côté serveur) | Variable (surcoût du framework) |
| Plafond de complexité | Plus bas (pas de mode hors ligne ni d’état client riche) | Plus élevé (toute interaction côté client est possible) |
| Dépendances | 17 paquets Python | Plus de 300 paquets npm |
| Temps de build | 0 seconde | 15 à 60 secondes |
Quand HTMX n’est pas adapté
HTMX remplace l’état client par des allers-retours avec le serveur. Cette approche fonctionne jusqu’à ce que la latence devienne déterminante :
- Interfaces par glisser-déposer — un aller-retour serveur de 200 ms pour chaque événement de déplacement est inacceptable
- Collaboration en temps réel — un état piloté par WebSocket nécessite une résolution des conflits côté client
- Applications conçues pour fonctionner hors ligne — sans serveur, pas de HTMX
- Animations complexes liées à l’état — Framer Motion et React Spring reposent sur le modèle de réconciliation de React
- Applications Canvas/WebGL — la boucle de rendu s’exécute par nature côté client
Pour ces cas d’usage, un framework côté client est l’outil approprié. L’approche sans build ne cherche pas à les remplacer.
Aide-mémoire
FastAPI
# Development
source venv/bin/activate
uvicorn app.main:app --reload --port 8000
# Production
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
# Testing
python -m pytest -v --cov=app
# Database migrations
alembic upgrade head
alembic revision --autogenerate -m "description"
Attributs HTMX
hx-get="/url" <!-- GET request -->
hx-post="/url" <!-- POST request -->
hx-target="#element" <!-- Where to put response -->
hx-swap="innerHTML" <!-- How to insert (innerHTML, outerHTML, beforeend) -->
hx-trigger="click" <!-- What triggers request -->
hx-trigger="keyup changed delay:300ms" <!-- Debounced input -->
hx-trigger="load" <!-- Fire on element load -->
hx-indicator="#spinner" <!-- Show during request -->
hx-push-url="true" <!-- Update browser URL -->
hx-replace-url="true" <!-- Replace URL (no history) -->
Attributs Alpine.js
x-data="{ open: false }" <!-- Component scope + state -->
x-show="open" <!-- Toggle visibility -->
x-cloak <!-- Hide until Alpine inits -->
@click="open = !open" <!-- Event handler -->
@click.away="open = false" <!-- Outside click -->
@keydown.escape="open = false" <!-- Keyboard event -->
:class="{ 'active': open }" <!-- Dynamic class -->
:aria-expanded="open" <!-- Dynamic attribute -->
x-text="count" <!-- Dynamic text content -->
x-init="fetchData()" <!-- Run on init -->
Propriétés personnalisées CSS
:root {
--color-bg: #000000;
--color-text: #ffffff;
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--font-size-lg: 1.25rem;
}
@media (max-width: 768px) {
:root { --gutter: 24px; }
}
En-têtes de sécurité
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Checklist de configuration du projet
[ ] FastAPI app with Jinja2Templates
[ ] Security headers middleware (CSP, HSTS, X-Frame-Options)
[ ] CSRF token generation and validation
[ ] GZip middleware (minimum_size=500)
[ ] Content-hash asset versioning (cache busting)
[ ] HTMX self-hosted in /static/js/vendor/
[ ] Alpine.js self-hosted in /static/js/vendor/
[ ] CSS custom properties for design tokens
[ ] Health check endpoint (/health)
[ ] Error handlers (404, 500)
[ ] robots.txt, sitemap.xml, llms.txt
[ ] JSON-LD structured data in base template
[ ] Hreflang tags for i18n (if multi-language)
[ ] HTML sanitization filter (nh3)
[ ] Rate limiting middleware
[ ] Deferred script loading
FAQ
HTMX est-il prêt pour de véritables applications web en production ?
Oui. HTMX est stable depuis 2020 et utilisé en production dans de nombreux secteurs. Carson Gross, son créateur, fait de la rétrocompatibilité un principe de conception fondamental — la documentation de HTMX indique que la bibliothèque ne rendra pas les applications existantes incompatibles au sein d’une même version majeure.19 La bibliothèque ne pèse qu’environ 16 Ko après minification et compression gzip, ne comporte aucune dépendance et respecte le versionnage sémantique. blakecrosley.com utilise HTMX en production depuis trois ans sans rencontrer le moindre bug lié à HTMX.20
Puis-je utiliser TypeScript sans étape de build ?
En partie. Le typage des fichiers TypeScript peut être vérifié avec tsc --noEmit sans générer de fichiers de sortie, ce qui permet d’effectuer une vérification à la compilation comme avec un linter. Toutefois, les navigateurs ne peuvent pas exécuter directement les fichiers .ts : une étape de build reste donc nécessaire pour distribuer du TypeScript. Une autre solution consiste à utiliser des annotations de type JSDoc dans de simples fichiers .js, que TypeScript peut vérifier sans compilation. Vous bénéficiez ainsi de la sûreté du typage pendant le développement tout en distribuant du JavaScript standard.
Comment cette approche se compare-t-elle à Astro ou 11ty ?
Astro et 11ty sont des générateurs de sites statiques qui produisent du HTML simple avec un minimum de JavaScript côté client, mais ils nécessitent une étape de build (Node.js, npm install et une commande de build). L’approche sans build supprime cette étape : le serveur génère le HTML à chaque requête. En contrepartie, Astro et 11ty produisent des pages statiques plus rapides, car aucun calcul serveur n’est nécessaire, tandis que FastAPI + HTMX prend nativement en charge le contenu dynamique — données propres à chaque utilisateur, envoi de formulaires et mises à jour en temps réel — sans couche API distincte.
Qu’en est-il du rendu côté serveur (SSR) avec React ?
Le SSR de Next.js et l’approche FastAPI + HTMX partagent le même objectif : envoyer au navigateur du HTML généré par le serveur. La différence réside dans ce qui se passe après le rendu initial. Next.js hydrate la page avec React, en envoyant au client le runtime du framework et le code des composants. FastAPI + HTMX n’effectue aucune hydratation : le HTML constitue le résultat final. HTMX gère les interactions suivantes en demandant de nouveaux fragments HTML au serveur. Résultat : FastAPI + HTMX envoie environ 35 à 40 Ko de JavaScript au total, contre 100 à 300 Ko pour une application Next.js.18
Comment gérer la validation des formulaires avec cette stack ?
Côté serveur. Pydantic valide les données saisies lors de l’envoi du formulaire. Si la validation échoue, le serveur renvoie le formulaire accompagné des messages d’erreur. HTMX remplace le contenu du DOM par la réponse :
<form hx-post="/contact" hx-target="#form-container" hx-swap="outerHTML">
<input type="email" name="email" required>
<button type="submit">Send</button>
</form>
@router.post("/contact")
async def contact(request: Request, email: str = Form(...)):
if not validate_email(email):
return templates.TemplateResponse("components/_contact_form.html", {
"request": request,
"error": "Please enter a valid email address",
"email": email, # Preserve input
})
await send_email(email)
return templates.TemplateResponse("components/_contact_success.html", {
"request": request
})
Le serveur effectue la validation, génère les états d’erreur et HTMX insère le résultat. Aucune bibliothèque de validation côté client n’est nécessaire. L’attribut HTML required fournit une validation élémentaire au niveau du navigateur comme première ligne de défense.
Puis-je ajouter des fonctionnalités en temps réel (WebSockets) ?
Oui. FastAPI prend nativement en charge les WebSocket :
from fastapi import WebSocket
@app.websocket("/ws/notifications")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await get_notification()
await websocket.send_text(render_notification_html(data))
HTMX propose une extension WebSocket (hx-ws) qui connecte des éléments aux endpoints WebSocket :
<!-- HTMX 2.x WebSocket extension syntax -->
<div hx-ext="ws" ws-connect="/ws/notifications">
<div id="notifications" ws-send></div>
</div>
Remarque : HTMX 1.x utilisait la syntaxe
hx-ws="connect:...". Dans HTMX 2.x, la prise en charge des WebSocket a été déplacée vers une extension distincte (htmx-ext-ws) qui utilise les attributsws-connectetws-sendprésentés ci-dessus. Si vous utilisez HTMX 1.x, l’ancienne syntaxehx-wsfonctionne toujours.Cycle bêta de HTMX 4.0 : htmx 4.0.0-beta6 est désormais disponible sous le tag npm
next, avec la documentation de la version 4.0 (bêta 6 publiée le 23 juillet 2026), tandis que le guide de démarrage rapide de htmx.org et le tag npmlatestrestent sur la version 2.0.10. Ce guide cible toujours HTMX 2.x, qui demeure la version recommandée pour la production jusqu’à la stabilisation de la version 4.0 ; la migration de 2.x vers 4.x représente un changement de génération, et non une simple mise à jour mineure de 2.x. Le modèle de versionnage de big-skies-software ignore les versions majeures impaires : 4.0 succède donc directement à 2.x.2122Points à suivre dans la documentation de la version 4.0. Deux nouveautés méritent une attention particulière lors des revues de sécurité et d’architecture avant la version GA de 4.0 : la nouvelle extension
hx-liveintroduit des expressions réactives au DOM, réévaluées lorsque l’état référencé change, tandis que la nouvelle extensionhx-nonceconditionne le traitement des attributs htmx à la présence de nonces CSP. Le guide de migration vers la version 4.0 déplace également plusieurs concepts de configuration, rétablit ou modifie certains comportements liés aux événements et à l’historique, et retire du cœur certains utilitaires JavaScript. Considérez la version 4.0 comme un véritable projet de migration, et non comme un correctif 2.x applicable tel quel.21
Les messages envoyés par le serveur sont insérés dans le DOM selon les mêmes mécanismes de ciblage et de remplacement que les réponses HTTP. Le serveur envoie des fragments HTML via la WebSocket, puis HTMX les insère.
Comment cette stack gère-t-elle le SEO ?
Le HTML généré côté serveur est naturellement adapté au SEO, car les robots d’indexation reçoivent l’intégralité du contenu de la page sans exécuter de JavaScript. blakecrosley.com ajoute plusieurs couches de SEO :
- Données structurées JSON-LD dans
<head>pour chaque page (schémas Person, Article, WebSite et FAQPage) - Sitemap dynamique avec des variantes hreflang pour les 10 locales
- Flux RSS à l’adresse
/blog/feed.xml llms.txtà la racine pour faciliter la découverte par les robots d’exploration d’IA- URL canoniques et balises Open Graph dans le template de base
- HTML sémantique :
<article>,<section>,<main>et hiérarchie correcte des titres
Aucune configuration SSR n’est nécessaire. Aucun getStaticProps. Aucun ISR. Le HTML est généré à chaque requête : il s’agit du comportement par défaut, pas d’une optimisation.
Quelle est la courbe d’apprentissage par rapport à React ?
Pour les développeurs Python, la courbe d’apprentissage est nettement plus accessible. Vous connaissez déjà le langage. Les gestionnaires de routes de FastAPI renvoient des réponses à partir de templates — le même modèle mental que les vues Flask ou Django. HTMX ajoute une poignée d’attributs HTML (hx-get, hx-target, hx-swap). Alpine.js en ajoute quelques autres (x-data, x-show, @click). Vous n’avez à apprendre ni JSX, ni DOM virtuel, ni système de hooks, ni bibliothèque de gestion d’état, ni configuration d’outil de build.
La documentation de HTMX tient sur une seule longue page. Celle de Alpine.js tient sur quelques pages. La documentation de React s’étend sur des centaines de pages consacrées aux hooks, au contexte, aux refs, aux effets, à suspense, aux composants serveur et au SSR en streaming.
Pour les développeurs JavaScript/React, le changement est davantage conceptuel que syntaxique. L’idée fondamentale est que le serveur détient l’état et génère le HTML. La gestion de l’état côté client devient alors une gestion des routes côté serveur. La récupération des données côté client est remplacée par des attributs HTMX sur les éléments HTML. La syntaxe est plus simple, mais ce modèle mental exige d’abandonner l’hypothèse propre aux SPA selon laquelle le client est responsable du rendu.
Journal des modifications
| Date | Modification | Source |
|---|---|---|
| 2026-08-16 | Starlette 1.3.1 → 1.6.0, et l’une de ses modifications altère silencieusement l’extrait GZip de ce guide. Changement de comportement : Starlette 1.5.0 (8 août) a considérablement étendu DEFAULT_EXCLUDED_CONTENT_TYPES au-delà de text/event-stream pour couvrir les archives gzip/zip, PNG, JPEG, WebP, GIF, AVIF, audio/*, video/* et les polices WOFF/WOFF2 ; ils ne sont donc plus compressés par défaut. image/* n’est délibérément pas exclu, ce qui laisse image/svg+xml compressible. Un nouveau mot-clé uniquement exclude_content_types remplace la liste, la correspondance est insensible à la casse et réaffecter la constante du module à l’exécution n’a plus d’effet. L’épinglage d’exécution de FastAPI est starlette>=0.46.0, sans limite supérieure ; une installation neuve récupère donc 1.6.0 et la modification s’applique à l’extrait de ce guide sans aucune action du lecteur — les deux passages GZip ont été corrigés. La version 1.5.0 ignore aussi les réponses partielles d’état 206 et vide le tampon pour chaque fragment diffusé ; la version 1.4.0 (5 août) a déplacé les fragments gzip d’au moins 128 KiB thread_minimum_size vers un thread de travail afin que les compressions importantes ne bloquent plus la boucle d’événements. Nouvelle capacité : la version 1.6.0 (8 août) a ajouté max_body_size sur Starlette/Router/Mount/Route, ainsi que RequestBodyLimitMiddleware — une nouvelle sous-section Sécurité la couvre, car la limitation du corps des requêtes était totalement absente de ce guide. Également signalé : encode/starlette redirige désormais vers Kludex/starlette, à l’image du déplacement d’Uvicorn. Uniquement dans le changelog : Uvicorn 0.52.0–0.52.3 (implémentation HTTP/1.1 expérimentale zttp basée sur Zig, que la publication elle-même déconseille de placer devant du trafic de production ; le conseil du guide d’utiliser --http httptools reste valable), Alpine.js 3.16.0/3.16.1, SQLAlchemy 2.0.52 (prise en charge de Python 3.15 ; correction d’un désalignement des colonnes de résultat ORM UPDATE synchronize_session="fetch"). Vérifiés comme inchangés : FastAPI 0.141.1, HTMX 2.0.10 latest / 4.0.0-beta6 next, Bootstrap 5.3.8, Jinja2 3.1.6, Pydantic 2.13.4 stable. Aucune nouvelle alerte de sécurité parmi les neuf dépendances pendant la période. |
28 |
| 2026-07-29 | FastAPI 0.141.0 + 0.141.1 (toutes deux le 29 juillet). La version 0.141.0 ajoute app.frontend(check_dir="auto"), afin que fastapi dev n’échoue plus lorsque le répertoire de build est absent — le cas habituel lorsque le serveur démarre avant l’exécution du build frontend. Quelques heures plus tard, 0.141.1 corrige les dépendances dans app.frontend() qui perdaient les tâches d’arrière-plan et les en-têtes de réponse ; une dépendance définissant un cookie ou planifiant une BackgroundTask voyait ce travail supprimé sur le montage frontend, alors qu’il fonctionnait correctement sur les routes API. Cela comble donc une véritable lacune dans la prise en charge des dépendances ajoutée dans 0.139.0. Les deux modifications s’intègrent dans le récit existant consacré à app.frontend() plutôt que dans une nouvelle section, puisque l’approche rendue côté serveur de ce guide ne monte pas de répertoire dist/. La version 0.141.1 documente également FASTAPI_ENV dans le guide FastAPI CLI (documentation uniquement, sans modification du corps). |
29 |
| 2026-07-27 | FastAPI a publié les versions 0.140.1 à 0.140.7 en cinq heures et demie le 27 juillet — sept publications, toutes consacrées à des refactorisations de la mécanique de dépendances entamées par la version 0.140.0. Deux axes : la copie aplatie du graphe de dépendances que FastAPI construisait et conservait a disparu (0.140.2), tout comme chaque site restant qui en reconstruisait une — génération OpenAPI (0.140.3, 0.140.7), champs de corps (0.140.5), paramètres de requête (0.140.6) — et 0.140.4 supprime une comptabilité de suivi des répétitions non lue. La seule modification assortie d’un seuil visible est 0.140.1 : le lru_cache des assistants de classification des appelables dans fastapi/dependencies/models.py passe de 1 024 à 4 096 entrées (nommé _CALLABLE_CLASSIFICATION_CACHE_SIZE), après des signalements d’applications dépassant 1 024 dépendances distinctes et le faisant saturer. Aucune modification de API ; la recommandation du paragraphe sur la mémoire des dépendances passe de 0.140.0 à 0.140.7 ou version ultérieure, avec une note précisant que cette branche évolue encore et que les benchmarks de dépendances OpenAPI (PR #16075) ne sont arrivés que dans la dernière publication de la série. |
30 |
| 2026-07-25 | FastAPI 0.140.0 (24 juillet, 21:16 UTC) corrige une régression de mémoire du système de dépendances présente depuis la version 0.121.0 (3 novembre 2025). La PR #16049 supprime dix attributs functools.cached_property de Dependant, les déplace vers des assistants au niveau du module et transforme la classe en @dataclass(slots=True) ; l’exécution CodSpeed officielle sur la PR fusionnée indique que le benchmark mémoire test_dependency_graph passe de 17,5 MB à 1,1 MB (×16), et le signalement initial décrivait un OOM en production sur 0.121.3, là où 0.120.4 restait sous environ 400 MB. Un passage consacré à 0.140.0 a été ajouté à Async Patterns, ainsi qu’une ligne sur la mémoire des workers à Uvicorn Production Configuration. Une erreur préexistante a également été corrigée : le guide affirmait que 0.137.0 « épingle Starlette dans la branche 1.x » — ce n’est pas le cas. L’exigence d’exécution de FastAPI est starlette>=0.46.0 (un plancher sans limite supérieure, toujours satisfait par Starlette 0.4x) uniformément dans les versions 0.136.3, 0.137.0, 0.138.0, 0.139.2 et 0.140.0 ; les numéros 1.x dans les notes de 0.137.0 correspondent à des mises à jour dependabot du fichier de verrouillage de test du dépôt (la PR #15722 ne touche que uv.lock). L’affirmation dans le corps et 24 ont toutes deux été corrigées. Deux non-modifications ont été signalées : les éléments internes de Dependant deviennent désormais incompatibles pour les outils (oauth_scopes, cache_key, _uses_scopes, _is_security_scheme ne sont plus des attributs, remplacés par les fonctions de module _get_oauth_scopes() / _get_cache_key() / _uses_scopes(), et slots=True empêche le monkey-patching des instances) — API interne non documenté auquel ce guide ne fait jamais référence, de la même catégorie que la modification de router.routes dans 0.137.0 ; et la documentation officielle de FastAPI privilégie désormais les projets uv plutôt que pip/venv dans 30 fichiers, dont le README, index.md, virtual-environments.md et les pages Docker/deployment (PR #16032, fusionnée le 21 juillet). Cette modification de documentation est cosmétique pour votre code, mais le guide enseigne partout pip install -r requirements.txt et diverge maintenant de la porte d’entrée amont — une décision éditoriale ultérieure, volontairement non prise dans cette passe. |
27 |
| 2026-07-24 | htmx 4.0.0-beta6 remplace beta5 comme balise npm next (publiée le 23 juillet 2026 ; publication GitHub le même jour). Éléments phares de la bêta : nouvelle extension hx-multipart (réponses multipart/mixed/multipart/parallel diffusées avec en-têtes d’action HX-* par partie), restauration du défilement de l’historique via la Navigation API avec solution de repli pour Firefox, renommage d’événement interne à la bêta htmx:swap:finally → htmx:finally:swap, les événements d’en-tête de réponse HX-Trigger se déclenchent désormais après le swap, méthodes de requête personnalisées et réécriture de hx-ws avec transmission de protocols. Recommandation inchangée — la production reste sur HTMX 2.x (latest = 2.0.10) jusqu’à la disponibilité générale de 4.0 ; le renommage n’est incompatible qu’au sein de la branche bêta 4.0. La note de suivi bêta et 21 ont été mises à jour. FastAPI 0.139.2, Uvicorn 0.51.0, Alpine.js 3.15.12, Starlette 1.3.1, Jinja2 3.1.6 ont tous été vérifiés comme inchangés ; aucune alerte de sécurité pendant la période. |
|
| 2026-07-17 | FastAPI 0.139.1 + 0.139.2 (16 juillet) : correction des chemins avec points pour les solutions de repli app.frontend() (/users/john.doe, PR #16011) et construction thread-safe des routes du routeur pour les tests en threads parallèles (PR #16013) — aucune modification de API visible pour l’application. Uvicorn 0.49.0 → 0.51.0 : l’implémentation historique websockets est déconseillée et auto utilise désormais websockets-sansio par défaut (0.50.0), l’implémentation par défaut exige websockets>=13.0 (0.50.2), et 0.51.0 (8 juillet) ajoute des redémarrages de workers SIGHUP avec chevauchement pour des rechargements presque sans interruption ; le dépôt se trouve désormais sur Kludex/uvicorn. HTMX (2.0.10 / 4.0.0-beta5 next), Alpine.js 3.15.12, Starlette 1.3.1, Pydantic 2.13.4, SQLAlchemy 2.0.51, Bootstrap 5.3.8 ont tous été vérifiés comme inchangés ; aucune alerte de sécurité pendant la période. |
|
| 2026-07-07 | htmx 4.0.0-beta5 est désormais la balise npm next (publiée le 26 juin 2026), remplaçant beta4 ; la note de suivi bêta HTMX 4.0 et [^22] sont mises à jour en conséquence. Recommandation inchangée — le travail de production reste sur HTMX 2.x (latest = 2.0.10) jusqu’à la disponibilité générale de 4.0. Vérifié auprès des balises de distribution npm de htmx.org. |
|
| 2026-07-02 | FastAPI 0.139.0 (1er juillet). app.frontend() prend désormais en charge les dependencies — par exemple, l’authentification automatique par cookie pour le frontend servi (PR #15908) — étendant le montage frontend statique de 0.138.0 avec la mécanique standard Depends() ; cela reste orthogonal à la thèse rendue côté serveur de ce guide et est signalé dans le même paragraphe de contraste. Aucun autre mouvement de la stack : HTMX 2.0.10, Alpine.js 3.15.12, Bootstrap 5.3.8, SQLAlchemy 2.0.51 inchangés. |
26 |
| 2026-06-22 | FastAPI 0.138.0 + 0.137.2. La version 0.138.0 (20 juin) ajoute app.frontend("/", directory="dist") / router.frontend(...) pour servir un frontend statique compilé (sortie SPA dist/) — orthogonal à la thèse de rendu côté serveur sans build de ce guide, signalé comme contraste dans la section Async Patterns. La version 0.137.2 (18 juin) ajoute iter_route_contexts() comme méthode prise en charge pour énumérer les routes, maintenant que router.routes est interne (depuis 0.137.0). Ce sont deux ajouts de fonctionnalités, sans changements incompatibles ; Starlette (1.3.1), Pydantic (2.13.4), HTMX (2.0.10), Alpine.js (3.15.12), Bootstrap (5.3.8), SQLAlchemy (2.0.51) restent tous inchangés. |
25 |
| 2026-06-16 | FastAPI 0.137.0/0.137.1 + Starlette 1.0→1.3.1. FastAPI 0.137.0 (14 juin) refactorise les éléments internes du routeur : router.routes est désormais un arbre interne, et non une liste plate de APIRoute (incompatible pour tout élément qui l’itère), tout en permettant les routes ajoutées après include_router() et de nouveaux hooks APIRouter.matches()/.handle() ; 0.137.1 (15 juin) corrige le typage de APIRoute et les routeurs sans préfixe dont le chemin est vide. Starlette a livré sa première version stable 1.0 (22 mars) et en est désormais à 1.3.1 (12 juin), supprimant les hooks déconseillés on_event/on_startup/on_shutdown ainsi que les décorateurs @app.route()/@app.websocket_route() — lifespan et Route/WebSocketRoute sont les seules voies. (Cette entrée affirmait initialement que FastAPI 0.137.0 épinglait Starlette 1.3.1 — corrigé le 2026-07-25 : ce n’est pas le cas ; l’exigence d’exécution est starlette>=0.46.0, sans limite supérieure.) Une note lifespan/routeur a été ajoutée à la section Async Patterns. SQLAlchemy 2.0.51 (15 juin) ne contient que des corrections de bugs. |
24 |
| 2026-06-08 | Modification de l’installation async de SQLAlchemy 2.0.50. À partir de SQLAlchemy 2.0.50, la dépendance greenlet de la stack async ne s’installe plus par défaut — installez l’extra sqlalchemy[asyncio] (sinon le premier await sur le moteur échoue avec une erreur greenlet manquant). La version 2.0.50 exige également Python 3.10+ (3.7–3.9 supprimés) et ajoute des wheels free-threaded 3.13t. Une note d’installation a été ajoutée à la section SQLAlchemy 2.0 Async. Aucune modification du corps pour le reste de la stack : la dernière version de FastAPI reste 0.136.3 (2026-05-23, aucune publication en juin), htmx stable reste 2.0.10 (4.0.0-beta4 « The Fetchening » est en bêta avec un objectif stable vers début 2027, pas encore une recommandation pour la production), Alpine.js 3.15.12, Bootstrap 5.3.x inchangé. Recommandation de production inchangée : HTMX 2.x jusqu’à la stabilité de 4.0.23 |
|
| 2026-05-24 | Vérification de maintenance : l’inventaire local du contenu indique toujours 210 articles de blog, 11 guides principaux, 48 études de design et 10 locales prises en charge, dont l’anglais. La dernière version de FastAPI est 0.136.3 (2026-05-23) ; le seul refactorisation visible pour l’application signalée dans les notes de publication concerne une gestion plus stricte des en-têtes avec traits de soulignement lorsque convert_underscores=True, et 0.136.2 valide les champs Server-Sent Event afin d’éviter des données d’événements défectueuses. htmx stable reste 2.0.10 tandis que npm next et la documentation 4.0 pointent désormais vers 4.0.0-beta4 ; la dernière version SQLAlchemy 2.0 est 2.0.50 ; la dernière version de Pydantic reste 2.13.4. La recommandation de production reste inchangée : utilisez HTMX 2.x jusqu’à ce que 4.0 devienne stable.122 |
|
| 2026-05-18 | Actualisation de l’inventaire du site : l’inventaire local du contenu indique désormais 210 articles de blog, 11 guides principaux, 48 études de design et 10 locales prises en charge, dont l’anglais. La dernière version de FastAPI reste 0.136.1 ; htmx stable reste 2.0.10 avec npm next sur 4.0.0-beta3 ; la dernière version npm de Alpine.js reste 3.15.12. La recommandation de production reste inchangée : utilisez HTMX 2.x jusqu’à ce que 4.0 devienne stable.12021 |
|
| 2026-05-15 | Vérification de maintenance : la dernière version de FastAPI reste 0.136.1 ; cet environnement local du site importe FastAPI 0.128.0 et Starlette 0.50.0 ; htmx stable reste 2.0.10 et npm next est désormais 4.0.0-beta3 ; la dernière version npm de Alpine.js est 3.15.12 ; la dernière version de Bootstrap est 5.3.8 ; la dernière version SQLAlchemy 2.0 est 2.0.49 ; la dernière version de Pydantic est 2.13.4. Recommandation de production inchangée : utilisez HTMX 2.x jusqu’à ce que 4.0 devienne stable.2021 |
|
| 2026-05-09 | Suivi de htmx 4.0.0-beta3 (8 mai 2026) : htmx 4.0.0-beta3 est disponible sur la balise npm next et dans la documentation 4.0, tandis que npm latest reste 2.0.10. Éléments à suivre avant la disponibilité générale : nouvelle extension hx-live (expressions réactives au DOM), nouvelle extension hx-nonce (protection nonce CSP pour les attributs htmx) et modifications du guide de migration concernant la configuration, l’historique, les événements et les assistants principaux JavaScript. Recommandation de production inchangée : htmx 2.x reste la dernière balise npm et la version recommandée jusqu’à la disponibilité générale de 4.0.21 |
|
| 2026-05-07 | Vérification de maintenance : la dernière version de FastAPI reste 0.136.1 ; htmx stable est 2.0.10 et v4 reste en bêta avec un objectif pour l’été 2026 ; la dernière version npm de Alpine.js est 3.15.12 ; la dernière version de Bootstrap est 5.3.8 ; la dernière version SQLAlchemy 2.0 est 2.0.49 ; la dernière version de Pydantic est 2.13.4. Les métriques locales du site ont été actualisées à 182 articles de blog, 11 guides, dix locales prises en charge et 17 exigences Python. Les conseils de migration restent inchangés : utilisez HTMX 2.x en production jusqu’à ce que 4.0 devienne stable.20 | |
| 2026-04-25 | FastAPI 0.136.1 (23 avril 2026) : nettoyage des dépréciations Pydantic v2 (aucun changement de comportement pour le code d’application). Calendrier HTMX 4.0 suivi : htmx 4.0.0-beta1 (6 avril) et htmx 4.0.0-beta2 (14 avril) ont été publiés. Conseils de migration inchangés — htmx 2.x reste sur la dernière balise npm jusqu’à ce que 4.0 soit stable ; les correctifs de sécurité continuent, sans pression de mise à niveau. Principales modifications de 4.0 à prendre en compte dès maintenant dans la conception : (1) fetch() remplace XMLHttpRequest comme infrastructure ajax principale, (2) l’héritage des attributs devient explicite par défaut, (3) la prise en charge de l’historique émet une requête réseau pour le contenu restauré (aucun instantané DOM local). FastAPI 0.135.4 (16 avril) a supprimé le décorateur @app.vibe() du poisson d’avril apparu dans 0.135.3. |
|
| 2026-04-16 | Ajout d’une prise en compte de HTMX 4.0-beta (référence anticipée). Signalement de la prise en charge par FastAPI 0.136.0 des builds free-threaded Python 3.14t. Fonctionnalités de Pydantic 2.13.x (fabriques par défaut d’attributs privés avec accès aux données de modèle validées, espace de noms pydantic.v1 vers 1.10.26 avec prise en charge de 3.14). Correctifs Alpine.js 3.15.11 : modificateur x-anchor.noflip, avertissement pour les éléments à racines multiples de x-for, correction d’une régression de morph de $refs. |
|
| 2026-03-24 | Publication initiale |
Références
Ce guide couvre l’ensemble du système utilisé pour construire blakecrosley.com. Le No-Build Manifesto présente l’argument philosophique. L’article Lighthouse Perfect Score documente le parcours d’optimisation des performances. L’article Vibe Coding vs. Engineering explore la place du développement assisté par IA dans ce workflow.
-
Métriques de production de blakecrosley.com au 18 mai 2026. Le site compte 210 articles de blog, des composants interactifs JavaScript, 11 guides principaux, 48 études de design, l’anglais ainsi que 9 langues traduites, des dépendances Python minimales et aucun outil de build. Vérifié à partir de l’inventaire local du contenu, de
app/i18n/config.pyet derequirements.txt. ↩↩↩↩↩ -
Google PageSpeed Insights (pagespeed.web.dev) exécute des audits Lighthouse sur toute URL publique. blakecrosley.com obtient 100/100/100/100 (Performance, Accessibilité, Bonnes pratiques, SEO) en mars 2026. Les résultats sont vérifiables publiquement. Consultez De 76 à 100 : obtenir un score Lighthouse parfait pour découvrir l’ensemble du parcours d’optimisation. ↩↩↩
-
Une installation fraîche de
npx create-next-app@latest(Next.js 15, testé en février 2026) installe 311 packages dansnode_modules/, pour un total de 187 Mo. Les projets de production avec des dépendances supplémentaires tendent à être plus lourds. Chaque projet varie. Source : tests de l’auteur, documentés dans The No-Build Manifesto. ↩ -
La documentation de performance Next.js de Vercel recommande des optimisations précises (optimisation des images, chargement des polices, code splitting) pour atteindre des scores supérieurs à 90. Consultez nextjs.org/docs/app/building-your-application/optimizing. La plage de 70 à 90 reflète les paramètres par défaut avant l’application de ces optimisations. ↩↩
-
Liste complète des dépendances vérifiée dans le
requirements.txtde blakecrosley.com en mai 2026. Le fichier comporte actuellement 17 entrées de dépendances Python et aucun outil de build, compilateur ou bundler. ↩ -
D’après l’expérience de l’auteur dans la maintenance de projets Next.js (2021-2024), l’écosystème JavaScript génère 15 à 25 PR Dependabot par mois pour les projets actifs, la plupart mettant à jour des dépendances transitives que le développeur n’a jamais importées directement. ↩
-
Tim Berners-Lee a formulé la rétrocompatibilité comme principe de conception du Web : « a browser should be backwards-compatible. » Une page de 1996 s’affiche dans Chrome 2026. Consultez w3.org/DesignIssues/Principles. ↩
-
OWASP recommande de désactiver les endpoints de documentation API en production afin de réduire la surface d’attaque. L’endpoint
/openapi.jsonexpose toutes les définitions de routes, les paramètres et les modèles de réponse. ↩ -
Documentation FastAPI sur les handlers async et sync : fastapi.tiangolo.com/async/. Mélanger
awaitet des appels bloquants dans des fonctionsasyncprive la boucle d’événements de ressources. ↩ -
nh3 est un sanitizer HTML basé sur Rust, successeur de la bibliothèque Bleach. Il est maintenu par le projet PyO3 et fournit une sanitisation HTML fondée sur une liste d’autorisations. Consultez github.com/messense/nh3. ↩
-
Le header
Varyest défini dans la section 12.5.5 de la RFC 9110. Il indique aux caches de stocker des réponses distinctes selon les valeurs des headers de requête spécifiés. SansVary: HX-Request, un CDN pourrait servir un fragment HTMX comme réponse de page complète. Consultez httpwg.org/specs/rfc9110.html#field.vary. ↩↩ -
Les Custom Properties CSS (Variables CSS) sont prises en charge par plus de 97 % des navigateurs mondiaux. Elles se propagent dans la cascade, sont héritées et réagissent aux media queries à l’exécution — des capacités absentes des variables de préprocesseur. Source : caniuse.com/css-variables. ↩
-
Documentation hreflang de Google : developers.google.com/search/docs/specialty/international/localized-versions. La valeur
x-defaultdésigne la page de repli pour les utilisateurs dont la langue ne figure pas dans la liste hreflang. ↩ -
Alpine.js requiert
'unsafe-eval'dans la Content Security Policy pour son moteur d’évaluation d’expressions. Le build compatible CSP (@alpinejs/csp) évite cette exigence, mais comporte des limitations. Consultez alpinejs.dev/advanced/csp. ↩ -
Les tokens CSRF fondés sur HMAC suivent le modèle « Signed Double-Submit Cookie » décrit dans l’OWASP CSRF Prevention Cheat Sheet.
hmac.compare_digestutilise une comparaison à temps constant pour empêcher les attaques par canal auxiliaire temporel. Consultez cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html. ↩ -
WebP produit des fichiers 25 à 35 % plus légers que JPEG à qualité visuelle équivalente. Étude WebP de Google : developers.google.com/speed/webp/docs/webp_study. ↩
-
103 Early Hints permet au serveur (ou au CDN) d’envoyer une réponse préliminaire avec des indications de preload avant que la réponse finale soit prête. Cloudflare prend en charge Early Hints pour les headers
Linkavecrel=preload. Consultez developer.chrome.com/blog/early-hints. ↩ -
React 18 + ReactDOM pèse environ 42 KB minifiés et gzippés. Avec un routeur, une bibliothèque de gestion d’état et le runtime d’un framework de build, les applications React typiques livrent 100 à 300 KB de JavaScript de framework. Source : bundlephobia.com/package/react-dom@18.2.0. ↩↩
-
La politique de versionnage et l’engagement de rétrocompatibilité de HTMX sont documentés sur htmx.org/migration-guide-htmx-1/. Carson Gross a énoncé le principe de rétrocompatibilité dans Hypermedia Systems (2023), de Gross, Stepinski et Cotter : hypermedia.systems. ↩
-
Vérification de maintenance du 15 mai 2026. Le PyPI et les notes de version de FastAPI indiquent 0.136.1 ; la vérification de l’import local a renvoyé FastAPI 0.128.0 et Starlette 0.50.0 pour l’environnement de ce site ; htmx.org indique 2.0.10 dans le guide de démarrage rapide ;
npm view htmx.org version dist-tagsa renvoyélatest=2.0.10etnext=4.0.0-beta3;npm view alpinejs versionetnpm view @alpinejs/csp versionont renvoyé3.15.12; le blog officiel de Bootstrap et les métadonnées du package npm indiquent 5.3.8 ; le PyPI et la documentation de SQLAlchemy indiquent 2.0.49 ; le PyPI de Pydantic indique 2.13.4. ↩↩↩↩ -
htmx 4.0.0-beta6 est l’actuel tag npm
next(publié le 23 juillet 2026 ; la branche bêta est passée de beta3 le 8 mai 2026 → beta4 → beta5 → beta6), tandis que npmlatestreste à 2.0.10. La documentation 4.0 sur four.htmx.org suit le buildnext, l’index des extensions 4.0 listehx-liveethx-nonce, et le guide de migration 4.0 documente les changements de migration à examiner avant de faire passer des applications de production de la version 2.x. Vérifié auprès des dist-tags npm dehtmx.orgle 24 juillet 2026. ↩↩↩↩↩↩ -
Vérification de maintenance du 24 mai 2026. Les commandes d’inventaire local ont renvoyé 210 articles de blog Markdown, 11 fichiers de guide de premier niveau et 48 fichiers d’études de design. Les notes de version de FastAPI indiquent 0.136.3 le 2026-05-23 avec une gestion plus stricte des headers contenant des underscores lorsque
convert_underscores=True; 0.136.2 valide les champs Server-Sent Event.python3 -m pip index versions fastapia renvoyé la dernière version0.136.3;python3 -m pip index versions sqlalchemya renvoyé la dernière version2.0.50;python3 -m pip index versions pydantica renvoyé la dernière version2.13.4.npm view htmx.org dist-tags version time.modified --jsona renvoyélatest=2.0.10,next=4.0.0-beta4ettime.modified=2026-05-22T15:56:21.948Z; la documentation d’installation de four.htmx.org affichehtmx.org@4.0.0-beta4. ↩↩ -
Changelog SQLAlchemy 2.0.50 et blog de publication, publiés le 2026-05-24. La dépendance asyncio
greenletne s’installe plus par défaut ; la cible d’installationsqlalchemy[asyncio]est désormais nécessaire pour l’inclure. La version 2.0.50 abandonne également Python 3.7/3.8/3.9 (désormais 3.10+), ajoute des wheels Python free-threaded et ajoute un paramètre de fenêtreover(..., exclude=...). Dernière version vérifiée sur PyPI au 2026-06-08. htmx 4.0.0-beta4 (« The Fetchening », 2026-05-22) reste en bêta, avec un objectif de version stable début 2027 ; FastAPI 0.136.3 (2026-05-23), Alpine.js 3.15.12 et Bootstrap 5.3.x sont inchangés durant cette période. ↩↩↩ -
Notes de version de FastAPI release notes : 0.137.0 (2026-06-14) refactorise les internes du routeur afin que
router.routesne soit plus une liste plate d’objetsAPIRoute, mais un arbre d’objets intermédiaires (à considérer comme interne) ; elle permet également d’ajouter des routes aprèsinclude_router(), y compris un sous-routeur avant la définition de ses routes, évite de copier les routes et ajouteAPIRouter.matches()/.handle(). Elle ne fixe pas Starlette à la version 1.x : l’exigence d’exécution de FastAPI eststarlette>=0.46.0— un plancher sans limite supérieure — de manière identique dans 0.136.3, 0.137.0, 0.138.0, 0.139.2 et 0.140.0, vérifié auprès des métadonnéesrequires_distdans le JSON API PyPI le 2026-07-25. La ligne « bump starlette from 1.1.0 to 1.2.1 » (PR #15722) dans les notes de la version 0.137.0 est une mise à jour dependabot dans la section Internal qui ne touche que le fichier lock de testuv.lockdu dépôt. (Une limite supérieure existait auparavant — 0.120.4 et 0.121.0 fournissaientstarlette<0.50.0,>=0.40.0— mais elle a été supprimée avec 0.136.3.) Correction appliquée le 2026-07-25 ; la formulation précédente de cette note de bas de page et l’affirmation dans le corps du guide étaient erronées. La version 0.137.1 (2026-06-15) corrige le typage deAPIRouteet un chemin vide dans un routeur sans préfixe. Notes de version de Starlette release notes : 1.0.0 (2026-03-22), sa première version stable depuis environ 8 ans, a suppriméon_startup/on_shutdown/on_event()ainsi que les décorateurs@app.route()/@app.websocket_route()(utilisezlifespanetRoute/WebSocketRoute) ; la dernière version est 1.3.1 (2026-06-12). SQLAlchemy 2.0.51 (changelog, 2026-06-15) ne contient que des corrections de bugs, sans incidence sur async ou l’installation. Vérifié via PyPI et les notes de version officielles le 2026-06-16. ↩↩↩ -
Notes de version de FastAPI release notes : 0.138.0 (2026-06-20) ajoute
app.frontend("/", directory="dist")etrouter.frontend("/", directory="dist")pour servir un frontend statique compilé (PR #15800 ; documentation Frontend) — une fonctionnalité statique de service de SPAdist/, et non un modèle de rendu côté serveur ; aucun changement cassant. La version 0.137.2 (2026-06-18) ajouteiter_route_contexts()pour les usages avancés qui parcouraient auparavantrouter.routes(interne depuis 0.137.0) ; aucun changement cassant. Aucune version plus récente que 0.138.0 au 2026-06-22. Starlette 1.3.1, Pydantic 2.13.4, Uvicorn 0.49.0, SQLAlchemy 2.0.51, HTMX 2.0.10, Alpine.js 3.15.12 et Bootstrap 5.3.8 sont tous inchangés. Vérifié via PyPI et les notes de version officielles le 2026-06-22. ↩↩ -
Notes de version de FastAPI 0.139.0, 1er juillet 2026 : « Support dependencies in
app.frontend(), e.g. for automatic cookie authentication for the frontend » (PR #15908). Le reste de la version comprend des traductions, de la documentation et des mises à jour de dépendances ; aucun changement cassant. Vérification de la session en cours le 2 juillet 2026 (PST) : 0.139.0 est la dernière version sur la page des versions GitHub. ↩↩ -
Notes de version de FastAPI 0.140.0, publiée le 24 juillet 2026 à 21:16 UTC (PyPI
upload_time_iso_86012026-07-24T21:16:42Z). L’unique entrée de refactorisation est « ⚡️ Reduce memory usage in dependencies. PR #16049 » (fusionnée le 2026-07-24T21:07:52Z). La régression a été introduite par la PR #14262 (fusionnée le 2025-11-03), publiée dans la version 0.121.0 le même jour, qui a ajoutéfunctools.cached_propertyàDependant.cache_key; dans la version 0.139.2, la classe contenait dix définitions@cached_property. Dans la version 0.140.0,fastapi/dependencies/models.pydéclare@dataclass(slots=True) class Dependant, avec la logique déplacée vers les fonctions de module_get_cache_key(),_get_oauth_scopes(),_uses_scopes()et_is_security_scheme()— source vérifiée au tag 0.140.0. Le bot CodSpeed sur la PR fusionnée indique que le benchmark de mémoiretest_dependency_graphpasse de 17.5 MB (base) à 1.1 MB (head), soit « improve performance by ×16 » ; 0.140.0 ajoute également un benchmark mémoire dans la CI (PR #16046) afin d’empêcher toute nouvelle régression. Le signalement d’origine est la discussion #14742, où la version 0.120.4 restait sous environ 400 MB tandis que la version 0.121.3 provoquait un OOM en production. Note à l’attention des auteurs d’outils :Dependant.oauth_scopes,.cache_key,._uses_scopeset._is_security_schemen’existent plus en tant qu’attributs, etslots=Trueempêche le monkey-patching des instances — un API interne non documenté que ce guide n’utilise pas, dans la même catégorie que la modification derouter.routesdans 0.137.0. Tous les faits ont été revérifiés auprès de PyPI, du GitHub API et de la source taguée le 2026-07-25. ↩↩↩ -
Versions Starlette 1.4.0 (2026-08-05), 1.5.0 (2026-08-08) et 1.6.0 (2026-08-08). La version 1.5.0 s’intitule « This release is all about giving
GZipMiddlewaresome love » et liste « Addexclude_content_typesparameter toGZipMiddleware», « Flush GZip output for each streamed chunk », « Skip compression of partial responses inGZipMiddleware» et « Expand default excluded content types inGZipMiddleware». Le tuple d’exclusion et la signature ont été lus directement dans starlette/middleware/gzip.py :DEFAULT_EXCLUDED_CONTENT_TYPES= application/gzip, application/x-gzip, application/zip, audio/, font/woff, font/woff2, image/avif, image/gif, image/jpeg, image/png, image/webp, text/event-stream, video/ — etdef __init__(self, app, minimum_size=500, compresslevel=9, thread_minimum_size=128*1024, *, exclude_content_types=DEFAULT_EXCLUDED_CONTENT_TYPES). Toutes récupérées et vérifiées le 2026-08-16. ↩↩↩↩ -
FastAPI 0.141.0 (2026-07-29, 14:47 UTC) a ajouté
app.frontend(check_dir="auto")pour le développement local avecfastapi dev(PR #16102). FastAPI 0.141.1 (2026-07-29, 17:17 UTC) a corrigé la prise en charge des tâches d’arrière-plan et des headers provenant des dépendances dansapp.frontend()(PR #16105) et a documentéFASTAPI_ENVdans le guide CLI de FastAPI (PR #16104). Toutes deux par @tiangolo. Dernière version PyPI confirmée : 0.141.1 le 2026-07-29. ↩↩ -
Les versions FastAPI 0.140.1 à 0.140.7, toutes publiées le 2026-07-27 entre 12:07 et 17:34 UTC (PyPI
upload_time_iso_8601: 0.140.112:07:51Z, 0.140.214:15:38Z, 0.140.315:30:52Z, 0.140.415:46:49Z, 0.140.516:02:53Z, 0.140.616:31:48Z, 0.140.717:34:47Z). Chaque description de version comporte une unique entrée Refactors : 0.140.1 « Update the lru_cache limit for dependencies to account for large apps » (PR #16062) ; 0.140.2 « Stop retaining flat dependency trees » (PR #16065) ; 0.140.3 « Avoid repeated dependency flattening in OpenAPI » (PR #16067) ; 0.140.4 « Skip unused dependency repeat bookkeeping » (PR #16069) ; 0.140.5 « Avoid flattening dependencies for body fields » (PR #16071) ; 0.140.6 « Avoid flattening dependencies for request parameters, mainly for OpenAPI » (PR #16073) ; 0.140.7 « Avoid flattening dependencies for OpenAPI » (PR #16076). Le chiffre du cache provient du diff #16062, qui remplace trois décorateurs@lru_cache(maxsize=1024)dansfastapi/dependencies/models.pypar@lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE)et met à jourtests/test_dependency_models.pyafin d’affirmer quecache_info.maxsize == 4096; le corps de la PR précise « Some users reported a number of dependencies larger than 1024, this should account for larger apps. » La version 0.140.2 ajoute également un benchmark de mémoire (PR #16064) et la version 0.140.7 ajoute des benchmarks de dépendances OpenAPI (PR #16075) ; la couverture de benchmark est donc postérieure à la majeure partie de la série. Vérifié auprès du GitHub des versions, des diffs des PR et de PyPI le 2026-07-27 ; 0.140.7 était la dernière version au moment de la rédaction. ↩↩