Une page magnifique mais invisible sur Google, ça ne sert à rien. Le référencement commence par des balises <head> propres : un title unique, une description pertinente, des balises Open Graph pour les partages sur les réseaux sociaux. Dans l'App Router de Next.js 16, tout cela se gère avec l'API Metadata, et notamment la fonction generateMetadata pour les pages dynamiques.
Dans cet article, je détaille comment exploiter cette API à fond : métadonnées statiques, dynamiques, héritage entre layouts, Open Graph, Twitter Cards et données structurées JSON-LD. L'objectif : que chacune de vos pages soit parfaitement comprise par Google et présentée correctement quand quelqu'un partage votre lien.
Metadata statique vs dynamique : deux outils complémentaires
Next.js 16 propose deux façons d'exporter des métadonnées depuis un fichier page.tsx ou layout.tsx. La première, l'objet metadata, convient quand les valeurs sont connues à la compilation. C'est le cas d'une page « À propos » ou d'une page d'accueil dont le titre ne change pas.
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "À propos — Sira development",
description:
"Découvrez Alexis Mouchon, développeur web fullstack freelance spécialisé Next.js et NestJS.",
};
export default function AboutPage() {
return <main>...</main>;
}
La seconde, la fonction generateMetadata, s'utilise dès que les valeurs dépendent de données récupérées au runtime : un article de blog, une fiche produit, un profil utilisateur. Next.js appelle cette fonction avant de rendre la page, récupère les données et injecte les balises correspondantes dans le <head>.
Ces deux approches sont mutuellement exclusives dans un même fichier : soit vous exportez l'objet, soit la fonction, jamais les deux.
generateMetadata : la base pour les pages dynamiques
Prenons l'exemple typique d'un article de blog accessible via /blog/[slug]. Le titre, la description et l'image de partage dépendent de l'article. Voici comment générer ces métadonnées :
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getArticleBySlug } from "@/lib/blog";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({
params,
}: Props): Promise<Metadata> {
const { slug } = await params;
const article = await getArticleBySlug(slug);
if (!article) {
return { title: "Article introuvable" };
}
return {
title: article.title,
description: article.description,
keywords: article.tags,
};
}
export default async function ArticlePage({ params }: Props) {
const { slug } = await params;
const article = await getArticleBySlug(slug);
if (!article) notFound();
return <article>{/* ... */}</article>;
}
Deux points méritent l'attention. Dans Next.js 16, params est une Promise que l'on doit await : c'est l'une des évolutions majeures de l'App Router récent. Ensuite, vous remarquez qu'on appelle getArticleBySlug à la fois dans generateMetadata et dans le composant de page. Pas de panique côté performance : si vous utilisez fetch ou que vous enveloppez votre accès aux données dans cache() de React, l'appel n'est exécuté qu'une seule fois grâce à la déduplication des requêtes.
import { cache } from "react";
export const getArticleBySlug = cache(async (slug: string) => {
const res = await fetch(`https://api.exemple.fr/articles/${slug}`);
if (!res.ok) return null;
return res.json();
});
Avec cache(), le double appel dans generateMetadata et dans la page ne déclenche qu'une seule requête réseau. C'est exactement le genre de détail qui sépare un site rapide d'un site qui rame.
Open Graph et Twitter Cards : soignez vos partages
Le title et la description couvrent le référencement de base, mais ils ne suffisent pas pour les réseaux sociaux. Quand quelqu'un partage votre lien sur LinkedIn, X ou WhatsApp, ce sont les balises Open Graph qui déterminent l'aperçu : titre, description et surtout l'image. Une carte de partage soignée augmente nettement le taux de clic.
export async function generateMetadata({
params,
}: Props): Promise<Metadata> {
const { slug } = await params;
const article = await getArticleBySlug(slug);
if (!article) return { title: "Article introuvable" };
const url = `https://alexis-mouchon.fr/blog/${slug}`;
return {
title: article.title,
description: article.description,
alternates: {
canonical: url,
},
openGraph: {
title: article.title,
description: article.description,
url,
type: "article",
publishedTime: article.date,
authors: ["Alexis Mouchon"],
images: [
{
url: article.coverImage,
width: 1200,
height: 630,
alt: article.title,
},
],
},
twitter: {
card: "summary_large_image",
title: article.title,
description: article.description,
images: [article.coverImage],
},
};
}
Notez la propriété alternates.canonical : elle déclare l'URL canonique de la page, ce qui évite les problèmes de contenu dupliqué si la même page est accessible via plusieurs URL (avec ou sans paramètres de tracking, par exemple). C'est un point souvent oublié qui pénalise le référencement sur le long terme. Pour l'image Open Graph, le ratio 1200×630 est le standard ; en dessous, l'aperçu risque de s'afficher en petit format peu engageant.
Générer des images Open Graph dynamiques
Plutôt que de produire manuellement une image de partage pour chaque article, Next.js 16 permet d'en générer à la volée avec ImageResponse. Vous créez un fichier opengraph-image.tsx dans le dossier de la route, et Next.js construit l'image à partir de JSX.
import { ImageResponse } from "next/og";
import { getArticleBySlug } from "@/lib/blog";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function OgImage({
params,
}: {
params: { slug: string };
}) {
const article = await getArticleBySlug(params.slug);
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: 80,
background: "#0f172a",
color: "white",
fontSize: 64,
}}
>
<span style={{ fontSize: 28, color: "#38bdf8" }}>
alexis-mouchon.fr
</span>
<h1 style={{ marginTop: 24 }}>{article?.title}</h1>
</div>
),
{ ...size },
);
}
Chaque article reçoit ainsi une image de partage personnalisée, avec son titre et votre identité visuelle, sans aucune intervention manuelle. C'est un gros gain de temps quand on publie régulièrement.
L'héritage des métadonnées entre layouts et pages
L'App Router applique un système d'héritage : les métadonnées définies dans un layout.tsx parent fusionnent avec celles des pages enfants. Cela vous permet de définir des valeurs par défaut au niveau racine, puis de les surcharger page par page.
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL("https://alexis-mouchon.fr"),
title: {
default: "Alexis Mouchon — Développeur web fullstack freelance",
template: "%s | Sira development",
},
description: "Création de sites web sur mesure avec Next.js et NestJS.",
};
La propriété title.template est particulièrement pratique : le %s est remplacé par le titre de chaque page enfant. Si une page exporte title: "Mon article", le titre final affiché sera Mon article | Sira development. Vous gardez une cohérence de marque sur tout le site sans répéter le suffixe partout.
Quant à metadataBase, il définit l'URL de base utilisée pour résoudre les chemins relatifs des images Open Graph. Sans lui, une image déclarée en /og.png ne se transforme pas en URL absolue, et certains crawlers de réseaux sociaux refusent les chemins relatifs. C'est une source de bug fréquente : les partages affichent un aperçu sans image alors que tout semble correct dans le code.
Ajouter des données structurées JSON-LD
Au-delà des balises classiques, Google exploite les données structurées (schema.org) pour enrichir vos résultats de recherche : étoiles d'avis, dates, fil d'Ariane, auteur. L'API Metadata ne couvre pas directement le JSON-LD, mais on l'ajoute facilement avec une balise <script> dans le composant de page.
export default async function ArticlePage({ params }: Props) {
const { slug } = await params;
const article = await getArticleBySlug(slug);
if (!article) notFound();
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: article.title,
description: article.description,
datePublished: article.date,
author: {
"@type": "Person",
name: "Alexis Mouchon",
},
publisher: {
"@type": "Organization",
name: "Sira development",
},
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* contenu de l'article */}
</article>
);
}
L'usage de dangerouslySetInnerHTML peut faire tiquer, mais il est ici recommandé par la documentation officielle de Next.js pour injecter du JSON-LD : on sérialise un objet contrôlé, sans contenu utilisateur arbitraire, le risque est donc maîtrisé. Pensez tout de même à valider votre balisage avec l'outil de test des résultats enrichis de Google une fois en production.
Quelques bonnes pratiques pour finir
Gardez vos description entre 150 et 160 caractères : au-delà, Google les tronque dans les résultats. Veillez à ce que chaque page ait un title unique, sous peine de cannibalisation entre vos propres pages. Déclarez systématiquement une URL canonique sur les pages susceptibles d'avoir des doublons. Enfin, testez toujours vos cartes de partage avant de publier, avec les validateurs officiels de chaque plateforme : un aperçu cassé sur LinkedIn, c'est des clics perdus.
L'API Metadata de Next.js 16 est l'un de ces outils qui font énormément pour le référencement sans demander beaucoup de code. Bien configurée une fois au niveau du layout racine, puis affinée page par page avec generateMetadata, elle garantit que chaque contenu que vous publiez part avec toutes ses chances sur Google et les réseaux sociaux.
Si vous voulez aller plus loin sur le sujet, je vous renvoie à mes articles sur les performances Next.js et les Core Web Vitals et sur l'optimisation des images avec next/image, deux piliers complémentaires d'un bon référencement.
Vous avez un projet web et vous voulez un site rapide, bien référencé et taillé pour convertir ? N'hésitez pas à me contacter ou à m'écrire à contact@alexis-mouchon.fr pour en discuter.