Next.js SEO complet : metadata, sitemap, robots et JSON-LD
Next.js App Router a entièrement repensé la façon de gérer le SEO. Metadata API, opengraph-image.tsx, sitemap.ts, robots.ts, canonical — voici la configuration complète.
Avant l'App Router, le SEO Next.js passait par next/head dans chaque page — verbeux, répétitif, facile à oublier. Depuis Next.js 13+, la Metadata API centralise tout : titre, description, Open Graph, Twitter Cards, canonical, robots. Et les fichiers sitemap.ts, robots.ts, opengraph-image.tsx sont des conventions du framework, pas des configs à brancher soi-même.
Ce guide couvre la configuration complète, en partant de zero.
metadataBase — la fondation
Avant tout, définir metadataBase dans le layout racine. Sans ça, les URLs d'images OG et les canonicals seront relatives — invalides pour les plateformes sociales et Google.
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://iducation.fr'),
title: {
template: '%s — Iducation',
default: 'Iducation — Apprendre le développement web',
},
description: 'Articles techniques sur le développement web, DevOps, et l\'intelligence artificielle.',
openGraph: {
siteName: 'Iducation',
locale: 'fr_FR',
type: 'website',
},
twitter: {
card: 'summary_large_image',
site: '@iducation',
},
robots: {
index: true,
follow: true,
},
}Le template dans title signifie que chaque page n'a besoin de définir que son propre titre — le suffixe "— Iducation" s'ajoute automatiquement.
Metadata statique par page
Pour les pages dont le contenu est connu à build time :
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'À propos',
description: 'William Loree, développeur web et créateur d\'Iducation. Articles sur React, Node.js, DevOps et IA.',
alternates: {
canonical: '/about',
},
openGraph: {
title: 'À propos — Iducation',
description: 'William Loree, développeur web et créateur d\'Iducation.',
type: 'website',
},
}alternates.canonical accepte une URL relative — Next.js la complète avec metadataBase.
generateMetadata — pages dynamiques
Pour les articles, formations, ou toute page dont le contenu vient de données :
import type { Metadata } from 'next'
import { getArticleBySlug, getAllArticles } from '@/lib/articles'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
const articles = getAllArticles()
return articles.map(a => ({ slug: a.slug }))
}
export async function generateMetadata({
params,
}: {
params: { slug: string }
}): Promise<Metadata> {
const article = getArticleBySlug(params.slug)
if (!article) notFound()
return {
title: article.title,
description: article.excerpt,
alternates: {
canonical: `/articles/${article.slug}`,
},
openGraph: {
title: article.title,
description: article.dek,
type: 'article',
publishedTime: article.date,
authors: ['William Loree'],
tags: article.tags,
},
twitter: {
title: article.title,
description: article.dek,
},
}
}generateStaticParams + generateMetadata ensemble = pages statiquement générées avec métadonnées correctes à build time. Aucun rendu serveur à la demande, aucun TTFB élevé.
Images Open Graph dynamiques
Next.js génère des images OG via des fichiers opengraph-image.tsx co-localisés avec les pages.
import { ImageResponse } from 'next/og'
import { getArticleBySlug } from '@/lib/articles'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/jpeg'
export const revalidate = false
export default async function Image({ params }: { params: { slug: string } }) {
const article = getArticleBySlug(params.slug)
return new ImageResponse(
(
<div
style={{
background: '#0a0a0a',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
padding: '60px',
fontFamily: 'Arial, sans-serif',
}}
>
<div style={{ color: '#cdff4f', fontSize: 18, letterSpacing: '0.15em', marginBottom: 16 }}>
IDUCATION · {article?.kicker?.toUpperCase()}
</div>
<div style={{ color: '#fff', fontSize: 52, fontWeight: 700, lineHeight: 1.15, maxWidth: 950 }}>
{article?.title}
</div>
<div style={{ color: '#888', fontSize: 20, marginTop: 20 }}>
{article?.dek}
</div>
</div>
),
{ ...size }
)
}Next.js sert cette image sur /articles/[slug]/opengraph-image et l'injecte automatiquement dans les balises OG et Twitter — sans rien ajouter dans generateMetadata. Open Graph et Twitter Cards couvrent les spécifications de taille et les outils de débogage.
sitemap.ts
import type { MetadataRoute } from 'next'
import { getAllArticles } from '@/lib/articles'
import { getAllFormations } from '@/lib/formations'
export default function sitemap(): MetadataRoute.Sitemap {
const articles = getAllArticles()
const formations = getAllFormations()
const staticPages = [
{ url: 'https://iducation.fr', lastModified: new Date(), changeFrequency: 'weekly' as const, priority: 1 },
{ url: 'https://iducation.fr/articles', lastModified: new Date(), changeFrequency: 'daily' as const, priority: 0.9 },
{ url: 'https://iducation.fr/formations', lastModified: new Date(), changeFrequency: 'weekly' as const, priority: 0.8 },
{ url: 'https://iducation.fr/about', lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.5 },
]
const articlePages = articles.map(article => ({
url: `https://iducation.fr/articles/${article.slug}`,
lastModified: new Date(article.date),
changeFrequency: 'monthly' as const,
priority: article.featured ? 0.9 : 0.7,
}))
const formationPages = formations.map(formation => ({
url: `https://iducation.fr/formations/${formation.slug}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
}))
return [...staticPages, ...articlePages, ...formationPages]
}Next.js sert automatiquement ce sitemap sur /sitemap.xml. À soumettre dans Google Search Console.
robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/_next/', '/admin/'],
},
],
sitemap: 'https://iducation.fr/sitemap.xml',
host: 'https://iducation.fr',
}
}Servi sur /robots.txt. La règle de base : ne bloquer que ce qui ne doit vraiment pas être indexé. Bloquer /api/ et /_next/ est standard.
JSON-LD — données structurées
Les données structurées ne s'injectent pas via la Metadata API — elles vont directement dans le JSX.
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = getArticleBySlug(params.slug)
if (!article) notFound()
const jsonLd = {
'@context': 'https://schema.org',
'@type': article.schema?.type === 'HowTo' ? 'HowTo' : 'TechArticle',
headline: article.title,
description: article.dek,
image: `https://iducation.fr/articles/${article.slug}/opengraph-image`,
datePublished: `${article.date}T00:00:00Z`,
dateModified: `${article.date}T00:00:00Z`,
author: {
'@type': 'Person',
name: 'William Loree',
url: 'https://iducation.fr/about',
},
publisher: {
'@type': 'Organization',
name: 'Iducation',
url: 'https://iducation.fr',
logo: {
'@type': 'ImageObject',
url: 'https://iducation.fr/logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://iducation.fr/articles/${article.slug}`,
},
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* Contenu de la page */}
</>
)
}Gestion des robots par page
Pour exclure certaines pages de l'indexation :
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
noarchive: true,
googleBot: {
index: false,
follow: false,
},
},
}Vérifier le résultat
# Tester que les balises sont dans le HTML servi
curl -s https://iducation.fr/articles/docker-introduction | grep -E '<title|og:|twitter:|canonical'
# Vérifier le sitemap
curl https://iducation.fr/sitemap.xml
# Vérifier robots.txt
curl https://iducation.fr/robots.txtChecklist SEO Next.js
✅ metadataBase défini dans app/layout.tsx
✅ title.template configuré
✅ generateMetadata dans chaque route dynamique
✅ generateStaticParams pour les routes dynamiques SSG
✅ alternates.canonical dans chaque page
✅ opengraph-image.tsx par type de contenu
✅ app/sitemap.ts avec toutes les URLs
✅ app/robots.ts avec disallow sur /api/
✅ JSON-LD dans les pages article
✅ Vérification HTML serveur (curl)
✅ Sitemap soumis dans Search ConsoleNext.js rend le SEO structurel : les conventions du framework imposent les bonnes pratiques. Le risque principal est l'omission — oublier generateMetadata sur une page dynamique, ou ne pas définir metadataBase. Un curl après chaque déploiement sur les pages importantes coûte 30 secondes et évite des semaines de désindexation. Pour l'analyse des images dans ce contexte, le guide SEO des images et Open Graph complètent ce tableau.