Astro 4 s'impose comme le framework idéal pour les sites de contenu en 2025 : blogs, documentations, landing pages, e-commerce. Avec son architecture islands, ses Content Collections typées, et son JavaScript zéro par défaut, Astro génère les sites les plus rapides du web.
Pourquoi Astro Domine les Sites de Contenu
Le Problème des SPA (React, Vue, etc.)
Site blog classique React :
Chargement page :
1. HTML (2kb)
2. React bundle (42kb gzipped)
3. App bundle (50kb+)
4. Hydratation (200-500ms)
5. Fetch contenu API
6. Render final
Total : ~500ms - 1s (mobile 3G)
Score Lighthouse : 60-80
Même site avec Astro :
Chargement page :
1. HTML complet (10kb) - Tout est dedans !
Total : ~100ms
Score Lighthouse : 100
Différence : Astro génère du HTML pur au build, pas de JavaScript à charger pour afficher le contenu.
Performances Comparées (Web.dev)
| Framework | First Contentful Paint | Time to Interactive | Lighthouse |
|---|---|---|---|
| Astro | 0.4s ⭐ | 0.4s ⭐ | 100 ⭐ |
| Next.js (SSG) | 0.8s | 1.2s | 95 |
| Gatsby | 1.1s | 1.8s | 92 |
| Nuxt (SSG) | 0.9s | 1.4s | 94 |
| SvelteKit | 0.6s | 0.7s | 98 |
Astro gagne sur sites statiques/contenu. Next.js/Nuxt meilleurs pour apps interactives complexes.
Content Collections : La Killer Feature
Avant : Gestion Manuelle de Contenu
Problème classique :
// pages/blog/[slug].astro (sans Content Collections)
const { slug } = Astro.params;
// ❌ Pas de typage
// ❌ Pas de validation
// ❌ Parsing manuel
const post = await import(`../../content/blog/${slug}.md`);
const { frontmatter, Content } = post;
// ❌ Erreur runtime si champ manquant
const title = frontmatter.title;
const date = new Date(frontmatter.date); // Peut crasher
Avec Content Collections : Type-Safe
- Définir le schéma ** :
// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blogCollection = defineCollection({
type: "content", // Markdown/MDX
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
author: z.string(),
tags: z.array(z.string()),
image: z.string().optional(),
draft: z.boolean().default(false),
}),
});
export const collections = {
blog: blogCollection,
};
- Structure fichiers ** :
src/
content/
blog/
post-1.md
post-2.mdx
2025/
january-update.md
- Utiliser dans pages ** :
---
// src/pages/blog/[slug].astro
import { getCollection, getEntry } from "astro:content";
import type { CollectionEntry } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("blog", ({ data }) => {
return data.draft !== true; // Exclure drafts en prod
});
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
interface Props {
post: CollectionEntry<"blog">;
}
const { post } = Astro.props;
const { Content } = await post.render();
// ✅ TypeScript connaît tous les champs !
const { title, description, date, author } = post.data;
---
<article>
<header>
<h1>{title}</h1>
<p>Par {author} • {date.toLocaleDateString("fr-FR")}</p>
<p>{description}</p>
</header>
<Content />
</article>
Avantages :
- ✅ Typage complet (autocomplete IDE)
- ✅ Validation au build (erreur si champ manquant)
- ✅ Parsing automatique (dates, URLs, etc.)
- ✅ Filtrage type-safe (exclure drafts)
Collections Avancées
Multiple collections :
// src/content/config.ts
import { defineCollection, reference, z } from "astro:content";
const authorsCollection = defineCollection({
type: "data", // JSON/YAML (pas de contenu Markdown)
schema: z.object({
name: z.string(),
bio: z.string(),
avatar: z.string(),
twitter: z.string().optional(),
}),
});
const blogCollection = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
author: reference("authors"), // ✅ Relation typée !
date: z.date(),
tags: z.array(z.string()),
}),
});
export const collections = {
blog: blogCollection,
authors: authorsCollection,
};
Utilisation relations :
---
// src/pages/blog/[slug].astro
import { getEntry } from "astro:content";
const { post } = Astro.props;
// ✅ Résolution automatique de la relation
const author = await getEntry(post.data.author);
---
<article>
<header>
<h1>{post.data.title}</h1>
<div class="author">
<img src={author.data.avatar} alt={author.data.name} />
<span>{author.data.name}</span>
</div>
</header>
<Content />
</article>
Génération Index/Listing
---
// src/pages/blog/index.astro
import { getCollection } from "astro:content";
// Récupérer tous les posts
const allPosts = await getCollection("blog");
// Trier par date (plus récent d'abord)
const posts = allPosts
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
// Grouper par année
const postsByYear = posts.reduce((acc, post) => {
const year = post.data.date.getFullYear();
if (!acc[year]) acc[year] = [];
acc[year].push(post);
return acc;
}, {} as Record<number, typeof posts>);
---
<div class="blog-list">
{Object.entries(postsByYear).map(([year, yearPosts]) => (
<section>
<h2>{year}</h2>
<ul>
{yearPosts.map((post) => (
<li>
<a href={`/blog/${post.slug}`}>
<h3>{post.data.title}</h3>
<time>{post.data.date.toLocaleDateString("fr-FR")}</time>
<p>{post.data.description}</p>
</a>
</li>
))}
</ul>
</section>
))}
</div>
Recherche Full-Text
---
// src/pages/api/search.json.ts
import { getCollection } from "astro:content";
import type { APIRoute } from "astro";
export const GET: APIRoute = async ({ url }) => {
const query = url.searchParams.get("q")?.toLowerCase() || "";
if (!query) {
return new Response(JSON.stringify([]), {
headers: { "Content-Type": "application/json" },
});
}
const posts = await getCollection("blog");
const results = posts
.filter((post) => {
const searchContent = [
post.data.title,
post.data.description,
post.data.tags.join(" "),
]
.join(" ")
.toLowerCase();
return searchContent.includes(query);
})
.map((post) => ({
title: post.data.title,
slug: post.slug,
description: post.data.description,
}));
return new Response(JSON.stringify(results), {
headers: { "Content-Type": "application/json" },
});
};
---
<!-- Composant recherche -->
<script>
const searchInput = document.querySelector("#search");
const resultsDiv = document.querySelector("#results");
searchInput.addEventListener("input", async (e) => {
const query = e.target.value;
if (query.length moins de 3) {
resultsDiv.innerHTML = "";
return;
}
const res = await fetch(`/api/search.json?q=${encodeURIComponent(query)}`);
const results = await res.json();
resultsDiv.innerHTML = results
.map(
(r) => `
<a href="/blog/${r.slug}">
<h4>${r.title}</h4>
<p>${r.description}</p>
</a>
`
)
.join("");
});
</script>
Islands Architecture : Le Meilleur des Deux Mondes
Concept
Problème SPA : Tout est JavaScript, même contenu statique.
Solution Astro :
- Génère HTML pur par défaut
- Ajoute JavaScript seulement pour composants interactifs
- Composants "Islands" isolés
---
// src/pages/product.astro
import Header from "../components/Header.astro"; // Statique
import ProductGallery from "../components/ProductGallery.tsx"; // React interactif
import AddToCartButton from "../components/AddToCart.svelte"; // Svelte interactif
import Footer from "../components/Footer.astro"; // Statique
---
<Header />
<!-- Island 1 : Gallery interactive -->
<ProductGallery client:load images={product.images} />
<!-- Island 2 : Bouton avec état -->
<AddToCartButton client:visible productId={product.id} />
<Footer />
Résultat :
HeaderetFooter: HTML pur (0 JS)ProductGallery: Hydraté au chargement (React)AddToCartButton: Hydraté quand visible (Svelte)
Gain : Bundle JS réduit de 80-90% vs SPA classique.
Client Directives
<!-- 1. client:load - Hydrate immédiatement -->
<InteractiveChart client:load data={chartData} />
<!-- 2. client:idle - Hydrate quand navigateur idle (recommandé) -->
<Chatbot client:idle />
<!-- 3. client:visible - Hydrate quand entre viewport (lazy) -->
<Comments client:visible postId={post.id} />
<!-- 4. client:media - Hydrate selon media query -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- 5. client:only - Jamais de SSR (uniquement client) -->
<BrowserOnlyComponent client:only="react" />
Stratégie recommandée :
client:load: Composants above-fold critiquesclient:idle: Composants interactifs non-critiquesclient:visible: Composants below-fold (commentaires, widgets)
Multi-Framework Support
React + Vue + Svelte dans le même projet :
# Installer intégrations
npm install @astrojs/react @astrojs/vue @astrojs/svelte
# Config
# astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import vue from "@astrojs/vue";
import svelte from "@astrojs/svelte";
export default defineConfig({
integrations: [react(), vue(), svelte()],
});
---
// Utiliser dans même page !
import ReactCounter from "./ReactCounter.tsx";
import VueChart from "./VueChart.vue";
import SvelteForm from "./SvelteForm.svelte";
---
<ReactCounter client:load />
<VueChart client:visible chartData={data} />
<SvelteForm client:idle />
Cas d'usage :
- Migrer progressivement depuis framework existant
- Utiliser librairie spécifique (ex: React pour Recharts, Vue pour Element UI)
- Équipe multi-compétences
MDX : Markdown Superpuissant
Installation
npm install @astrojs/mdx
// astro.config.mjs
import mdx from "@astrojs/mdx";
export default defineConfig({
integrations: [mdx()],
});
Composants dans Markdown
---
# src/content/blog/interactive-post.mdx
title: "Article Interactif"
date: 2025-01-13
---
import { YouTube } from "@astro-community/astro-embed-youtube";
import Counter from "../../components/Counter.tsx";
import Chart from "../../components/Chart.svelte";
# Mon Article Interactif
Voici un compteur React embedded :
<Counter client:load />
Et une vidéo YouTube :
<YouTube id="dQw4w9WgXcQ" />
Avec un graphique Svelte :
<Chart data={[1, 2, 3, 4, 5]} client:visible />
**Tout fonctionne ensemble !**
Custom Components (Remplacer éléments HTML)
// src/content/config.ts
import { defineCollection, z } from "astro:content";
const blogCollection = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
}),
});
export const collections = { blog: blogCollection };
---
// src/components/MDXComponents.astro
const components = {
h2: (props) => <h2 class="custom-heading" {...props} />,
a: (props) => <a class="custom-link" target="_blank" {...props} />,
pre: (props) => <CodeBlock {...props} />,
};
---
---
// src/pages/blog/[slug].astro
import { getEntry } from "astro:content";
import MDXComponents from "../../components/MDXComponents.astro";
const { slug } = Astro.params;
const post = await getEntry("blog", slug);
const { Content } = await post.render();
---
<article>
<Content components={MDXComponents} />
</article>
Intégrations Essentielles
1. Tailwind CSS
npx astro add tailwind
---
// src/pages/index.astro
---
<div class="container mx-auto px-4">
<h1 class="text-4xl font-bold text-gray-900">
Hello Astro + Tailwind
</h1>
</div>
2. Sitemap + RSS
npx astro add sitemap
// astro.config.mjs
import sitemap from "@astrojs/sitemap";
export default defineConfig({
site: "https://example.com",
integrations: [sitemap()],
});
RSS Feed :
// src/pages/rss.xml.ts
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
export async function GET(context) {
const posts = await getCollection("blog");
return rss({
title: "Mon Blog",
description: "Articles sur le dev web",
site: context.site,
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.date,
description: post.data.description,
link: `/blog/${post.slug}/`,
})),
});
}
3. Syntax Highlighting (Shiki)
---
// Intégré par défaut, pas d'install nécessaire
---
```typescript
// Code TypeScript avec coloration
const greeting: string = "Hello Astro!";
console.log(greeting);
\```
Thèmes custom :
// astro.config.mjs
export default defineConfig({
markdown: {
shikiConfig: {
theme: "dracula", // ou 'github-dark', 'nord', etc.
wrap: true,
},
},
});
4. Image Optimization
---
import { Image } from "astro:assets";
import heroImage from "../assets/hero.jpg";
---
<!-- Optimisation automatique (WebP, lazy, responsive) -->
<Image
src={heroImage}
alt="Hero"
width={800}
height={600}
format="webp"
quality={80}
/>
<!-- Images distantes -->
<Image
src="https://example.com/image.jpg"
alt="Remote"
width={400}
height={300}
inferSize
/>
Résultat :
- Génère WebP + fallback JPEG
- Lazy loading automatique
- Responsive srcset
- Tailles optimisées
Server-Side Rendering (SSR)
Activer SSR
// astro.config.mjs
import node from "@astrojs/node";
export default defineConfig({
output: "server", // ou 'hybrid'
adapter: node({ mode: "standalone" }),
});
Modes :
static(défaut) : SSG purserver: SSR pour toutes les pageshybrid: SSG par défaut, SSR opt-in
Page SSR
---
// src/pages/user/[id].astro
export const prerender = false; // Forcer SSR (mode hybrid)
const { id } = Astro.params;
// Fetch à chaque requête
const res = await fetch(`https://api.example.com/users/${id}`);
const user = await res.json();
---
<h1>{user.name}</h1>
<p>Chargé à la demande (SSR)</p>
API Routes
// src/pages/api/contact.ts
import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request }) => {
const data = await request.formData();
const name = data.get("name");
const email = data.get("email");
// Send email, save to DB, etc.
await sendEmail({ name, email });
return new Response(
JSON.stringify({ success: true }),
{ headers: { "Content-Type": "application/json" } }
);
};
Utiliser depuis client :
<form action="/api/contact" method="POST">
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">Envoyer</button>
</form>
<script>
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
const res = await fetch("/api/contact", {
method: "POST",
body: formData,
});
const result = await res.json();
alert(result.success ? "Envoyé !" : "Erreur");
});
</script>
View Transitions (Navigation SPA)
---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from "astro:transitions";
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
Résultat : Navigation entre pages sans reload, comme une SPA !
Animations custom :
<style>
@view-transition {
navigation: auto;
}
::view-transition-old(root) {
animation: fade-out 0.3s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-in;
}
</style>
Déploiement
Vercel / Netlify (Zero Config)
# Vercel
npm install --save-dev @astrojs/vercel
// astro.config.mjs
import vercel from "@astrojs/vercel/serverless";
export default defineConfig({
output: "server",
adapter: vercel(),
});
vercel deploy
Cloudflare Pages
npm install --save-dev @astrojs/cloudflare
import cloudflare from "@astrojs/cloudflare";
export default defineConfig({
output: "server",
adapter: cloudflare(),
});
Docker
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package.json .
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]
Cas d'Usage Parfaits pour Astro
✅ Idéal pour :
- Blogs (comme ce site !)
- Documentation (Starlight theme)
- Landing pages (marketing sites)
- E-commerce (pages produits)
- Portfolios
- Corporate websites
❌ Pas idéal pour :
- Dashboards temps réel (préférer Next.js/Nuxt)
- Applications complexes (préférer SPA)
- Apps mobiles (pas de React Native)
Articles connexes
Pour approfondir le sujet, consultez également ces articles :
- React 19 stable : Server Components et Actions révolutionnent le développement web
- Angular v19 : Signals GA et performances +60% en octobre 2025
- Mojo : Le langage qui combine syntaxe Python et performance C, 68 000x plus rapide
Conclusion : Astro en 2025
Astro 4 s'impose comme le meilleur choix pour sites de contenu :
- Performance inégalée (Lighthouse 100)
- Content Collections type-safe
- Islands Architecture (0 JS inutile)
- Multi-framework (React, Vue, Svelte)
- DX excellente (TypeScript, HMR)
Démarrer :
npm create astro@latest my-blog
# Template recommandé : Blog
# TypeScript : Strict
# Installer dépendances : Oui
cd my-blog
npm run dev
Astro rend le web plus rapide, un site à la fois !


