Publication de sites web Astro
Construisez des sites web statiques rapides et orientés contenu avec l'approche SSG sans runtime d'Astro, l'hydratation partielle et la prise en charge étendue de Markdown.
Sommaire
- Démarrage rapide
- Quand ne pas utiliser
- Structure du projet
- SSG vs SSR vs Hybride
- Collections de contenu — API Content Layer, chargeurs personnalisés
- Coloration syntaxique — Shiki, Transformers, Expressive Code
- Intégration de diagrammes — Mermaid, PlantUML, thème sombre
- Recherche côté client — Pagefind (contrôles, pondération), Fuse.js
- Documentation versionnée — Multi-version
- Internationalisation — Routage, replis
- Schémas courants — Pagination, tags, RSS, formulaires
- Bonnes pratiques de performances — Préchargement, CSS critique
- Déploiement — Netlify
- Liste de contrôle pré-déploiement
- Tests et qualité — Vitest, Playwright, vérification des liens
- Dépannage
Démarrage rapide
# Créer un nouveau projet (utiliser le modèle Blog pour les sites Markdown)
npm create astro@latest
# Développement
npm run dev # Serveur local sur http://localhost:4321
npm run build # Générer les fichiers statiques dans dist/
npm run preview # Prévisualiser la version de production
Quand ne pas utiliser
Ce skill se concentre sur la génération de sites statiques (SSG). Envisagez d'autres approches pour :
- Applications de données en temps réel — Utilisez le mode SSR avec des connexions à une base de données
- Flux d'authentification utilisateur — Nécessite la gestion des sessions côté serveur
- Commerce électronique avec inventaire dynamique — Utilisez le mode hybride ou SSR complet
- Applications monopages (SPA) — Envisagez directement les frameworks React/Vue
Pour les schémas hybrides SSG+SSR, consultez la documentation des adaptateurs Astro.
Structure du projet
src/
components/ # Composants Astro, React, Vue, Svelte
content/ # Collections de contenu (Markdown/MDX)
config.ts # Schémas de collections
docs/ # Exemple de collection
layouts/ # Wrappers de page avec slots
pages/ # Routage basé sur les fichiers
public/ # Actifs statiques (images, polices, favicons)
astro.config.mjs # Configuration du framework
SSG vs SSR vs Hybride
| Mode | Quand les pages sont rendues | Cas d'utilisation |
|---|---|---|
| SSG (par défaut) | Au moment de la construction | Blogs, documentation, sites marketing |
| SSR | À chaque requête | Données dynamiques, personnalisation |
| Hybride | Mélange des deux | Pages statiques + points de terminaison dynamiques |
Pour les sites purement statiques, utilisez output: 'static' par défaut — aucun adaptateur nécessaire.
Collections de contenu
Schéma hérité (Astro 4.x)
Définissez les schémas dans src/content/config.ts :
import { defineCollection, z } from "astro:content";
export const collections = {
docs: defineCollection({
schema: z.object({
title: z.string(),
description: z.string().optional(),
tags: z.array(z.string()).optional(),
order: z.number().optional(),
draft: z.boolean().default(false)
})
})
};
API Content Layer (Astro 5.0+)
Nouveau schéma avec le chargeur glob() — jusqu'à 75% de constructions plus rapides pour les grands sites :
// src/content.config.ts (note : nom de fichier différent)
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
schema: ({ image }) => z.object({
title: z.string(),
pubDate: z.coerce.date(),
draft: z.boolean().default(false),
cover: image(), // Valide que l'image existe
author: reference('authors'), // Référence entre collections
})
});
export const collections = { blog };
Schémas avancés
schema: ({ image }) => z.object({
cover: image(), // Valide l'image dans src/
category: z.enum(['tech', 'news']),
author: reference('authors'), // Référence entre collections
relatedPosts: z.array(reference('blog')).optional(),
})
Chargeurs personnalisés (contenu distant)
Récupérez du contenu depuis des API externes (versions GitHub, CMS, etc.) :
// src/loaders/github-releases.ts
import type { Loader } from 'astro/loaders';
export function githubReleasesLoader(repo: string): Loader {
return {
name: 'github-releases',
load: async ({ store, logger }) => {
logger.info(`Récupération des versions pour ${repo}`);
const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
const releases = await response.json();
for (const release of releases) {
store.set({
id: release.tag_name,
data: {
version: release.tag_name,
published_at: release.published_at,
body: release.body // Notes de version en Markdown
}
});
}
}
};
}
Enregistrez dans content.config.ts :
import { githubReleasesLoader } from './loaders/github-releases';
const releases = defineCollection({
loader: githubReleasesLoader('owner/repo'),
schema: z.object({
version: z.string(),
published_at: z.string(),
body: z.string(),
})
});
Interrogez et affichez les collections :
---
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const docs = await getCollection("docs");
return docs.map(doc => ({
params: { slug: doc.slug },
props: { doc }
}));
}
const { doc } = Astro.props;
const { Content } = await doc.render();
---
<article>
<h1>{doc.data.title}</h1>
<Content />
</article>
Coloration syntaxique
Configuration de base de Shiki
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
theme: "github-dark",
wrap: true
}
}
});
Thème double clair/sombre
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
}
Ajoutez du CSS pour basculer entre les thèmes :
@media (prefers-color-scheme: dark) {
.astro-code, .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
}
Surlignage de lignes et transformateurs
```typescript {2,4}
const a = 1;
const b = 2; // surligné
const c = 3;
console.log(a + b + c); // surligné
```
Transformateurs Shiki (Astro 4.14+) :
import { transformerNotationFocus, transformerNotationDiff } from '@shikijs/transformers';
shikiConfig: {
transformers: [transformerNotationFocus(), transformerNotationDiff()],
}
Utilisez des commentaires de notation dans le code :
// [!code focus]- Met cette ligne en focus// [!code ++]- Marque comme ajout (vert)// [!code --]- Marque comme suppression (rouge)
Expressive Code (Recommandé pour la documentation)
Blocs de code enrichis avec bouton de copie, noms de fichiers, surlignage des différences :
npm install astro-expressive-code
import expressiveCode from 'astro-expressive-code';
export default defineConfig({
integrations: [expressiveCode()],
});
Fonctionnalités : Bouton de copie, onglets de fichiers, marqueurs de ligne, cadres de terminal, marqueurs de texte.
Intégration de diagrammes
Mermaid (Recommandé)
Installez l'intégration Astro :
npm install astro-mermaid mermaid
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mermaid from 'astro-mermaid';
export default defineConfig({
integrations: [mermaid({ theme: 'default' })]
});
Utilisation en Markdown :
```mermaid
graph TD;
A-->B;
B-->C;
```
Fonctionnalités : Rendu côté client, basculement automatique du thème, fonctionne hors ligne, pas besoin de Playwright.
Alternative (SVG statique au moment de la construction) : Utilisez rehype-mermaid avec Playwright pour des diagrammes pré-rendus (npx playwright install --with-deps requis).
Stratégies de thème sombre :
- Variables CSS - Laissez le navigateur résoudre les couleurs au moment de l'exécution :
// configuration mermaid
mermaid.initialize({
theme: 'base',
themeVariables: {
primaryColor: 'var(--diagram-primary)',
lineColor: 'var(--diagram-line)'
}
});
- Élément Picture - Générez les deux thèmes, échangez avec la requête média :
<picture>
<source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
<img src="/diagrams/flow-light.svg" alt="Diagramme de flux">
</picture>
- SVG en ligne - Ciblez les classes SVG avec CSS (risque : collisions de styles) - ajoutez des critères d'accessibilité avec la possibilité d'avoir role='image' ou role="presentation" :
.dark .mermaid-svg .node rect {
fill: var(--bg-dark);
}
PlantUML
npx astro add plantuml
Utilisation en Markdown :
```plantuml
@startuml
Alice -> Bob: Bonjour
Bob --> Alice: Salut!
@enduml
```
Recherche côté client
Pagefind (Recommandé pour les grands sites)
Recherche statique sans configuration qui indexe au moment de la construction :
npm install pagefind
Ajoutez à package.json :
{
"scripts": {
"build": "astro build && npx pagefind --site dist",
"postbuild": "pagefind --site dist"
}
}
Utilisation dans les composants :
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: '#search', showSubResults: true });
});
</script>
Fonctionnalités : Aucun service externe, fonctionne hors ligne, indexation automatique, petit bundle (~10KB).
Contrôle d'indexation granulaire :
<!-- Indexer uniquement le contenu principal, pas les en-têtes/barres latérales -->
<main data-pagefind-body>
<h1 data-pagefind-meta="title">Titre de la page</h1>
<p data-pagefind-weight="10">Texte d'introduction important</p>
<!-- Exclure des extraits de recherche -->
<nav data-pagefind-ignore>
<a href="/related">Articles connexes</a>
</nav>
</main>
| Attribut | Objectif |
|---|---|
data-pagefind-body | Limiter l'indexation à cet élément uniquement |
data-pagefind-ignore | Exclure l'élément de l'index |
data-pagefind-meta="key" | Définir un champ de métadonnées |
data-pagefind-weight="10" | Augmenter la pertinence (par défaut : 1) |
Pagefind vs Fuse.js
| Fonctionnalité | Pagefind | Fuse.js |
|---|---|---|
| Architecture | Morceaux binaires pré-construits | En mémoire lors de l'exécution |
| Bande passante | Faible (charge uniquement les morceaux nécessaires) | Élevée (télécharge l'index complet) |
| Évolutivité | 10 000+ pages | < 500 pages |
| Multilingue | Stemming natif | Configuration manuelle |
| Cas d'utilisation | Recherche globale du site | Filtrage de petites listes |
Fuse.js (Alternative légère)
Pour les petits sites avec des besoins d'interface personnalisée :
---
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const searchIndex = JSON.stringify(posts.map(post => ({
title: post.data.title,
slug: post.slug,
body: post.body.slice(0, 500)
})));
---
<input type="search" id="search" placeholder="Recherche..." />
<ul id="results"></ul>
<script define:vars={{ searchIndex }}>
import Fuse from 'fuse.js';
const fuse = new Fuse(JSON.parse(searchIndex), {
keys: ['title', 'body'],
threshold: 0.3
});
document.getElementById('search').addEventListener('input', (e) => {
const results = fuse.search(e.target.value);
const resultsEl = document.getElementById('results');
// Effacer les résultats précédents en toute sécurité
resultsEl.replaceChildren();
// Construire les résultats avec des méthodes DOM sécurisées
results.forEach(r => {
const li = document.createElement('li');
const link = document.createElement('a');
link.href = `/blog/${r.item.slug}`;
link.textContent = r.item.title;
li.appendChild(link);
resultsEl.appendChild(li);
});
});
</script>
Pour les besoins d'entreprise, envisagez Algolia (API de recherche hébergée).
Documentation versionnée
Schéma de documentation multi-version
Utilisez une structure basée sur des dossiers :
src/content/docs/
v1/
getting-started.md
api-reference.md
v2/
getting-started.md
api-reference.md
new-feature.md
Internationalisation (i18n)
Configurez dans astro.config.mjs :
export default defineConfig({
i18n: {
defaultLocale: "fr",
locales: ["fr", "en", "es"],
routing: {
prefixDefaultLocale: false
}
}
});
Structurez le contenu par langue :
src/content/docs/
fr/
getting-started.md
en/
getting-started.md
Détectez la langue dans les composants :
---
const locale = Astro.currentLocale || 'fr';
---
Repli pour les traductions manquantes
Affichez le contenu en anglais avec une bannière lorsque les traductions n'existent pas :
---
// src/pages/[lang]/[...slug].astro
import { getCollection, getEntry } from "astro:content";
const languages = ['fr', 'en', 'es'];
const defaultLang = 'fr';
export async function getStaticPaths() {
const frenchDocs = await getCollection('docs', ({ id }) => id.startsWith('fr/'));
const paths = [];
for (const doc of frenchDocs) {
const slug = doc.id.replace(/^fr\//, '');
for (const lang of languages) {
const localizedId = `${lang}/${slug}`;
const localizedDoc = await getEntry('docs', localizedId);
paths.push({
params: { lang, slug },
props: {
entry: localizedDoc || doc, // Repli vers le français
isFallback: !localizedDoc
}
});
}
}
return paths;
}
const { entry, isFallback } = Astro.props;
const { Content } = await entry.render();
---
{isFallback && (
<div class="translation-notice">
Cette page n'est pas encore disponible dans votre langue.
</div>
)}
<Content />
Schémas courants
Listes paginées
Pour les sites avec plus de 50 articles, divisez en pages :
---
// src/pages/blog/page/[page].astro
import { getCollection } from "astro:content";
const POSTS_PER_PAGE = 10;
export async function getStaticPaths() {
const allPosts = await getCollection("blog");
const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
const paths = [];
for (let i = 1; i <= totalPages; i++) {
paths.push({
params: { page: String(i) },
props: { page: i, totalPages }
});
}
return paths;
}
const { page, totalPages } = Astro.props;
const start = (page - 1) * POSTS_PER_PAGE;
const end = start + POSTS_PER_PAGE;
const pagePosts = (await getCollection("blog")).slice(start, end);
---
<ul>
{pagePosts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<p>{post.data.description}</p>
</li>
))}
</ul>
<nav>
{page > 1 && <a href={`/blog/page/${page - 1}`}>Précédent</a>}
Page {page} sur {totalPages}
{page < totalPages && <a href={`/blog/page/${page + 1}`}>Suivant</a>}
</nav>
Pages de tags
Regroupez les articles par tags :
---
// src/pages/tags/[tag].astro
import { getCollection } from "astro:content";
const posts = await getCollection("blog");
const tag = Astro.params.tag;
const filteredPosts = posts.filter(post =>
post.data.tags?.includes(tag)
);
---
<h1>Articles tagués : {tag}</h1>
<ul>
{filteredPosts.map(post => (
<li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
))}
</ul>
Flux RSS
Utilisez le package @astrojs/rss :
npm install @astrojs/rss
---
// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET() {
const posts = await getCollection('blog');
return rss({
title: 'Mon Blog',
description: 'Articles récents',
site: 'https://monsite.com',
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/blog/${post.slug}/`,
})),
customData: `<language>fr-fr</language>`,
});
}
---
Formulaires
Pour les formulaires statiques, utilisez des services de backend comme Formspree ou Netlify Forms :
---
// src/pages/contact.astro
---
<form method="POST" action="https://formspree.io/f/xxxxxxxx">
<label for="name">Nom</label>
<input type="text" id="name" name="name" required />
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
<button type="submit">Envoyer</button>
</form>
Bonnes pratiques de performances
Préchargement
Astro gère le préchargement pour les liens visibles :
<!-- Activer manuellement si nécessaire -->
<a href="/blog" data-astro-prefetch>Blog</a>
<!-- Désactiver le préchargement pour un lien spécifique -->
<a href="/logout" data-astro-prefetch="false">Déconnexion</a>
CSS critique
Minimisez le CSS bloquant avec l'intégration critique :
npm install astro-critters
import critters from 'astro-critters';
export default defineConfig({
integrations: [critters()],
});
Images
Utilisez les composants Image d'Astro pour les images optimisées :
---
import { Image } from 'astro:assets';
import heroImage from '../images/hero.jpg';
---
<Image src={heroImage} alt="Image d'accueil" width={800} height={400} />
Déploiement
Netlify
npm install @astrojs/netlify
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'static',
adapter: netlify(),
});
Déployez via l'interface Netlify ou CLI :
npx netlify deploy --prod --dir=dist
Vercel
npm install @astrojs/vercel
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server', // Pour SSR ou hybride ; pour SSG, utilisez output: 'static'
adapter: vercel(),
});
Déployez via l'interface Vercel ou CLI :
npx vercel --prod
GitHub Pages
npm install @astrojs/github-pages
import githubPages from '@astrojs/github-pages';
export default defineConfig({
site: 'https://nomutilisateur.github.io',
base: '/nom-projet',
output: 'static',
adapter: githubPages(),
});
`
Déployez via un workflow GitHub Actions.
GCP/Firebase
npm install @astrojs/firebase
import firebase from '@astrojs/firebase';
export default defineConfig({
output: 'server', // Pour SSR ; pour SSG, utilisez output: 'static'
adapter: firebase(),
});
Déployez avec Firebase CLI :
firebase deploy --only hosting
Liste de contrôle pré-déploiement
- Tous les liens internes fonctionnent (utilisez
astro check) - Images optimisées et avec attributs
alt - Balises meta SEO (titre, description, Open Graph)
- Sitemap généré (intégration
@astrojs/sitemap) - Flux RSS (si applicable)
- Page 404 personnalisée
- Fichier
robots.txt - Performances vérifiées avec Lighthouse ou PageSpeed Insights
- Tests sur mobile et ordinateur
- Accessibilité de base (navigation au clavier, contrastes)
Tests et qualité
Tests unitaires avec Vitest
npm install vitest
// vite.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
Exemple de test :
import { describe, it, expect } from 'vitest';
function getPostUrl(slug: string): string {
return `/blog/${slug}`;
}
describe('Fonctions utilitaires', () => {
it('devrait générer l\'URL correcte pour un article', () => {
expect(getPostUrl('mon-article')).toBe('/blog/mon-article');
});
});
Tests de bout en bout avec Playwright
npm install playwright
npx playwright install
// e2e/home.test.ts
import { test, expect } from '@playwright/test';
test('la page d\'accueil devrait s\'afficher correctement', async ({ page }) => {
await page.goto('http://localhost:4321');
await expect(page).toHaveTitle(/Mon Site/);
const mainContent = page.locator('main');
await expect(mainContent).toBeVisible();
});
Vérification des liens
Utilisez astro check ou des outils comme broken-link-checker :
npm install broken-link-checker
npx broken-link-checker http://localhost:4321 --recursive
Dépannage
Problèmes courants
| Symptôme | Cause probable | Solution |
|---|---|---|
| Erreur "Cannot find module 'astro:content'" | Astro non installé ou version incorrecte | Exécutez npm install astro@latest |
| Les images ne s'affichent pas | Chemin incorrect ou image manquante | Vérifiez les chemins dans src/ et utilisez public/ pour les actifs statiques |
| Le déploiement échoue | Configuration de l'adaptateur manquante | Vérifiez output et adapter dans astro.config.mjs |
| Les pages 404 après déploiement | Base URL incorrecte | Configurez site et base correctement |
| Performances lentes en développement | Trop d'articles ou d'images lourdes | Utilisez Image d'Astro pour les images et limitez la taille des collections |