Un projet Next.js se crée en deux minutes. Le configurer pour que Google le comprenne correctement, c'est une autre histoire. La Metadata API de Next.js, les fichiers de convention (opengraph-image, sitemap, robots), et le balisage structuré JSON-LD forment un ensemble cohérent, mais il faut savoir dans quel ordre les poser et pourquoi.
On va construire tout ça sur un exemple concret : la page d'accueil de Kreio, l'agence web. Chaque brique SEO sera illustrée avec le code réel qu'on utiliserait pour ce projet.
Initialiser le projet avec les bons choix dès le départ
pnpm create next-app kreio --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd kreio
Les options qui comptent pour le SEO :
--typescript: la Metadata API est typée, TypeScript attrape les erreurs de configuration avant le build.--app: l'App Router est le seul à supporter la Metadata API native de Next.js.--src-dir: sépare le code applicatif de la configuration à la racine.
La structure de départ pour kreio.fr :
src/
app/
layout.tsx ← metadata globale (title template, metadataBase)
page.tsx ← page d'accueil avec sa metadata
opengraph-image.tsx ← OG image générée dynamiquement
sitemap.ts ← sitemap généré côté serveur
robots.ts ← robots.txt
Le layout racine : poser les fondations globales
Le fichier app/layout.tsx est le point de départ de toute la configuration SEO. C'est ici qu'on définit le metadataBase, le title template, et les valeurs par défaut qui s'appliquent à toutes les pages.
// src/app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://kreio.fr"),
title: {
default: "Kreio — Agence web sur-mesure",
template: "%s | Kreio",
},
description:
"Kreio conçoit et développe des sites et applications web sur-mesure pour les entreprises françaises et européennes.",
keywords: [
"agence web",
"développement web sur-mesure",
"Next.js",
"TypeScript",
"site vitrine",
"application web",
],
authors: [{ name: "Oliwer Skweres", url: "https://kreio.fr" }],
creator: "Kreio",
publisher: "Kreio",
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
},
},
alternates: {
canonical: "https://kreio.fr",
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fr">
<body>{children}</body>
</html>
);
}
Trois points importants ici.
metadataBase est indispensable. Sans lui, Next.js ne peut pas résoudre les URLs relatives dans openGraph.images et affiche un avertissement à chaque build. Il faut le définir une seule fois dans le layout racine.
title.template permet à chaque page de définir uniquement son propre titre. Si app/services/page.tsx exporte title: "Services", le rendu final sera Services | Kreio sans duplication de code.
lang="fr" sur la balise html est un signal fort pour les moteurs de recherche sur la langue du contenu. Ne pas l'oublier.
La metadata de la page d'accueil
La page d'accueil a ses propres balises qui précisent le contexte de la route /.
// src/app/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Agence web sur-mesure — Next.js & TypeScript",
description:
"Kreio développe des sites vitrines, applications web et plateformes SaaS pour les PME françaises. Stack moderne : Next.js, TypeScript, Tailwind CSS.",
alternates: {
canonical: "https://kreio.fr",
},
openGraph: {
title: "Kreio — Agence web sur-mesure",
description:
"Sites vitrines, applications web et SaaS pour les PME françaises. Next.js, TypeScript, Tailwind CSS.",
url: "https://kreio.fr",
siteName: "Kreio",
locale: "fr_FR",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Kreio — Agence web sur-mesure",
description: "Sites vitrines et applications web sur-mesure pour les PME françaises.",
creator: "@kreio_fr",
},
};
export default function HomePage() {
return (
<main>
<h1>Agence web sur-mesure en France</h1>
{/* contenu de la page */}
</main>
);
}
Le canonical sur la page d'accueil est important. Il indique à Google qu'il n'existe qu'une seule URL canonique pour cette page et évite les problèmes de contenu dupliqué si le site est accessible via www.kreio.fr et kreio.fr en même temps.
| Balise | Rôle SEO | Priorité |
|---|---|---|
title | Titre affiché dans les résultats Google | Critique |
description | Extrait dans les résultats (160 car. max) | Haute |
canonical | Évite le contenu dupliqué | Haute |
openGraph.title | Titre affiché lors d'un partage social | Moyenne |
twitter.card | Format de la carte Twitter/X | Moyenne |
keywords | Signal de pertinence thématique | Faible |
L'OG image générée dynamiquement
Une image Open Graph statique (opengraph-image.png dans app/) suffit pour la homepage. Mais Next.js permet aussi de la générer côté serveur avec ImageResponse, ce qui est utile pour personnaliser l'image selon le contenu de la page.
// src/app/opengraph-image.tsx
import { ImageResponse } from "next/og";
export const alt = "Kreio — Agence web sur-mesure";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default function OgImage() {
return new ImageResponse(
(
<div
style={{
background: "#0a0a0a",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "flex-end",
padding: "60px 72px",
fontFamily: "sans-serif",
}}
>
<p style={{ color: "#6b7280", fontSize: 20, margin: "0 0 12px" }}>
Agence web sur-mesure
</p>
<h1 style={{ color: "#ffffff", fontSize: 72, fontWeight: 700, margin: 0, lineHeight: 1.1 }}>
Kreio
</h1>
<p style={{ color: "#9ca3af", fontSize: 24, margin: "16px 0 0" }}>
Next.js · TypeScript · Tailwind CSS
</p>
</div>
),
size,
);
}
Next.js détecte automatiquement le fichier opengraph-image.tsx et génère les balises og:image correspondantes sans configuration supplémentaire. La même mécanique s'applique pour un fichier statique opengraph-image.png placé au même endroit.
Le sitemap et le fichier robots
Ces deux fichiers sont générés programmatiquement via des fichiers de convention à la racine du dossier app/.
// src/app/sitemap.ts
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://kreio.fr",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1,
},
{
url: "https://kreio.fr/services",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: "https://kreio.fr/articles",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.7,
},
{
url: "https://kreio.fr/contact",
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.5,
},
];
}
// src/app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/api/", "/_next/"],
},
sitemap: "https://kreio.fr/sitemap.xml",
};
}
Ces deux fichiers sont accessibles automatiquement sur /sitemap.xml et /robots.txt après le build. Pas de configuration supplémentaire, pas de plugin externe nécessaire.
Le balisage structuré JSON-LD
Le JSON-LD permet à Google d'afficher des rich snippets dans les résultats de recherche : breadcrumbs, notes, FAQ, ou pour une organisation, les informations de contact directement dans le knowledge panel.
Pour la page d'accueil de Kreio, on utilise le schema Organization :
// src/app/page.tsx (extrait — ajouter dans le composant)
export default function HomePage() {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
name: "Kreio",
url: "https://kreio.fr",
logo: "https://kreio.fr/logo.png",
description:
"Agence web sur-mesure spécialisée en Next.js, TypeScript et Tailwind CSS.",
address: {
"@type": "PostalAddress",
addressLocality: "Évreux",
addressRegion: "Normandie",
addressCountry: "FR",
},
contactPoint: {
"@type": "ContactPoint",
contactType: "customer service",
email: "hello@kreio.fr",
},
sameAs: [
"https://github.com/kreio",
"https://www.linkedin.com/company/kreio",
],
};
return (
<main>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<h1>Agence web sur-mesure en France</h1>
</main>
);
}
La structure sémantique HTML : ce que Google lit vraiment
Les balises meta sont une chose. La structure du HTML en est une autre. Google analyse les deux. Sur la homepage de Kreio, les règles à respecter :
export default function HomePage() {
return (
<main>
{/* Un seul h1 par page, contenant le mot-clé principal */}
<h1>Agence web sur-mesure en France</h1>
{/* Les h2 structurent le contenu comme des chapitres */}
<section aria-labelledby="services-title">
<h2 id="services-title">Nos services</h2>
<p>Développement de sites vitrines, applications web et SaaS.</p>
</section>
<section aria-labelledby="stack-title">
<h2 id="stack-title">Notre stack technique</h2>
<p>Next.js, TypeScript, Tailwind CSS, Prisma.</p>
</section>
</main>
);
}
- Un seul
h1par page, qui reprend le mot-clé principal de la page. - Les
h2doivent être descriptifs et contenir des variantes du mot-clé principal. aria-labelledbyrelie la section à son titre, ce qui améliore l'accessibilité et la compréhension sémantique par les crawlers.- Le contenu doit être présent dans le HTML initial, pas chargé via un
useEffectcôté client. Les Server Components de Next.js garantissent ça par défaut.
Checklist avant le premier déploiement
Avant de pousser le projet en production, voici les vérifications à faire dans l'ordre :
-
metadataBasedéfini dansapp/layout.tsx -
title.templateconfiguré avec le nom de la marque -
lang="fr"sur la balisehtml -
canonicaldéfini sur chaque page -
opengraph-image.tsxouopengraph-image.pngprésent dansapp/ -
sitemap.tsretourne toutes les URLs publiques -
robots.tsautorise le crawl et pointe vers le sitemap - JSON-LD validé via Rich Results Test
- Un seul
h1par page, contenu rendu côté serveur -
pnpm buildpasse sans erreur ni avertissement metadata
Ce qu'on en retient
Le SEO avec Next.js App Router n'est pas compliqué, mais il demande de la rigueur dès l'initialisation. Les erreurs classiques (pas de metadataBase, contenu rendu côté client, OG images manquantes, un seul sitemap vide) se corrigent facilement si on les prend en amont. Les reprendre après coup sur un projet en production coûte plus cher.
Le fil rouge Kreio montre que toutes ces briques s'articulent naturellement : le layout pose les fondations, chaque page précise son contexte, et les fichiers de convention (sitemap, robots, OG image) s'ajoutent au fur et à mesure sans configuration externe.