Tu veux un blog perso, sans y passer trois mois ni payer un CMS chaque mois.
Ce site que tu es en train de lire, c’est justement ça : un blog Astro.
Pas de base de données, pas d’admin à maintenir :
- Des fichiers Markdown
- Des Content Collections
- Un peu de Tailwind
- Et un déploiement statique sur Cloudflare Workers.
Voici comment le monter, avec les mêmes briques que celles utilisées ici.
Un blog perso, c’est un projet qu’on doit pouvoir maintenir seul, aussi longtemps qu’on le souhaite. Pas de dépendances externes lourdes, pas de CMS qui disparaît du jour au lendemain, pas de serveur à faire tourner. Juste un dépôt Git, des fichiers Markdown, et un hébergement statique.
Pourquoi Astro pour un blog
Astro détaille lui-même sa philosophie dans « Pourquoi Astro » : un framework pensé pour les sites axés contenu, orienté serveur par défaut. Ce qui, concrètement, donne :
- Simplicité : pas de state management, pas de framework JS à apprendre pour écrire un article. Des composants
.astro, du Markdown, c’est tout. - Rapidité : Astro rend du HTML statique par défaut, zéro JS envoyé au client sauf si tu le demandes explicitement (
client:load, etc.). Pour un blog, c’est exactement ce qu’il faut : du contenu qui se lit, pas une SPA. - Déploiement rapide : build statique, aucun serveur à faire tourner, ça se déploie sur n’importe quel CDN en quelques secondes.
- Contenu en Markdown : chaque article est un fichier
.md, versionné dans Git, éditable dans n’importe quel éditeur de texte. - Facile à maintenir : les content collections apportent un typage des données de front matter avec Zod, directement validé au build : une erreur de frontmatter casse le build plutôt que de planter silencieusement en prod.
Créer le projet
npm create astro@latest -- --template minimal
cd mon-blog
npm install
Une fois Tailwind et les content collections ajoutés (étapes suivantes), l’arborescence d’un blog Astro minimal ressemble à ça :
mon-blog/
├── astro.config.ts
├── package.json
├── tsconfig.json
└── src/
├── content.config.ts # schéma des articles (Zod)
├── styles/
│ └── global.css # import Tailwind
├── data/
│ └── blog/
│ ├── premier-article/
│ │ ├── index.md
│ │ └── banner.webp
│ └── deuxieme-article/
│ └── index.md
├── layouts/
│ ├── Layout.astro # <head>, meta, thème
│ └── PostDetails.astro # rendu d'un article
├── components/
│ └── Header.astro
└── pages/
├── index.astro
└── blog/
├── index.astro # liste des articles
└── [...slug]/
└── index.astro # page d'un article (route dynamique)
Structure volontairement simplifiée pour démarrer : dans un vrai projet, tu ajouteras vite une route de pagination ([...page].astro) et pas mal d’autres composants au fil de l’eau.
Ajoute Tailwind (v4, via le plugin Vite, pas de fichier tailwind.config.js à gérer) :
npm install tailwindcss @tailwindcss/vite
// astro.config.ts
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});
Et un point d’entrée CSS qui importe Tailwind :
/* src/styles/global.css */
@import "tailwindcss";
Les articles en content collections
Chaque article vit dans son propre dossier sous src/data/blog/<slug>/, avec un index.md et ses images à côté (banner.webp, screenshots, etc.).
Ça garde chaque article autonome : pas besoin de fouiller un dossier public/images partagé pour retrouver un asset.
Le schéma est déclaré une fois, dans src/content.config.ts :
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const blog = defineCollection({
loader: glob({ pattern: "**/[^_]*.md", base: "./src/data/blog" }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDatetime: z.date(),
modDatetime: z.date().optional(),
draft: z.boolean().default(true),
tags: z.array(z.string()).default(["notes"]),
ogImage: image().or(z.string()).optional(),
}),
});
export const collections = { blog };
Un article, c’est alors juste ça :
---
title: Mon premier article
description: Une courte accroche pour les listes et le SEO.
pubDatetime: 2026-08-10
draft: false
tags:
- Astro
---
Le contenu en Markdown ici.
Si draft est à true ou que pubDatetime est dans le futur, l’article ne doit pas apparaître dans les listings :
c’est un filtre à appliquer toi-même au moment de getCollection, Astro ne le fait pas pour toi :
import { getCollection } from "astro:content";
const now = Date.now();
const posts = await getCollection("blog", ({ data }) => {
const isPublished = now > data.pubDatetime.getTime();
return !data.draft && isPublished;
});
Layouts : un pour la liste, un pour l’article
Deux layouts suffisent pour la grande majorité des blogs perso :
- Un layout de base (
Layout.astro) qui pose le<head>, les meta OpenGraph, le thème clair/sombre. - Un
PostDetails.astroqui reçoit une entrée de collection, affiche le titre, les dates, et rend le Markdown viarender(post):
---
import { render } from "astro:content";
const { post } = Astro.props;
const { Content } = await render(post);
---
<article>
<Content />
</article>
Tout le style typographique (titres, citations, code) passe par une classe Tailwind type prose appliquée à cet <article>,
pas par du CSS écrit à la main par élément.
i18n léger : un dictionnaire, pas un routeur
Pas besoin du routing i18n natif d’Astro si le site n’a qu’une seule langue de contenu et juste des libellés d’interface à traduire. Un simple dictionnaire suffit :
// src/i18n/ui.ts
export const defaultLang = "fr";
export const ui = {
fr: {
"nav.home": "Accueil",
"footer.rights": "Tous droits réservés",
},
en: {
"nav.home": "Home",
"footer.rights": "All rights reserved",
},
} as const;
// src/i18n/utils.ts
import { ui, defaultLang } from "./ui";
export function useTranslations(lang: keyof typeof ui) {
return function t(key: keyof (typeof ui)[typeof defaultLang]) {
return ui[lang][key] ?? ui[defaultLang][key];
};
}
Dans un composant : const t = useTranslations(lang); t("nav.home").
Zéro dépendance externe, zéro fichier de traduction JSON à charger dynamiquement : tout est typé et vérifié au build par TypeScript.
Pour les articles eux-mêmes, plutôt qu’un vrai contenu dupliqué en deux langues, un champ englishUrl optionnel dans le front matter suffit à pointer vers une version anglaise publiée ailleurs (dev.to par exemple), sans avoir à maintenir une traduction complète synchronisée à chaque modification.
Déployer sur Cloudflare Workers
Astro en mode statique produit un dossier dist/ prêt à héberger.
Cloudflare Workers sait servir ces assets statiques directement (plus besoin de Pages) via Wrangler :
npm install -D wrangler
// wrangler.jsonc
{
"name": "mon-blog",
"compatibility_date": "2026-01-01",
"assets": {
"directory": "./dist"
}
}
npm run build
npx wrangler deploy
Chaque wrangler deploy pousse les assets sur le edge Cloudflare.
Pour le CI/CD automatique (push = déploiement), connecte le repo Git dans le dashboard Cloudflare Workers ou ajoute une étape wrangler deploy dans ta pipeline (GitHub Actions par exemple).
Pas de serveur à gérer, pas de cold start pour du contenu statique. Le CDN de Cloudflare sert les fichiers directement depuis le edge, et toi tu retournes écrire.
Pas envie de partir de zéro ?
Ce site est justement repris d’un thème existant : AstroPaper, un thème de blog minimaliste, responsive, accessible et pensé SEO, ensuite modifié à ma sauce (Tailwind v4, i18n léger, TOC, etc.).
Pas besoin de tout reconstruire depuis rien : la marketplace officielle de thèmes Astro référence des dizaines de templates prêts à l’emploi (blogs, portfolios, docs, e-commerce) à cloner puis personnaliser.
Conclusion
| Brique | Rôle |
|---|---|
| Astro | Rend du HTML statique, zéro JS par défaut |
| Content collections | Articles en Markdown, validés par un schéma Zod |
| Tailwind v4 | Style, sans fichier de config à maintenir |
| Dictionnaire i18n | Libellés d’interface traduits, sans routing lourd |
| Cloudflare Workers | Hébergement statique, déploiement en une commande |
Un blog perso avec Astro, c’est peu de pièces mobiles : des fichiers Markdown organisés en dossiers, un schéma Zod qui garde le front matter honnête, Tailwind pour le style, et un hébergeur statique qui pousse tout sur un CDN. Rien à opérer, rien à faire tourner en continu : juste écrire, commit, push.
