Retour au blog

SEO technique pour l’App Router de Next.js : une check-list pratique

SEO technique pour l’App Router de Next.js : une check-list pratique

9 octobre 2026 — SEO

Publié le

9 octobre 2026

Temps de lecture

4 min de lecture

Catégorie

SEO

Mots-clés

  • SEO
  • Next.js
  • App Router
  • Metadata
  • Structured Data

(Intro)Métadonnées, URL canoniques, sitemaps, robots.txt, données structurées, hreflang et Core Web Vitals dans l’App Router de Next.js, avec du code à copier.

Next.js fournit tout ce qu’il faut pour le SEO technique, mais presque rien n’est activé par défaut. Voici la check-list que je suis sur chaque projet App Router, à peu près par ordre d’impact.

1. Rendre côté serveur le contenu qui compte

Les moteurs de recherche savent exécuter le JavaScript, mais ils indexent plus vite et plus sûrement le HTML rendu côté serveur. Dans l’App Router, les Server Components sont la norme : gardez-y vos titres, vos textes et vos liens, et réservez les Client Components à l’interaction, pas au contenu.

Un test rapide : ouvrez votre page avec le JavaScript désactivé, ou lancez curl https://yoursite.com/page. Si le texte principal n’est pas dans la réponse, il n’est pas non plus dans le premier passage d’indexation.

2. Définir metadataBase et un modèle de titre

Dans le layout racine :

app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: {
    default: 'Example Studio — Web Design and Development',
    template: '%s — Example Studio',
  },
  description: 'Websites and products for small teams: design, development and launch.',
  openGraph: { siteName: 'Example Studio', type: 'website' },
}

metadataBase transforme les URL relatives (canonique, images Open Graph) en URL absolues. Grâce au modèle, les pages enfants ne définissent que leur propre partie du titre.

3. Un titre et une description uniques par page

Chaque page indexable a besoin de son propre <title> (environ 50 à 60 caractères, mot-clé au début) et de sa propre meta description (environ 150 à 160 caractères, rédigée pour donner envie de cliquer). Pour les pages dynamiques, utilisez generateMetadata :

app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) return {}
  return {
    title: post.title,
    description: post.excerpt,
    alternates: { canonical: `/blog/${post.slug}` },
    openGraph: {
      type: 'article',
      publishedTime: post.date,
      images: [{ url: post.cover, width: 1200, height: 630 }],
    },
  }
}

4. Les URL canoniques

Définissez alternates.canonical sur chaque page. Cela vous protège des doublons créés par les paramètres d’URL (?utm_source=...), les barres obliques finales et les domaines alternatifs. Choisissez une forme — avec ou sans www, avec ou sans barre oblique finale — et redirigez les autres vers elle au niveau de l’hébergeur.

5. Un sitemap

app/sitemap.ts génère /sitemap.xml, y compris en export statique :

app/sitemap.ts
import type { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/blog'

export const dynamic = 'force-static'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const base = 'https://example.com'
  const posts = await getAllPosts()
  return [
    { url: base, changeFrequency: 'monthly', priority: 1 },
    ...posts.map((post) => ({
      url: `${base}/blog/${post.slug}`,
      lastModified: post.date,
      changeFrequency: 'yearly' as const,
      priority: 0.7,
    })),
  ]
}

Soumettez-le une fois dans la Google Search Console et dans Bing Webmaster Tools.

6. robots.txt

app/robots.ts
import type { MetadataRoute } from 'next'

export const dynamic = 'force-static'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/' },
    sitemap: 'https://example.com/sitemap.xml',
  }
}

N’utilisez pas le robots.txt pour retirer des pages de l’index : une page bloquée peut tout de même être indexée via des liens. Utilisez plutôt robots: { index: false } dans les métadonnées de la page.

7. Les données structurées (JSON-LD)

Les données structurées aident les moteurs à comprendre ce qu’est une page. Pour un article, affichez une balise script dans la page :

tsx
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.title,
  description: post.excerpt,
  datePublished: post.date,
  image: `https://example.com${post.cover}`,
  author: { '@type': 'Person', name: 'Jane Doe', url: 'https://example.com' },
}

return (
  <>
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }}
    />
    <article>{/* ... */}</article>
  </>
)

Types utiles pour une petite entreprise : Organization ou Person sur la page d’accueil, BlogPosting sur les articles, BreadcrumbList sur les pages profondes, Product sur les pages boutique. Validez-les avec le test des résultats enrichis de Google.

8. Les images Open Graph

Les liens partagés sur LinkedIn, X, Slack et les messageries utilisent les balises Open Graph. Donnez à chaque page une image de 1200×630 pixels. Vous pouvez placer un opengraph-image.png statique à côté d’une page, ou en générer un avec ImageResponse dans opengraph-image.tsx (généré au build en export statique, il ne peut donc pas dépendre de la requête).

9. Sites multilingues : de vraies URL et hreflang

Chaque langue a besoin de sa propre URL, sinon les moteurs n’en verront jamais qu’une. Changer de langue côté client via localStorage convient aux visiteurs, mais reste invisible pour Google. Utilisez un segment de langue — app/[lang]/... — et déclarez les alternatives :

ts
alternates: {
  canonical: '/en/blog/my-post',
  languages: {
    en: '/en/blog/my-post',
    'zh-CN': '/zh/blog/my-post',
    'sr-Latn': '/sr/blog/my-post',
    'x-default': '/en/blog/my-post',
  },
}

Chaque version linguistique doit lister toutes les autres, elle-même comprise.

10. Liens internes et titres

  • Un seul <h1> par page, puis des <h2>/<h3> dans l’ordre. Les titres sont un plan, pas une taille de police.
  • Reliez les pages proches entre elles avec des ancres descriptives (« guide de l’export statique », pas « cliquez ici »). Utilisez <Link> pour que les liens soient de vrais éléments <a href>.
  • Gardez les pages importantes à trois clics maximum de la page d’accueil.

11. Core Web Vitals

La vitesse est un signal de classement et, surtout, un signal de conversion. Mesurez avec PageSpeed Insights (les données terrain viennent de vrais utilisateurs de Chrome) et visez LCP ≤ 2,5 s, INP ≤ 200 ms et CLS ≤ 0,1. Les coupables habituels dans l’App Router : une image de hero surdimensionnée, trop de JavaScript côté client, et des décalages de mise en page dus aux polices ou aux images sans dimensions. Si votre site est très animé, voir Core Web Vitals pour sites animés.

12. Les images

  • Un texte alt descriptif sur les images de contenu ; un alt="" vide sur les images décoratives.
  • Des width/height explicites ou un aspect-ratio pour éviter les décalages.
  • Des formats modernes (AVIF, WebP) et des tailles raisonnables.
  • Des noms de fichiers parlants : nextjs-sitemap-example.png, pas IMG_2041.png.

13. Les 404 et les redirections

Renvoyez un vrai 404 pour les pages manquantes (notFound() dans l’App Router). Quand vous déplacez ou renommez une page, ajoutez une redirection 301 depuis l’ancienne URL — en export statique, c’est une règle _redirects au niveau de l’hébergeur.

La version courte

  • Le contenu dans des Server Components
  • metadataBase et un modèle de titre dans le layout racine
  • Titre, description et URL canonique uniques sur chaque page
  • sitemap.ts et robots.ts, sitemap soumis
  • JSON-LD pour les articles, l’organisation, le fil d’Ariane
  • Une image Open Graph sur chaque page
  • Une URL par langue, avec hreflang
  • Des titres logiques et des liens internes descriptifs
  • Des Core Web Vitals au vert
  • De vrais 404 et des 301 pour les pages déplacées

Vous construisez un site statique ? Lisez le guide de l’export statique avec Next.js pour les aspects qui fonctionnent différemment sans serveur.

Newsletter

Recevez les nouveaux articles par e-mail

Un court e-mail à chaque nouvelle publication. Pas de spam, et désinscription possible à tout moment.

Un blog Markdown avec Next.js, sans CMS ni base de données
Un blog Markdown avec Next.js, sans CMS ni base de données
Article suivant