Changer le thème

Guide SEO Next.js complet : Metadata API + données structurées en pratique

Easton editorial illustration: hydration gauge console

Le tableau de bord Google Search Console affiche un gros « 0 » — jour 23 après le lancement. Deux mois de dev, des nuits blanches, une UI soignée, une UX fluide : tout réduit à néant face à ce chiffre glacial.

Pire encore : en partageant le lien sur Twitter, l’aperçu reste vide. Pas même une couverture décente.

« Next.js a le SSR intégré, pourquoi le SEO est-il si mauvais ? » Après avoir parcouru la doc officielle, une vérité cruelle : le SSR ne signifie pas SEO-friendly. Meta tags mal configurés, données structurées absentes, Open Graph ignoré — les moteurs vous traitent comme de l’air.

Ce douleur, vous la connaissez peut-être : un produit abouti, introuvable en recherche, peu professionnel au partage, promotion payante obligatoire. Avec la Metadata API de Next.js 15 et quelques réglages clés, ces problèmes se résolvent.

Ce guide vous montre : metadata uniques par page via la Metadata API, données structurées pour des SERP plus attractives, aperçus sociaux impeccables. Et surtout 5 pièges SEO que j’ai moi-même frôlés.

Pourquoi le SEO de votre site Next.js est mauvais ?

Le SSR ne signifie pas SEO-friendly

Au début, je pensais pareil : Next.js, SSR, HTML servi aux crawlers — SEO parfait, non ?

Naïf.

En regardant le code d’un ami dans les DevTools, toutes les pages avaient <title>My App</title>, <meta name="description"> identique ou absent. Comme une boutique haut de gamme avec une enseigne « Magasin » — personne ne sait ce que vous vendez.

Next.js fournit le SSR, mais vous devez configurer les meta tags. Sans ça, du HTML vide que Google ne comprend pas.

5 erreurs SEO fatales

Trop de devs — moi inclus — tombent dans ces pièges :

1. Un seul title et description pour toutes les pages

L’erreur la plus courante : title figé dans _document.tsx ou absent. Google affiche le même titre pour accueil, à propos et produits.

Comme des livres avec le même titre en couverture — vous achèteriez ?

2. Oublier l’URL canonique → contenu dupliqué

Piège discret : pagination (?page=2), filtres (?category=tech), tri (?sort=date) — mêmes vues, URLs différentes. Sans canonical, Google pénalise le « duplicate content ».

Un blog client a perdu 40 % de trafic pour ça ; canonical ajouté, retour à la normale en deux semaines.

3. Pas de données structurées → rich snippets manqués

En cherchant « recette tarte aux pommes », certaines fiches affichent note, temps, calories — ce n’est pas deviné, c’est du JSON-LD.

Les sites avec données structurées voient en moyenne +20 à 30 % de CTR — l’équivalent de +30 % de trafic pour quelques lignes de code.

4. Images sans alt → trafic image search perdu

Beaucoup croient que alt sert seulement l’accessibilité. Faux.

Google Images est un canal énorme. Un site de ressources design tire 30 % de son trafic de là grâce à des alt soignés.

5. Pas de sitemap.xml ni robots.txt

Ils indiquent quelles pages crawler et lesquelles éviter. Sans sitemap, Google met des mois à découvrir un nouvel article ; avec soumission Search Console, indexation en jours.

Next.js App Router génère sitemap.ts et robots.ts — pourquoi s’en priver ?

Maîtriser la Metadata API (Next.js 15)

La Metadata API est le plus grand cadeau SEO de Next.js. Fini le <Head> page par page : exportez un objet ou une fonction.

Metadata statique : pages à contenu fixe

Pour « À propos », politique de confidentialité, etc. :

// app/about/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'À propos - TechBlog',
  description: 'Une équipe de développeurs passionnés partageant frontend, backend et DevOps.',
  keywords: ['blog tech', 'développement frontend', 'Next.js', 'React'],
  authors: [{ name: 'Zhang San' }],
  openGraph: {
    title: 'À propos - TechBlog',
    description: 'Blog tech, retours d\'expérience dev',
    url: 'https://yourdomain.com/about',
    siteName: 'TechBlog',
    images: [
      {
        url: 'https://yourdomain.com/og-about.jpg',
        width: 1200,
        height: 630,
      }
    ],
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'À propos - TechBlog',
    description: 'Blog tech, retours d\'expérience dev',
    images: ['https://yourdomain.com/og-about.jpg'],
  },
}

export default function AboutPage() {
  return <div>Contenu à propos...</div>
}

Typage, autocomplétion IDE, déduplication automatique des meta tags.

Bonnes pratiques :

  • title : 60 caractères max (troncature en SERP)
  • description : 150-160 caractères
  • openGraph.images : 1200x630 px (Twitter, Facebook, LinkedIn)

Metadata dynamique : blogs et fiches produit

generateMetadata brille pour les articles :

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { getPostBySlug } from '@/lib/posts'

export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPostBySlug(params.slug)

  return {
    title: `${post.title} - TechBlog`,
    description: post.excerpt,
    authors: [{ name: post.author }],
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)
  return <article>{post.content}</article>
}

Chaque article a son SEO ; Google reçoit du HTML complet sans attendre le JS client.

Astuce : metadataBase

Pour des chemins relatifs (/images/cover.jpg), configurez dans le root layout :

// app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL('https://yourdomain.com'),
}

Les URLs relatives deviennent absolues — sinon Open Graph échoue et le partage n’a pas d’image.

Templates : format de titre uniforme

« Page | TechBlog » partout ? Utilisez title.template :

// app/layout.tsx (root layout)
export const metadata: Metadata = {
  title: {
    template: '%s | TechBlog',
    default: 'TechBlog - Blog technique',
  },
  description: 'Blog tech : frontend, backend, DevOps',
  metadataBase: new URL('https://yourdomain.com'),
}

Page enfant :

// app/about/page.tsx
export const metadata: Metadata = {
  title: 'À propos', // rendu : "À propos | TechBlog"
}

Accueil sans suffixe : title.absolute :

// app/page.tsx
export const metadata: Metadata = {
  title: {
    absolute: 'TechBlog - Accueil du blog technique',
  },
}

Cohérence des titres ; un changement dans le root layout suffit.

Données structurées (Schema.org) pour se démarquer

Qu’est-ce que c’est et pourquoi c’est important ?

Cherchez « recette tarte aux pommes » : certaines fiches montrent note (4,8), temps (45 min), calories — d’autres seulement titre et extrait.

La différence : JSON-LD.

Format standard pour dire aux moteurs : « article de blog, auteur X, publié le Y » ou « produit, prix Z, note W ». Résultat : rich snippets (notes, prix, auteur).

+20 à 30 % de CTR en moyenne — coût quasi nul.

Types Schema courants

Parmi des centaines, les plus utiles :

  1. Organization — entreprise (page d’accueil)
  2. BlogPosting — articles de blog
  3. Product — e-commerce (prix, note, stock)
  4. FAQPage — FAQ dépliable en SERP
  5. LocalBusiness — commerce local (adresse, horaires)

Focus ici sur les deux premiers.

JSON-LD dans Next.js

Le composant <Script> simplifie tout :

// components/StructuredData.tsx
import Script from 'next/script'

type StructuredDataProps = {
  data: object
}

export default function StructuredData({ data }: StructuredDataProps) {
  return (
    <Script
      id="structured-data"
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  )
}

Exemple 1 : Organization

// app/layout.tsx (root layout)
import StructuredData from '@/components/StructuredData'

const organizationData = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: 'TechBlog',
  url: 'https://yourdomain.com',
  logo: 'https://yourdomain.com/logo.png',
  sameAs: [
    'https://twitter.com/yourusername',
    'https://github.com/yourcompany',
    'https://linkedin.com/company/yourcompany',
  ],
  contactPoint: {
    '@type': 'ContactPoint',
    email: '[email protected]',
    contactType: 'Customer Service',
  },
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <StructuredData data={organizationData} />
      </body>
    </html>
  )
}

Exemple 2 : BlogPosting

// app/blog/[slug]/page.tsx
import StructuredData from '@/components/StructuredData'
import { getPostBySlug } from '@/lib/posts'

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)

  const articleData = {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.excerpt,
    image: post.coverImage,
    author: {
      '@type': 'Person',
      name: post.author,
      url: `https://yourdomain.com/author/${post.authorSlug}`,
    },
    publisher: {
      '@type': 'Organization',
      name: 'TechBlog',
      logo: {
        '@type': 'ImageObject',
        url: 'https://yourdomain.com/logo.png',
      },
    },
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://yourdomain.com/blog/${post.slug}`,
    },
  }

  return (
    <>
      <article>{post.content}</article>
      <StructuredData data={articleData} />
    </>
  )
}

Beaucoup de code, mais ce sont les métadonnées de l’article en format standard. Configurez une fois, copiez ensuite.

Valider les données structurées

  1. Google Rich Results Test (https://search.google.com/test/rich-results)

    • URL → quels rich results sont possibles
    • Erreurs explicites
  2. Schema Markup Validator (https://validator.schema.org/)

    • Conformité Schema.org, plus strict que Google

Erreurs fréquentes : publisher manquant (BlogPosting), URL d’image non absolue. Corrigez et retestez.

Open Graph et Twitter Cards en pratique

Pourquoi le partage social compte

Partage Twitter ou Facebook : aperçu vide ou mauvaise image — peu professionnel. Bien configuré : belle couverture, titre, extrait — 2 à 3× plus de clics.

Open Graph et Twitter Cards contrôlent l’affichage des liens.

Protocole Open Graph

Standard Facebook, adopté par Twitter, LinkedIn, Slack, Discord.

Champs clés :

export const metadata: Metadata = {
  openGraph: {
    title: 'Titre de l\'article',
    description: 'Résumé, ~150 caractères',
    url: 'https://yourdomain.com/article',
    siteName: 'TechBlog',
    images: [
      {
        url: 'https://yourdomain.com/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'Description image (accessibilité et SEO)',
      },
    ],
    type: 'article',
    publishedTime: '2025-01-15T08:00:00.000Z',
    modifiedTime: '2025-01-16T10:30:00.000Z',
    authors: ['Zhang San', 'Li Si'],
    tags: ['Next.js', 'SEO', 'développement frontend'],
    locale: 'zh_CN',
    alternateLocale: ['en_US', 'ja_JP'],
  },
}

Dimensions image : 1200x630 (ratio 1,91:1), parfait sur Facebook, LinkedIn, Twitter, Slack. Max 8 Mo.

Twitter Cards

Twitter a ses propres meta tags ; configurez-les explicitement :

export const metadata: Metadata = {
  twitter: {
    card: 'summary_large_image',
    site: '@yourusername',
    creator: '@authorusername',
    title: 'Titre de l\'article',
    description: 'Résumé',
    images: ['https://yourdomain.com/twitter-image.jpg'],
  },
}
  • summary : petite vignette
  • summary_large_image : grande image (recommandé)

Limite Twitter : 5 Mo par image.

Génération dynamique d’images OG (avancé)

Créer manuellement 1200x630 par article ? Épuisant.

Next.js 13.3+ génère les OG par code :

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPostBySlug } from '@/lib/posts'

export const runtime = 'edge'
export const alt = 'Couverture article de blog'
export const size = {
  width: 1200,
  height: 630,
}
export const contentType = 'image/png'

export default async function Image({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 60,
          background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          color: 'white',
          padding: '80px',
        }}
      >
        <h1 style={{ fontSize: 72, fontWeight: 'bold', textAlign: 'center' }}>
          {post.title}
        </h1>
        <p style={{ fontSize: 36, marginTop: 20, opacity: 0.9 }}>
          by {post.author}
        </p>
      </div>
    ),
    {
      ...size,
    }
  )
}

Image de partage générée depuis le titre — sans design manuel. Voir la doc next/og pour polices et arrière-plans custom.

Tester le partage social

  1. Facebook Sharing Debugger (https://developers.facebook.com/tools/debug/)

    • Aperçu Facebook ; « Scrape Again » pour vider le cache OG
  2. Twitter Card Validator (https://cards-dev.twitter.com/validator)

    • Compte développeur requis depuis 2023 ; sinon test en publiant un tweet
  3. LinkedIn Post Inspector (https://www.linkedin.com/post-inspector/)

Piège vécu : nouvelle image OG, Facebook montre l’ancienne — rafraîchissez le cache dans Sharing Debugger.

Autres configurations SEO indispensables

Metadata API, données structurées et Open Graph sont le cœur ; ne négligez pas le reste.

sitemap.xml — lister vos pages

Fichier XML listant les URL. Google et Bing accélèrent l’indexation.

Créez app/sitemap.ts :

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

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts()
  const baseUrl = 'https://yourdomain.com'

  const staticPages: MetadataRoute.Sitemap = [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
  ]

  const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }))

  return [...staticPages, ...blogPages]
}

Disponible sur https://yourdomain.com/sitemap.xml.

Ensuite :

  1. Soumettre à Google Search Console
  2. Soumettre à Bing Webmaster Tools

Indexation +50 % plus rapide en pratique : sans sitemap, deux semaines ; avec, trois jours.

robots.txt — permissions des crawlers

Indique ce qui est crawlable :

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

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/admin', '/api', '/private'],
      },
    ],
    sitemap: 'https://yourdomain.com/sitemap.xml',
  }
}

Génère https://yourdomain.com/robots.txt :

User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Disallow: /private

Sitemap: https://yourdomain.com/sitemap.xml

Cas typiques : /admin et /api en disallow ; brouillons via noindex ou Disallow.

URL canonique — éviter la pénalité duplicate

Déclare l’URL « officielle » parmi plusieurs variantes (pagination, filtres, tri).

// app/blog/page.tsx
export const metadata: Metadata = {
  alternates: {
    canonical: 'https://yourdomain.com/blog',
  },
}

Dynamique :

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  return {
    alternates: {
      canonical: `https://yourdomain.com/blog/${params.slug}`,
    },
  }
}

Multilingue avec alternates.languages :

export const metadata: Metadata = {
  alternates: {
    canonical: 'https://yourdomain.com/blog/nextjs-seo',
    languages: {
      'en-US': 'https://yourdomain.com/en/blog/nextjs-seo',
      'ja-JP': 'https://yourdomain.com/ja/blog/nextjs-seo',
    },
  },
}

Optimisation images — next/image + alt

Google Images = gros trafic potentiel.

1. next/image plutôt que &lt;img&gt;

import Image from 'next/image'

<Image
  src="/cover.jpg"
  alt="Couverture du guide SEO Next.js complet"
  width={1200}
  height={630}
  priority
/>

Lazy load, WebP, tailles responsives, moins de CLS (Core Web Vitals).

2. alt obligatoire

SEO + accessibilité (lecteurs d’écran).

Bon :

  • ✅ « Exemple de code Metadata API Next.js »
  • ✅ « Rich snippet d’article de blog dans Google »

Mauvais :

  • ❌ « image »
  • ❌ « screenshot.png »
  • ❌ alt vide

Cas pratique — blog tech SEO complet

Structure type Next.js 15 App Router :

app/
├── layout.tsx                 # Root layout — config globale
├── page.tsx                   # Accueil
├── about/page.tsx            # À propos
├── blog/
│   ├── page.tsx              # Liste articles
│   └── [slug]/
│       ├── page.tsx          # Détail article
│       └── opengraph-image.tsx  # OG dynamique (optionnel)
├── sitemap.ts
└── robots.ts

1. Root Layout

// app/layout.tsx
import { Metadata } from 'next'
import StructuredData from '@/components/StructuredData'

export const metadata: Metadata = {
  metadataBase: new URL('https://yourdomain.com'),
  title: {
    template: '%s | TechBlog',
    default: 'TechBlog - Blog développement frontend',
  },
  description: 'Blog tech : Next.js, React, TypeScript',
  keywords: ['Next.js', 'React', 'TypeScript', 'développement frontend', 'blog tech'],
  authors: [{ name: 'Zhang San', url: 'https://yourdomain.com/about' }],
  openGraph: {
    type: 'website',
    siteName: 'TechBlog',
    locale: 'zh_CN',
  },
  twitter: {
    card: 'summary_large_image',
    site: '@yourusername',
  },
}

const organizationData = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  name: 'TechBlog',
  url: 'https://yourdomain.com',
  logo: 'https://yourdomain.com/logo.png',
  sameAs: [
    'https://twitter.com/yourusername',
    'https://github.com/yourcompany',
  ],
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>
        {children}
        <StructuredData data={organizationData} />
      </body>
    </html>
  )
}

2. Accueil — metadata statique

// app/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    absolute: 'TechBlog - Blog développement frontend',
  },
  description: 'Next.js, React, TypeScript : retours d\'expérience pour monter en compétences',
  openGraph: {
    title: 'TechBlog - Blog développement frontend',
    description: 'Retours d\'expérience frontend',
    url: 'https://yourdomain.com',
    images: [
      {
        url: 'https://yourdomain.com/og-home.jpg',
        width: 1200,
        height: 630,
        alt: 'Couverture accueil TechBlog',
      },
    ],
  },
}

export default function HomePage() {
  return <div>Contenu accueil...</div>
}

3. Détail article — metadata dynamique + JSON-LD

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPostBySlug } from '@/lib/posts'
import StructuredData from '@/components/StructuredData'

export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPostBySlug(params.slug)
  if (!post) return {}

  return {
    title: post.title,
    description: post.excerpt,
    keywords: post.tags,
    authors: [{ name: post.author }],
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url: `https://yourdomain.com/blog/${post.slug}`,
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
    alternates: {
      canonical: `https://yourdomain.com/blog/${post.slug}`,
    },
  }
}

export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)
  if (!post) notFound()

  const articleData = {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    headline: post.title,
    description: post.excerpt,
    image: post.coverImage,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt || post.publishedAt,
    author: {
      '@type': 'Person',
      name: post.author,
    },
    publisher: {
      '@type': 'Organization',
      name: 'TechBlog',
      logo: {
        '@type': 'ImageObject',
        url: 'https://yourdomain.com/logo.png',
      },
    },
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://yourdomain.com/blog/${post.slug}`,
    },
  }

  return (
    <>
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
      <StructuredData data={articleData} />
    </>
  )
}

4. Sitemap et Robots

// app/sitemap.ts
import { getAllPosts } from '@/lib/posts'

export default async function sitemap() {
  const posts = await getAllPosts()
  const baseUrl = 'https://yourdomain.com'

  const blogUrls = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }))

  return [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'daily' as const,
      priority: 1,
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date(),
      changeFrequency: 'monthly' as const,
      priority: 0.8,
    },
    ...blogUrls,
  ]
}

// app/robots.ts
export default function robots() {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/api', '/admin'],
    },
    sitemap: 'https://yourdomain.com/sitemap.xml',
  }
}

Checklist post-déploiement

  1. Code source HTML — F12 → <head> : title, description, OG OK
  2. sitemap et robots/sitemap.xml et /robots.txt accessibles
  3. Données structurées — Google Rich Results Test
  4. Partage social — Facebook Debugger, Twitter Card Validator
  5. Soumission — Search Console + Bing Webmaster Tools

Conclusion

Le SSR Next.js ≠ SEO automatique, mais Next.js 15 rend la configuration simple.

Récap :

  • Metadata API : typée ; metadata statique, generateMetadata dynamique
  • JSON-LD : +20-30 % CTR ; <Script> suffit
  • Open Graph / Twitter Cards : 1200x630 universel
  • sitemap.ts / robots.ts : génération auto ; soumettez à Search Console
  • Images : next/image + alt soignés → trafic Google Images

Ces réglages semblent lourds, ROI énorme. Trop de produits solides restent invisibles faute de SEO ; bien configurés, le trafic organique compense largement.

Le SEO est une science, pas de la magie. Checklist, validation outils, effets visibles en quelques mois.

N’attendez pas le « 0 » en Search Console. Ouvrez votre projet, une demi-journée suffit. Gardez ce guide sous la main.

Si utile, partagez-le — moins de pièges pour vos collègues dev.

Configuration SEO complète Next.js

Étapes complètes d'optimisation SEO : Metadata API, données structurées, sitemap et robots.txt

⏱️ Estimated time: 4 hr

  1. 1

    Step 1: Configurer les metadata de base

    Metadata API Next.js 15 :
    • Exporter l'objet metadata dans layout.js ou page.js
    • Configurer title, description, keywords
    • Définir Open Graph et Twitter Cards

    Exemple :
    export const metadata = {
    title: 'Titre de page',
    description: 'Description de page',
    openGraph: {
    title: 'Titre OG',
    description: 'Description OG',
    images: ['/og-image.jpg']
    }
    }
  2. 2

    Step 2: Configurer les metadata dynamiques

    Pour les routes dynamiques :
    • Utiliser generateMetadata
    • Générer les metadata selon les paramètres de route
    • Support des fonctions async pour récupérer les données

    Exemple :
    export async function generateMetadata({ params }) {
    const post = await getPost(params.id)
    return {
    title: post.title,
    description: post.description
    }
    }
  3. 3

    Step 3: Ajouter des données structurées

    Format JSON-LD :
    • Balise script, type application/ld+json, contenu JSON (implémentation avec balise)
    • Types Article, Product, FAQ, etc.
    • Vocabulaire Schema.org

    Exemple (corps JSON ; sur la page, JSON.stringify dans le script) :
    {
    "@context": "https://schema.org",
    "@type": "Article",
    "headline": "Titre de l'article"
    }

    Next.js : next/script ou équivalent pour sortir le JSON.
  4. 4

    Step 4: Configurer sitemap et robots.txt

    Créer sitemap.ts :
    • Exporter une fonction default retournant un tableau sitemap
    • Inclure URL, lastModified, changeFrequency de chaque page
    • Génération dynamique possible

    Créer robots.ts :
    • Configurer les crawlers autorisés/interdits
    • Définir le chemin du sitemap
    • Règles de crawl
  5. 5

    Step 5: Optimiser le SEO des images

    Points clés :
    • Composant next/image
    • Texte alt significatif
    • Dimensions (1200x630 pour OG)
    • Formats WebP/AVIF
    • Données structurées ImageObject
  6. 6

    Step 6: Valider et tester

    Outils :
    • Google Rich Results Test : valider les données structurées
    • Facebook Sharing Debugger : tester les balises OG
    • Twitter Card Validator : tester Twitter Cards
    • Google Search Console : soumettre le sitemap et surveiller

    Checklist :
    • Title et description uniques par page
    • Dimensions OG correctes (1200x630)
    • Format des données structurées valide
    • Sitemap soumis à Search Console

FAQ

Quel lien entre SSR et SEO ?
Le SSR (rendu côté serveur) génère le HTML côté serveur, mais le SEO exige aussi meta tags, données structurées, Open Graph, etc. Le SSR seul ne suffit pas : configurez activement la Metadata API pour que les moteurs comprennent et indexent correctement.
Metadata API vs composant Head ?
La Metadata API est la méthode recommandée par Next.js 15 : typée, metadata statique et dynamique, gestion automatique des doublons. Head est l'approche React, manuelle et sujette aux erreurs. Les nouveaux projets devraient utiliser la Metadata API.
Comment configurer les metadata pour les routes dynamiques ?
Utilisez generateMetadata avec params ; fonction async possible pour récupérer les données, puis retourner l'objet metadata.

Exemple :
export async function generateMetadata({ params }) {
const data = await getData(params.id)
return { title: data.title }
}
Quelle taille pour les images Open Graph ?
1200x630 pixels — format universel pour Facebook, Twitter, LinkedIn. Fichier sous 1 Mo, JPEG ou PNG recommandés.
Les données structurées sont-elles obligatoires ?
Non, mais fortement recommandées. Elles permettent des résultats enrichis (notes, prix, FAQ) et améliorent le CTR. Google, Bing et autres supportent JSON-LD.
Faut-il créer sitemap et robots.txt à la main ?
Non. Next.js accepte sitemap.ts et robots.ts pour générer automatiquement sitemap.xml et robots.txt. sitemap.ts peut lister dynamiquement toutes les URL ; robots.ts définit les règles de crawl.
Combien de temps avant de voir les effets du SEO ?
En général 1 à 3 mois — les moteurs ont besoin de temps pour crawler et indexer.

Conseils :
1) Soumettre le sitemap à Google Search Console
2) Valider avec Google Rich Results Test
3) Surveiller Search Console
4) Maintenir le contenu à jour

12 min de lecture · Publié le: 19 déc. 2025 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog