Un site Next.js exporté en statique, c’est simplement du HTML, du CSS, du JavaScript et des ressources dans un dossier. Vous pouvez déposer ce dossier sur n’importe quel hébergeur statique — Cloudflare Pages, GitHub Pages, S3, Netlify, un serveur Nginx — sans serveur Node.js à faire tourner, à mettre à jour ou à payer. Ce site est construit exactement ainsi.
Ce guide explique comment fonctionne l’export statique dans l’App Router, ce à quoi vous renoncez, et comment contourner chaque limite.
L’activer
Depuis Next.js 14, next export a disparu ; l’export statique est une option de configuration :
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
// Optional: /about → /about/index.html instead of /about.html
trailingSlash: true,
images: { unoptimized: true },
}
export default nextConfignext build écrit désormais le site dans out/. Prévisualisez-le avec n’importe quel serveur statique :
npx serve outCe qui fonctionne
Plus qu’on ne le croit :
- Les Server Components. Ils s’exécutent une seule fois, au build. Récupérer des données, lire des fichiers, appeler des API : le résultat est figé dans le HTML.
- Les Client Components, les hooks, le context et toutes les bibliothèques côté client.
- Les routes dynamiques, à condition de lister chaque chemin avec
generateStaticParams. - Les Route Handlers en
GET, qui deviennent des fichiers statiques (pratique pourfeed.json,sitemap.xml,robots.txt). - L’API Metadata : titres, descriptions, Open Graph, URL canoniques.
- La navigation côté client avec
<Link>et le préchargement.
Ce qui ne fonctionne pas, et quoi faire à la place
| Fonctionnalité | Pourquoi ça échoue | Solution |
|---|---|---|
| Optimisation d’images par défaut | Nécessite un serveur | images.unoptimized: true, images pré-dimensionnées ou loader personnalisé (Cloudinary, imgix, Cloudflare Images) |
Routes dynamiques sans generateStaticParams |
Les pages doivent être connues au build | Lister tous les paramètres ; définir dynamicParams = false |
cookies(), headers(), données de requête |
Aucune requête à l’exécution | Les lire côté client, ou déplacer la logique vers une API |
| Server Actions | Nécessitent un serveur | Envoyer un POST à une fonction serverless (Workers, Lambda) |
| Middleware | Nécessite un serveur | Règles au niveau de l’hébergeur : _redirects, _headers, fonctions edge |
redirects, rewrites, headers dans la config |
Appliqués par le serveur Next | Idem : les configurer chez l’hébergeur |
ISR / revalidate |
Pas de serveur pour régénérer | Reconstruire à chaque changement de contenu (push Git ou webhook) |
| Routage i18n intégré | Fonctionnalité du Pages Router | Segments de langue : app/[lang]/... avec generateStaticParams |
Des routes dynamiques bien faites
Chaque segment dynamique a besoin de la liste de ses valeurs au moment du build :
import { getAllPosts, getPost } from '@/lib/blog'
import { notFound } from 'next/navigation'
export const dynamicParams = false // unknown slugs are a 404, not a runtime render
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
if (!post) notFound()
return <article dangerouslySetInnerHTML={{ __html: post.html }} />
}Notez que dans Next.js 15, params est une Promise : pensez à l’await.
Un piège : si generateStaticParams renvoie un tableau vide, le build échoue, car la route ne produirait aucune page. Si une collection peut être vide (un nouveau blog, par exemple), renvoyez un paramètre factice et laissez-le afficher une 404.
Les images
Vous avez trois options raisonnables :
unoptimized: true, en exportant vous-même les images à la bonne taille (WebP ou AVIF, 1 à 2 fois la largeur affichée). Simple et rapide si vous êtes rigoureux.- Un loader personnalisé pointant vers un CDN d’images, qui renvoie des variantes redimensionnées à la demande :
export default function loader({ src, width, quality }) {
return `https://images.example.com/${src}?w=${width}&q=${quality || 75}`
}const nextConfig = {
output: 'export',
images: { loader: 'custom', loaderFile: './image-loader.js' },
}- Un simple
<img>avecsrcset,loading="lazy"et deswidth/heightexplicites pour éviter les décalages de mise en page.
Formulaires et tout ce qui est côté serveur
Un site statique peut tout à fait avoir des formulaires, une recherche et des connexions : la partie serveur vit simplement ailleurs. Le schéma habituel, c’est une petite fonction serverless que la page appelle avec fetch. J’ai détaillé celle de ce site dans un formulaire de contact serverless avec Cloudflare Workers et D1.
Redirections et en-têtes
Configurez-les chez l’hébergeur. Sur Cloudflare Pages et Netlify, un fichier _redirects placé dans public/ est copié dans out/ :
/old-page /new-page 301
/blog/* /journal/:splat 301Et un fichier _headers pour les en-têtes de cache et de sécurité :
/_next/static/*
Cache-Control: public, max-age=31536000, immutableMettre à jour le contenu sans serveur
Comme il n’y a pas d’ISR, chaque changement de contenu demande un nouveau build. C’est moins contraignant qu’il n’y paraît : connectez l’hébergeur à votre dépôt Git, et chaque push construit et déploie en une ou deux minutes. Pour un contenu géré dans un CMS, faites pointer le webhook du CMS vers le hook de déploiement de l’hébergeur. Pour un blog, garder les articles en Markdown dans le dépôt reste l’option la plus simple — voir un blog Markdown avec Next.js sans CMS.
Quand l’export statique n’est pas le bon choix
- Des pages qui dépendent de l’utilisateur connecté, rendues côté serveur.
- Des milliers de pages qui changent toutes les quelques minutes (le temps de build augmente avec le nombre de pages).
- Tout ce qui exige vraiment un rendu à la requête, comme des prix personnalisés.
Pour les sites vitrines, portfolios, documentations, blogs et pages d’atterrissage, l’export statique est généralement l’option la plus rapide, la moins chère et la plus robuste qui soit.
Check-list
-
output: 'export'dansnext.config - Chaque route dynamique a
generateStaticParamsetdynamicParams = false - Images gérées (unoptimized, loader personnalisé ou
<img>) - Pas de
cookies(),headers(), Server Actions ni middleware - Redirections et en-têtes configurés chez l’hébergeur
- Déploiements déclenchés par les push Git ou les webhooks du CMS
-
out/prévisualisé en local avant le premier déploiement
Étape suivante : faites en sorte que les moteurs de recherche comprennent les pages exportées — le SEO technique pour l’App Router de Next.js.

