
Next.js fournit tous les blocs nécessaires à un excellent SEO : SSR/SSG pour la crawlabilité, Metadata API pour des aperçus riches, et outillage performance pour réussir les Core Web Vitals. Dans cet article, on met en place l’essentiel et on évite les pièges courants.
Pourquoi Next.js pour le SEO ?
- HTML pré‑rendu (SSG/SSR) → meilleure exploration et premier rendu plus rapide
- App Router & Metadata API → titres propres, URLs canoniques, Open Graph/Twitter cards
- Optimisation d’images intégrée → LCP plus rapide avec images responsives et lazy‑load
- Edge & stratégie de cache → garder les pages fraîches sans sacrifier la vitesse
1) Metadata API : titres, canoniques et aperçus
Définissez des valeurs par défaut dans app/layout.tsx
, puis surchargez par page. Les canoniques préviennent les contenus dupliqués ; Open Graph/Twitter génèrent des aperçus de liens de qualité.
// app/layout.tsx
import type { Metadata } from "next";
const baseUrl = "https://example.com";
export const metadata: Metadata = {
metadataBase: new URL(baseUrl),
title: {
default: "Example — Construisez vite avec Next.js",
template: "%s · Example",
},
description: "Site Next.js orienté performance et SEO.",
alternates: {
canonical: "/",
},
openGraph: {
type: "website",
url: baseUrl,
title: "Example — Construisez vite avec Next.js",
description: "Starter Next.js performance‑first.",
images: ["/og-default.png"],
},
twitter: {
card: "summary_large_image",
creator: "@example",
},
};
Surcharges par page avec generateMetadata
:
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { getPostBySlug } from "@/lib/posts";
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
const title = post.seoTitle ?? post.title;
const url = `/blog/${post.slug}`;
const image = post.coverImage ?? "/og-default.png";
return {
title,
description: post.description,
alternates: { canonical: url },
openGraph: {
type: "article",
url,
title,
description: post.description,
publishedTime: post.date,
authors: post.author ? [post.author] : undefined,
images: [image],
},
twitter: {
card: "summary_large_image",
title,
description: post.description,
images: [image],
},
robots: { index: true, follow: true },
};
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return <article className="prose">{/* Contenu MDX... */}</article>;
}
Astuce : Pour les listes paginées/filtrées, mettez la canonique sur la page de base et, si vous avez une pagination profonde, gérez
rel="next"
/rel="prev"
via<link>
dansgenerateMetadata
.
2) Robots.txt & sitemap sans plugins
Générez les deux avec les conventions natives.
// app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: "*", allow: "/" },
// Interdire des chemins privés :
{ userAgent: "*", disallow: ["/api/", "/admin/"] },
],
sitemap: "https://example.com/sitemap.xml",
};
}
// app/sitemap.ts
import type { MetadataRoute } from "next";
import { getAllPostSlugs } from "@/lib/posts";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = "https://example.com";
const slugs = await getAllPostSlugs();
const posts = slugs.map((slug) => ({
url: `${baseUrl}/blog/${slug}`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.7,
}));
return [
{ url: `${baseUrl}/`, lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
{ url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: "daily", priority: 0.9 },
...posts,
];
}
Gros site ? Pensez à découper le sitemap en plusieurs fichiers et à automatiser la génération pour rester léger et frais.
3) Données structurées JSON‑LD (Article, Breadcrumbs, etc.)
Les moteurs adorent les données structurées. Pour un article de blog, utilisez le schéma Article
. Ajoutez un petit composant client pour injecter du JSON‑LD en sécurité.
// components/JsonLd.tsx
"use client";
import Script from "next/script";
export default function JsonLd({ id, data }: { id: string; data: any }) {
return (
<Script
id={id}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
// app/blog/[slug]/page.tsx (dans le rendu)
import JsonLd from "@/components/JsonLd";
const schema = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.description,
datePublished: post.date,
author: post.author ? [{ "@type": "Person", name: post.author }] : undefined,
image: post.coverImage ? [`https://example.com${post.coverImage}`] : undefined,
mainEntityOfPage: `https://example.com/blog/${post.slug}`,
};
return (
<article className="prose">
{/* ...contenu... */}
<JsonLd id="blogpost-jsonld" data={schema} />
</article>
);
Ajoutez BreadcrumbList sur les pages d’articles pour de meilleurs sitelinks.
4) Des images qui rankent (et passent le LCP)
Utilisez <Image />
partout : tailles responsives, texte alternatif, et priority
pour les éléments au‑dessus de la ligne de flottaison.
import Image from "next/image";
export default function Cover({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={1200}
height={630}
priority
sizes="(max-width: 768px) 100vw, 1200px"
className="rounded-xl"
/>
);
}
Checklist :
alt
descriptif (sans bourrage de mots‑clés)- Dimensions
width/height
correctes pour éviter la CLS - Utilisez AVIF/WebP quand c’est possible
- Marquez l’image principale en
priority
pour aider le LCP
5) SEO international & multi‑locale (optionnel)
Exposez hreflang
via alternates.languages
:
// app/layout.tsx (metadata)
alternates: {
canonical: "/",
languages: {
"en-US": "/en",
"fr-FR": "/fr",
},
},
Assurez‑vous que le routage reflète les locales (/en/...
) et que le contenu est réellement localisé (pas juste des métadonnées traduites).
6) Fraîcheur avec ISR, cache et revalidation
Garder du statique rapide et à jour.
// app/blog/[slug]/page.tsx
export const revalidate = 60 * 60; // régénérer au plus une fois par heure
// Si vous déclenchez ailleurs, pensez aux tags :
// import { revalidateTag } from "next/cache";
// revalidateTag("posts");
Utilisez fetch(..., { next: { revalidate: 3600, tags: ["posts"] } })
sur les requêtes de données pour aligner pages et data.
7) Images OG dynamiques (gros gain de CTR)
Générez des aperçus uniques par article pour les réseaux sociaux / Slack.
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function OG({ params }: { params: { slug: string } }) {
// vous pouvez charger les données de l’article ici
const title = decodeURIComponent(params.slug).replace(/-/g, " ");
return new ImageResponse(
(
<div
style={{
display: "flex",
width: "100%",
height: "100%",
alignItems: "center",
justifyContent: "center",
fontSize: 72,
fontWeight: 800,
}}
>
{title}
</div>
),
{ ...size }
);
}
8) Contenu & maillage interne (faible effort, gros impact)
- Un sujet principal par page ; utilisez un
<h1>
clair et une hiérarchie logique de titres. - Liez les articles connexes avec
<a>
/<Link>
et un texte d’ancre descriptif. - Gardez des slugs courts, lisibles et stables (
/blog/boost-seo-with-nextjs
).
9) Analytics & monitoring
- Suivez les Core Web Vitals (LCP, CLS, INP) et corrigez tôt.
- Ajoutez l’analytics avec
next/script
etstrategy: "afterInteractive"
pour ne pas bloquer le rendu.
10) Pièges courants à éviter
- Canonique manquante sur listes paginées/filtrées → contenu dupliqué
- Images lourdes sans dimensions définies → la CLS explose
- Pas de
robots.txt
/sitemap.xml
→ couverture d’exploration incomplète - Contenu critique rendu uniquement côté client → préférez le rendu serveur quand c’est possible
Checklist de pré‑publication SEO
- Titre et meta description uniques
- Canonique définie (et hreflang si multilingue)
- Open Graph/Twitter images présentes
- JSON‑LD Article + (optionnel) BreadcrumbList
- Images optimisées (
<Image>
, alt, tailles correctes) - LCP < 2,5s mobile ; CLS < 0,1 ; INP < 200ms
robots.txt
etsitemap.xml
générés- Liens internes vers/depuis des contenus liés
En résumé : Next.js fournit les bons primitifs pour rendre le SEO simple — HTML pré‑rendu, Metadata API puissante, données structurées faciles, et des outils de performance pour viser les Core Web Vitals. Une fois les bons patterns en place, chaque nouvelle page est prête à ranker.
Août 2025