Aller au contenu
17 juin 2025·13 min de lecture

SEO Next.js : Metadata API, sitemaps et données structurées

Implémentez le SEO correctement dans Next.js 14+ — generateMetadata, opengraph-image, configuration next-sitemap, données structurées JSON-LD et URLs canoniques.

Retour

Next.js App Router offre des outils SEO intégrés puissants. Mais la plupart des tutoriels ne font qu'effleurer la surface. Voici une implémentation complète couvrant les métadonnées, les images Open Graph, les données structurées et les sitemaps.


Prérequis

  • Next.js 14+ avec App Router
  • Notions de base sur les métadonnées et le SEO

generateMetadata — SEO dynamique par page

La fonction generateMetadata remplace l'ancien composant Head. Elle s'exécute côté serveur et prend en charge la récupération de données asynchrone :

// app/[locale]/blog/[slug]/page.tsx
import type { Metadata } from "next";
 
type Props = {
  params: Promise<{ locale: string; slug: string }>;
};
 
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale, slug } = await params;
  const post = await getPostBySlug(slug, locale);
 
  if (!post) return { title: "Article introuvable" };
 
  const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://votresite.com";
  const canonical = `${siteUrl}/${locale}/blog/${slug}`;
 
  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical,
      languages: {
        en: `${siteUrl}/en/blog/${slug}`,
        fr: `${siteUrl}/fr/blog/${slug}`,
        es: `${siteUrl}/es/blog/${slug}`,
      },
    },
    openGraph: {
      type: "article",
      title: post.title,
      description: post.excerpt,
      url: canonical,
      publishedTime: post.date,
      authors: ["Aïcha Imène DAHOUMANE"],
      tags: post.tags,
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
    },
  };
}

opengraph-image.tsx — Images OG générées de façon programmatique

Plutôt que des images statiques, générez des images Open Graph dynamiques avec l'API ImageResponse :

// app/[locale]/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";
 
type Props = { params: Promise<{ slug: string; locale: string }> };
 
export default async function Image({ params }: Props) {
  const { slug, locale } = await params;
  const post = await getPostBySlug(slug, locale);
 
  return new ImageResponse(
    (
      <div
        style={{
          background: "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)",
          width: "100%",
          height: "100%",
          display: "flex",
          flexDirection: "column",
          padding: "60px",
          justifyContent: "space-between",
        }}
      >
        <div style={{ color: "#22d3ee", fontSize: 18, fontWeight: 600 }}>
          votresite.com
        </div>
        <div>
          <div style={{ color: "#f1f5f9", fontSize: 52, fontWeight: 700, lineHeight: 1.2 }}>
            {post?.title ?? "Article"}
          </div>
          <div style={{ color: "#94a3b8", fontSize: 24, marginTop: 20 }}>
            {post?.excerpt}
          </div>
        </div>
      </div>
    ),
    { ...size }
  );
}

Données structurées JSON-LD

Ajoutez des données structurées pour obtenir des rich results dans Google Search :

// components/JsonLd.tsx
export function PersonSchema() {
  const schema = {
    "@context": "https://schema.org",
    "@type": "Person",
    name: "Aïcha Imène DAHOUMANE",
    url: "https://votresite.com",
    jobTitle: "Développeuse Salesforce & Consultante IT",
    sameAs: [
      "https://github.com/Aiyeesha",
      "https://www.linkedin.com/in/aïcha-imène-dahoumane",
    ],
  };
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}
 
export function ArticleSchema({ post, url }: { post: Post; url: string }) {
  const schema = {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    headline: post.title,
    description: post.excerpt,
    datePublished: post.date,
    author: {
      "@type": "Person",
      name: "Aïcha Imène DAHOUMANE",
    },
    url,
  };
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Utilisez-les respectivement dans votre layout et votre page :

// app/layout.tsx
import { PersonSchema } from "@/components/JsonLd";
// ...
<body>
  <PersonSchema />
  {children}
</body>

Configuration next-sitemap

npm install next-sitemap
// next-sitemap.config.js
/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL || "https://votresite.com",
  generateRobotsTxt: true,
  sitemapSize: 5000,
  changefreq: "weekly",
  priority: 0.7,
  exclude: ["/admin/*", "/api/*"],
  robotsTxtOptions: {
    policies: [
      { userAgent: "*", allow: "/" },
      { userAgent: "*", disallow: ["/admin/", "/api/"] },
    ],
  },
  // Support multilingue
  alternateRefs: [
    { href: "https://votresite.com/en", hreflang: "en" },
    { href: "https://votresite.com/fr", hreflang: "fr" },
    { href: "https://votresite.com/es", hreflang: "es" },
  ],
};
// package.json
{
  "scripts": {
    "postbuild": "next-sitemap"
  }
}

URLs canoniques et hreflang

Définissez toujours des URLs canoniques pour éviter les pénalités de contenu dupliqué :

// Dans generateMetadata
alternates: {
  canonical: `https://votresite.com/${locale}/blog/${slug}`,
  languages: {
    "en": `https://votresite.com/en/blog/${slug}`,
    "fr": `https://votresite.com/fr/blog/${slug}`,
    "es": `https://votresite.com/es/blog/${slug}`,
    "x-default": `https://votresite.com/en/blog/${slug}`,
  },
},

Pièges courants

  • x-default manquant : incluez toujours un hreflang x-default pointant vers votre langue principale
  • Images OG dupliquées : si vous avez un opengraph-image.tsx, ne définissez pas aussi openGraph.images dans les métadonnées — les deux entreraient en conflit
  • robots.txt qui bloque CSS/JS : certains robots.txt générés bloquent accidentellement des assets — vérifiez sur votresite.com/robots.txt
  • Routes dynamiques absentes du sitemap : assurez-vous que generateStaticParams est implémenté pour que next-sitemap explore bien tous les slugs

Ressources

Aïcha Imène DAHOUMANE
À propos de l'auteure
Aïcha Imène DAHOUMANE
Administratrice & Développeuse Salesforce · Consultante

Développeuse Salesforce avec un background en infrastructures IT — je construis des solutions fiables et maintenables avec Apex, Flows, LWC et CI/CD.

PartagerPartager sur LinkedIn