Ce qu'il faut retenir
Next.js 16 avec l'App Router est le framework le mieux équipé pour le SEO technique en 2026 : Metadata API native, sitemap.ts et robots.ts automatisés, Partial Prerendering pour combiner vitesse CDN et fraîcheur des données, et intégration first-class de JSON-LD.
Les Core Web Vitals restent un facteur de ranking actif : LCP ≤ 2,5 s, INP ≤ 200 ms et CLS ≤ 0,1 sont les seuils à atteindre. Chaque mode de rendu Next.js (SSG, SSR, ISR, PPR) a des implications SEO distinctes qu'il faut maîtriser pour choisir la bonne stratégie par type de page.
Une checklist de 25 points techniques — de l'audit Lighthouse CI à l'implémentation hreflang — permet de garantir qu'aucun signal SEO n'est oublié à chaque déploiement. Automatisez ces contrôles dans votre pipeline CI/CD dès le sprint 0.

SEO technique Next.js 2026 : guide d'optimisation avancé
De la Metadata API au Partial Prerendering, en passant par les données structurées et l'internationalisation : tout ce qu'un projet Next.js 16 doit implémenter pour dominer les SERP en 2026.
Adapté à toute taille de structure
#Pourquoi Next.js est le meilleur framework pour le SEO technique en 2026
En 2026, le SEO technique ne se résume plus à quelques balises meta et un fichier robots.txt. Les moteurs de recherche — Google en tête, mais aussi les moteurs de réponse IA qui alimentent les AI Overviews — évaluent la crawlabilité, la vitesse de rendu, la richesse sémantique des données structurées, la cohérence des signaux d'internationalisation et la fraîcheur des contenus dynamiques.
Next.js 16 avec l'App Router répond à toutes ces exigences de manière native, sans plugin tiers ni configuration complexe. Voici pourquoi c'est un choix structurellement supérieur aux autres frameworks pour les projets SEO-first :
Rendu hybride natif — Contrairement à un SPA pur (React sans SSR, Vue sans Nuxt), Next.js génère du HTML côté serveur que Googlebot peut lire sans exécuter JavaScript. Le JavaScript est ensuite progressivement hydraté côté client pour l'interactivité.
Metadata API intégrée — Plus besoin de librairies comme react-helmet ou next-seo. La Metadata API de l'App Router permet de définir des métadonnées statiques ou dynamiques directement dans chaque page.tsx, avec gestion complète de l'Open Graph, des robots directives et des balises hreflang.
Optimisations automatiques — next/image pour le format AVIF/WebP, next/font pour l'auto-hébergement des polices sans requêtes tierces, next/script avec stratégies de chargement différé — chaque composant natif embarque des bonnes pratiques SEO et performance.
Écosystème Vercel — Vercel Analytics, Speed Insights et l'Edge Network mondial font de Next.js la solution la plus simple pour avoir des Core Web Vitals en zone « Bon » sans infrastructure dédiée.
Dans notre pratique sur plus de 40 refontes B2B avec l'App Router, Next.js permet de livrer des sites qui passent l'audit Lighthouse avec des scores ≥ 90 dès la mise en production initiale — à condition d'implémenter correctement les patterns décrits dans ce guide.
#Métadonnées et Open Graph : Metadata API Next.js 16, generateMetadata dynamique
#Métadonnées statiques : le cas simple
Pour les pages dont les métadonnées sont connues au build, exportez un objet metadata directement depuis le fichier page.tsx :
// app/services/refonte-b2b/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Refonte site B2B Next.js | Nehos Groupe — Toulouse',
description: 'Refonte complète de votre site B2B en Next.js 16 : architecture App Router, performance Core Web Vitals et SEO technique au niveau entreprise.',
keywords: ['refonte site b2b', 'next.js 16', 'app router', 'seo technique'],
authors: [{ name: 'Chokri Siala', url: 'https://nehos-groupe.com/team/chokri-siala' }],
openGraph: {
title: 'Refonte site B2B Next.js — Nehos Groupe',
description: 'Architecture App Router, Core Web Vitals et SEO technique : la refonte B2B pensée pour la performance et le ranking.',
url: 'https://nehos-groupe.com/services/refonte-b2b',
siteName: 'Nehos Groupe',
images: [{
url: 'https://cdn.nehos-groupe.com/og/services-refonte-b2b.webp',
width: 1200,
height: 630,
alt: 'Refonte site B2B Next.js — Nehos Groupe'
}],
locale: 'fr_FR',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Refonte site B2B Next.js — Nehos Groupe',
description: 'Architecture App Router, Core Web Vitals et SEO technique.',
images: ['https://cdn.nehos-groupe.com/og/services-refonte-b2b.webp'],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
alternates: {
canonical: 'https://nehos-groupe.com/services/refonte-b2b',
},
}
#generateMetadata dynamique : le cas avancé
Pour les pages dont les métadonnées dépendent du contenu (articles de blog, fiches produit, pages de catégorie), utilisez generateMetadata — une fonction asynchrone qui reçoit les params de route :
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
import { getArticleBySlug } from '@/lib/cms'
type Props = {
params: { slug: string }
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const article = await getArticleBySlug(params.slug)
// Héritage des métadonnées parentes (layout.tsx)
const previousImages = (await parent).openGraph?.images || []
return {
title: `${article.seoTitle} | Blog Nehos`,
description: article.seoDescription,
openGraph: {
title: article.ogTitle || article.seoTitle,
description: article.ogDescription || article.seoDescription,
images: [article.ogImage, ...previousImages],
publishedTime: article.publishedAt,
modifiedTime: article.updatedAt,
authors: [article.author.profileUrl],
type: 'article',
},
alternates: {
canonical: `https://nehos-groupe.com/blog/${params.slug}`,
languages: {
'fr-FR': `https://nehos-groupe.com/blog/${params.slug}`,
'x-default': `https://nehos-groupe.com/blog/${params.slug}`,
},
},
}
}
Point critique : generateMetadata s'exécute côté serveur au moment du rendu — elle ne gonfle pas le bundle JavaScript client. Les appels à l'API ou au CMS dans cette fonction sont automatiquement mémoïsés par Next.js via le cache de requêtes, ce qui évite les doublons avec le fetch du composant page.
#Métadonnées héritées : le layout.tsx comme base
Le fichier app/layout.tsx peut exporter un objet metadata qui définit les valeurs par défaut pour toutes les pages de l'application. Les pages enfants héritent et peuvent surcharger chaque propriété :
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://nehos-groupe.com'),
title: {
default: 'Nehos Groupe — ESN B2B Next.js & IA à Toulouse',
template: '%s | Nehos Groupe',
},
description: 'Nehos Groupe : ESN B2B premium à Toulouse — refontes Next.js, agents IA et SEO technique pour ETI et grands comptes.',
openGraph: {
siteName: 'Nehos Groupe',
locale: 'fr_FR',
type: 'website',
},
robots: {
index: true,
follow: true,
googleBot: { index: true, follow: true, 'max-image-preview': 'large' },
},
}
Le champ metadataBase est essentiel : il permet à Next.js de résoudre les URLs relatives dans les balises og:image, canonical et twitter:image en URLs absolues correctes.
#Sitemap et robots.txt automatisés : App Router sitemap.ts, robots.ts
#sitemap.ts : génération dynamique du plan de site
L'App Router permet de générer un sitemap XML dynamique avec un fichier app/sitemap.ts. Ce fichier s'exécute au build (SSG) ou à chaque requête (SSR) selon la configuration :
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { getAllArticles } from '@/lib/cms'
import { getAllServices } from '@/lib/content'
export const revalidate = 86400 // ISR : régénération toutes les 24h
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://nehos-groupe.com'
// Pages statiques
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 0.9,
},
{
url: `${baseUrl}/services`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
]
// Articles de blog dynamiques
const articles = await getAllArticles()
const articleUrls: MetadataRoute.Sitemap = articles.map((article) => ({
url: `${baseUrl}/blog/${article.slug}`,
lastModified: new Date(article.updatedAt),
changeFrequency: 'weekly',
priority: article.priority === 'P0' ? 0.8 : 0.6,
}))
// Pages services dynamiques
const services = await getAllServices()
const serviceUrls: MetadataRoute.Sitemap = services.map((service) => ({
url: `${baseUrl}/services/${service.slug}`,
lastModified: new Date(service.updatedAt),
changeFrequency: 'monthly',
priority: 0.85,
}))
return [...staticPages, ...articleUrls, ...serviceUrls]
}
Avec revalidate = 86400, le sitemap est régénéré toutes les 24 heures via ISR — chaque nouvel article publié apparaît dans le sitemap sans rebuild complet.
#robots.ts : configuration fine du crawl
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: [
'/api/',
'/admin/',
'/_next/',
'/preview/',
'/*?*', // bloque les paramètres de requête (facettes, tracking)
],
},
{
userAgent: 'GPTBot',
allow: '/blog/', // autorise l'indexation IA des articles
disallow: '/',
},
{
userAgent: 'Claude-Web',
allow: '/blog/',
disallow: '/',
},
],
sitemap: 'https://nehos-groupe.com/sitemap.xml',
host: 'https://nehos-groupe.com',
}
}
Note sur les crawlers IA : en 2026, définir des règles spécifiques pour GPTBot, Claude-Web et PerplexityBot est une décision stratégique. Autoriser le crawl des articles de blog augmente les chances d'être cité dans les réponses des moteurs de réponse IA. Bloquer le crawl des pages de service protège le contenu commercial.
#Sitemap multi-sections pour les grands sites
Pour les sites avec plus de 50 000 URLs, utilisez plusieurs fichiers sitemap référencés dans un sitemap index :
// app/sitemap/blog/sitemap.ts
export default async function blogSitemap() { /* ... */ }
// app/sitemap/services/sitemap.ts
export default async function servicesSitemap() { /* ... */ }
Next.js gère automatiquement le sitemap index et la pagination des sitemaps à partir de 50 000 entrées.
#Rendu hybride et crawlabilité : SSG, SSR, ISR, PPR — impact SEO de chaque mode
Le choix du mode de rendu est la décision architecturale la plus impactante sur le SEO technique d'un projet Next.js. Chaque mode a des implications différentes sur la crawlabilité, la fraîcheur du contenu et les Core Web Vitals.
#SSG (Static Site Generation) : idéal pour les pages stables
Le SSG génère le HTML au moment du build. Le résultat est servi depuis le CDN avec une latence infime (< 20 ms TTFB). C'est le mode optimal pour les pages dont le contenu change peu : pages de service, pages de landing, articles de blog finalisés.
// app/services/[slug]/page.tsx
export async function generateStaticParams() {
const services = await getAllServices()
return services.map((s) => ({ slug: s.slug }))
}
// Pas de fetch dynamique → rendu statique
export default async function ServicePage({ params }) {
const service = await getServiceBySlug(params.slug)
return <ServiceContent service={service} />
}
Impact SEO : TTFB optimal, pages servies depuis le CDN edge. Limitation : les nouveaux contenus ne sont pas disponibles avant le prochain build.
#SSR (Server-Side Rendering) : pour les pages personnalisées
Le SSR génère le HTML à chaque requête sur le serveur. Utilisez-le uniquement lorsque le contenu doit être personnalisé par utilisateur ou dépendre de données en temps réel.
// app/dashboard/[userId]/page.tsx
import { cookies } from 'next/headers'
// La présence de cookies() déclenche le rendu dynamique
export default async function DashboardPage({ params }) {
const session = cookies().get('session')?.value
const userData = await getUserData(params.userId, session)
return <Dashboard data={userData} />
}
Impact SEO : TTFB plus élevé (temps de calcul serveur). Les pages SSR authentifiées sont en général exclues du crawl (via robots.txt ou noindex). À éviter pour les pages publiques indexables.
#ISR (Incremental Static Regeneration) : le compromis parfait
L'ISR combine la vitesse du CDN (premier rendu statique) avec la fraîcheur des données (régénération périodique en arrière-plan). C'est le mode recommandé pour la majorité des pages de blog et de contenu.
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // Régénération toutes les heures
// Ou revalidation à la demande via webhook CMS
export async function revalidatePath(path: string) {
revalidatePath(path, 'page') // Invalide le cache uniquement pour ce chemin
}
export default async function ArticlePage({ params }) {
const article = await getArticleBySlug(params.slug)
return <ArticleContent article={article} />
}
Impact SEO : meilleur compromis vitesse/fraîcheur. Googlebot obtient du HTML statique servi depuis le CDN. La lastModified du sitemap reflète la dernière régénération.
#PPR (Partial Prerendering) : l'innovation Next.js 16
Le PPR est la fonctionnalité phare de Next.js 16 pour le SEO. Il permet de rendre une partie de la page en statique (shell) et de streamer les parties dynamiques depuis le serveur. Le shell statique est servi immédiatement depuis le CDN — l'élément LCP est disponible en < 50 ms. Les blocs dynamiques (recommendations, données personnalisées) arrivent en streaming.
// next.config.ts — activation du PPR
const config: NextConfig = {
experimental: {
ppr: true, // Activé par défaut en Next.js 16 stable
},
}
// app/services/[slug]/page.tsx
import { Suspense } from 'react'
import { unstable_noStore as noStore } from 'next/cache'
// Shell statique — servi depuis CDN
function ServiceHero({ service }) {
return (
<section>
<h1>{service.title}</h1>
<p>{service.description}</p>
</section>
)
}
// Composant dynamique — streamé
async function RelatedCaseStudies({ serviceSlug }) {
noStore() // Marque explicitement comme dynamique
const cases = await fetchRelatedCases(serviceSlug)
return <CaseStudiesList cases={cases} />
}
export default async function ServicePage({ params }) {
const service = await getServiceBySlug(params.slug)
return (
<main>
<ServiceHero service={service} />
<Suspense fallback={<CaseStudiesSkeleton />}>
<RelatedCaseStudies serviceSlug={params.slug} />
</Suspense>
</main>
)
}
Impact SEO : le HTML du shell statique (contenant le titre H1, la description et l'image hero candidate LCP) est disponible dans le premier chunk HTML. Googlebot peut indexer le contenu principal sans attendre le streaming. Le TTFB pour l'élément LCP est équivalent au SSG.
| Mode | TTFB | Fraîcheur | Crawlabilité | Cas d'usage SEO |
|---|---|---|---|---|
| SSG | < 20 ms | Au build | Excellente | Pages stables, landing |
| SSR | 100-500 ms | Temps réel | Bonne | Pages auth, personnalisées |
| ISR | < 20 ms | Périodique | Excellente | Blog, catalogue produit |
| PPR | < 50 ms | Shell statique + stream | Excellente | Pages mixtes, services |
#Core Web Vitals et App Router : LCP, INP, CLS — les pièges et les solutions
#LCP sur l'App Router : le piège du Suspense
Le principal piège LCP en App Router est d'envelopper l'image hero dans un Suspense. Googlebot et les utilisateurs ne voient pas l'image avant que le streaming ne complète — le LCP s'envole.
// ❌ À éviter : image hero dans un Suspense
export default function Page() {
return (
<Suspense fallback={<Spinner />}>
<HeroWithImage /> {/* L'image LCP est bloquée par le streaming */}
</Suspense>
)
}
// ✅ Correct : image hero dans le shell statique, le reste en Suspense
export default async function Page({ params }) {
const hero = await getHeroContent(params.slug) // Data fetching dans le shell
return (
<main>
<HeroSection hero={hero} />{/* Image LCP ici, hors Suspense */}
<Suspense fallback={<ContentSkeleton />}>
<DynamicContent slug={params.slug} />
</Suspense>
</main>
)
}
#INP et Server Actions : éviter le blocage du thread
Les Server Actions Next.js 16 simplifient les mutations de données, mais une Server Action lente peut dégrader l'INP car l'interface reste non-responsive en attendant la réponse :
'use client'
import { useTransition } from 'react'
import { submitLeadForm } from '@/actions/lead'
export function LeadForm() {
const [isPending, startTransition] = useTransition()
async function handleSubmit(formData: FormData) {
startTransition(async () => {
await submitLeadForm(formData) // Server Action — hors du thread principal
})
}
return (
<form action={handleSubmit}>
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Envoi...' : 'Demander un audit'}
</button>
</form>
)
}
#CLS et les composants Client : l'hydratation comme source de décalage
Les composants Client qui lisent localStorage ou sessionStorage pendant l'hydratation génèrent du CLS car le rendu serveur et le rendu client diffèrent. La solution est de différer le rendu au montage :
'use client'
import { useState, useEffect } from 'react'
export function ThemeAwareHeader() {
const [theme, setTheme] = useState<string | null>(null)
useEffect(() => {
// Exécuté uniquement côté client, après hydratation
setTheme(localStorage.getItem('theme') ?? 'light')
}, [])
// Pendant l'hydratation : rendu neutre sans accès localStorage
if (theme === null) return <HeaderSkeleton />
return <Header theme={theme} />
}
#Structured data / JSON-LD : implémentation dans Next.js App Router
Les données structurées JSON-LD permettent à Google d'extraire des informations sémantiques précises depuis vos pages. En 2026, elles alimentent également les AI Overviews et les réponses des moteurs de recherche IA. Une implémentation correcte augmente la probabilité d'obtenir des rich snippets (FAQ, breadcrumb, article, organisation) et d'être cité par les IA génératives.
#Implémentation via un composant JsonLd réutilisable
// components/JsonLd.tsx
type JsonLdProps = {
data: Record<string, unknown> | Record<string, unknown>[]
}
export function JsonLd({ data }: JsonLdProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
)
}
#Article avec FAQ et BreadcrumbList
// app/blog/[slug]/page.tsx
import { JsonLd } from '@/components/JsonLd'
export default async function ArticlePage({ params }) {
const article = await getArticleBySlug(params.slug)
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.title,
description: article.description,
datePublished: article.publishedAt,
dateModified: article.updatedAt,
author: {
'@type': 'Person',
name: article.author.name,
url: `https://nehos-groupe.com/team/${article.author.slug}`,
sameAs: [article.author.linkedin],
},
publisher: {
'@type': 'Organization',
name: 'Nehos Groupe',
logo: {
'@type': 'ImageObject',
url: 'https://cdn.nehos-groupe.com/logo.svg',
},
},
image: {
'@type': 'ImageObject',
url: article.heroImage,
width: 1200,
height: 630,
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://nehos-groupe.com/blog/${article.slug}`,
},
}
const faqSchema = article.faq?.questions?.length
? {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: article.faq.questions.map((q) => ({
'@type': 'Question',
name: q.question,
acceptedAnswer: {
'@type': 'Answer',
text: q.answer,
},
})),
}
: null
const breadcrumbSchema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: article.breadcrumb.map((crumb, i) => ({
'@type': 'ListItem',
position: i + 1,
name: crumb.name,
item: crumb.url,
})),
}
return (
<>
<JsonLd data={articleSchema} />
{faqSchema && <JsonLd data={faqSchema} />}
<JsonLd data={breadcrumbSchema} />
<ArticleContent article={article} />
</>
)
}
#Schema Organization dans le layout racine
L'entité Organisation doit être déclarée une seule fois, dans le layout racine, avec tous ses identifiants (sameAs) pour que Google puisse consolider le Knowledge Graph de votre marque :
// app/layout.tsx
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Nehos Groupe',
url: 'https://nehos-groupe.com',
logo: 'https://cdn.nehos-groupe.com/logo.svg',
description: 'ESN B2B premium à Toulouse — refontes Next.js, agents IA et SEO technique pour ETI et grands comptes.',
address: {
'@type': 'PostalAddress',
addressLocality: 'Toulouse',
addressRegion: 'Occitanie',
addressCountry: 'FR',
},
sameAs: [
'https://www.linkedin.com/company/nehos-groupe/',
'https://twitter.com/nehosgroupe',
'https://github.com/nehos-groupe',
],
contactPoint: {
'@type': 'ContactPoint',
contactType: 'sales',
availableLanguage: 'French',
url: 'https://calendly.com/raphael-poirier_/decouverte15min-nehos-groupe',
},
}
#Images et performance : next/image, formats WebP/AVIF, lazy loading optimal
#Le composant next/image : fonctionnement interne
next/image est bien plus qu'un simple wrapper HTML <img>. Voici ce qu'il fait automatiquement :
- Négociation de format : génère AVIF si le navigateur le supporte, WebP sinon, JPEG en fallback
- Srcset responsive : génère automatiquement des variantes pour les tailles d'écran courantes
- Lazy loading : ajoute
loading="lazy"etdecoding="async"par défaut - Optimisation serveur : transforme l'image à la demande et met en cache le résultat
- Dimension enforcement : oblige à déclarer
widthetheightpour prévenir le CLS
// Utilisation correcte selon le contexte
import Image from 'next/image'
// ✅ Image hero (au-dessus de la ligne de flottaison) : priority
<Image
src="/images/hero-nextjs-seo.webp"
alt="Dashboard SEO technique Next.js — métriques Core Web Vitals"
width={1200}
height={630}
priority // fetchpriority="high" + preload dans <head>
quality={85} // AVIF 85 = qualité visuelle optimale / taille minimale
/>
// ✅ Image de contenu (sous la ligne de flottaison) : lazy loading par défaut
<Image
src="/images/schema-metadata-api.webp"
alt="Schéma de la Metadata API Next.js 16 — generateMetadata et héritage"
width={800}
height={450}
// Pas de priority → loading="lazy" automatique
/>
// ✅ Image fill pour les conteneurs CSS fluides
<div className="relative aspect-video rounded-lg overflow-hidden">
<Image
src={article.heroImage}
alt={article.heroImageAlt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
className="object-cover"
/>
</div>
#Configuration next/image dans next.config.ts
// next.config.ts
const config: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'], // AVIF en priorité
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 2592000, // 30 jours de cache
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.nehos-groupe.com',
pathname: '/**',
},
],
},
}
#Polices web avec next/font : zéro requête tierce
// app/fonts.ts
import { Inter, Playfair_Display } from 'next/font/google'
// Auto-hébergé par Next.js — aucune requête vers fonts.googleapis.com
export const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
preload: true,
})
export const playfair = Playfair_Display({
subsets: ['latin'],
display: 'optional', // Pas de FOUT sur les headings
variable: '--font-playfair',
weight: ['400', '700'],
})
next/font auto-héberge les fichiers de police et génère les balises <link rel="preload"> optimales. Résultat : zéro requête vers fonts.googleapis.com ou fonts.gstatic.com, ce qui élimine une source de latence externe et protège la vie privée des utilisateurs.
#Internationalisation SEO : hreflang, next-intl, routing locale
#Pourquoi hreflang est critique en 2026
hreflang indique à Google quelle version linguistique ou régionale servir à quel utilisateur. Sans hreflang, Google choisit lui-même la version à indexer — et peut se tromper, avec des conséquences directes sur le ranking par marché géographique.
#Implémentation hreflang avec la Metadata API
// app/[locale]/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const { locale, slug } = params
const article = await getArticleBySlug(slug, locale)
const locales = await getAvailableLocalesForSlug(slug)
const languageAlternates = locales.reduce((acc, loc) => {
acc[loc.hreflang] = `https://nehos-groupe.com/${loc.prefix}/blog/${loc.slug}`
return acc
}, {} as Record<string, string>)
// Toujours ajouter x-default
languageAlternates['x-default'] = `https://nehos-groupe.com/blog/${slug}`
return {
alternates: {
canonical: `https://nehos-groupe.com/${locale}/blog/${slug}`,
languages: languageAlternates,
},
}
}
Cela génère dans le HTML :
<link rel="alternate" hreflang="fr-FR" href="https://nehos-groupe.com/fr/blog/nextjs-seo-guide" />
<link rel="alternate" hreflang="x-default" href="https://nehos-groupe.com/blog/nextjs-seo-guide" />
#Structure de routing avec next-intl
// middleware.ts
import createMiddleware from 'next-intl/middleware'
export default createMiddleware({
locales: ['fr', 'en'],
defaultLocale: 'fr',
localePrefix: 'as-needed', // /fr/ uniquement pour les langues non-défaut
})
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
}
// Structure de fichiers recommandée
app/
├── [locale]/
│ ├── layout.tsx ← `params.locale` pour i18n
│ ├── page.tsx ← Page d'accueil localisée
│ └── blog/
│ └── [slug]/
│ └── page.tsx ← Article localisé
├── sitemap.ts ← Sitemap multi-langues
└── robots.ts
#Sitemap multi-langues
// app/sitemap.ts — version internationalisation
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const articles = await getAllArticles()
const locales = ['fr', 'en']
const baseUrl = 'https://nehos-groupe.com'
const urls = articles.flatMap((article) =>
locales.map((locale) => ({
url: locale === 'fr'
? `${baseUrl}/blog/${article.slug}`
: `${baseUrl}/${locale}/blog/${article.slug}`,
lastModified: new Date(article.updatedAt),
changeFrequency: 'weekly' as const,
priority: locale === 'fr' ? 0.8 : 0.6,
}))
)
return urls
}
#Audit SEO technique Next.js : outils, checklist 25 points
Un audit SEO technique Next.js rigoureux couvre cinq dimensions : structure technique, métadonnées, performance, données structurées et crawlabilité. Voici la checklist complète que nous appliquons sur chaque projet Nehos.
#Outils d'audit indispensables
| Outil | Usage principal | Fréquence |
|---|---|---|
| Lighthouse CI | Performance, accessibilité, best practices | À chaque PR |
| Google Search Console | Données terrain CrUX, erreurs de crawl, couverture sitemap | Hebdomadaire |
| Screaming Frog | Crawl technique complet, redirections, canonical, hreflang | Mensuel / avant launch |
| PageSpeed Insights | LCP, INP, CLS données terrain + lab | Ad hoc |
| Rich Results Test | Validation JSON-LD, eligibilité aux rich snippets | Après ajout schema |
| Chrome DevTools | Diagnostic LCP, Long Tasks, réseau | Ad hoc |
| Vercel Analytics | RUM Web Vitals en production | Continu |
#Checklist 25 points SEO technique Next.js
Métadonnées (5 points)
metadataBasedéfini dansapp/layout.tsxavec l'URL de productiontitle.templateconfiguré pour les pages enfantsgenerateMetadataimplémenté sur toutes les pages dynamiques- Balises
og:imageavec dimensions 1200×630 sur toutes les pages indexables robotsmeta tag avecmax-image-preview: largeetmax-snippet: -1
Crawlabilité (5 points)
6. robots.ts généré dynamiquement avec règles par user-agent
7. sitemap.ts avec revalidate pour ISR et priority cohérente
8. URLs canoniques absolues sur toutes les pages (pas de canonical relative)
9. Pas de contenu dupliqué entre /page et /page/ (trailing slash cohérent)
10. Pagination avec rel="next" / rel="prev" sur les listes paginées
Performance (5 points)
11. priority sur l'image hero candidate LCP de chaque page
12. next/font avec display: swap ou optional selon criticité
13. Pas de dynamic(() => import(...), { ssr: false }) sur les composants au-dessus de la ligne de flottaison
14. Budget de performance budget.json dans Lighthouse CI (TBT < 200 ms, LCP < 2500 ms)
15. Cache-Control long sur les assets statiques (/_next/static/*)
Données structurées (5 points)
16. Schema Organization dans le layout racine avec sameAs complet
17. Schema Article + BreadcrumbList sur toutes les pages blog
18. Schema FAQPage sur les pages avec section FAQ
19. Schema Service ou Product sur les pages services
20. Validation via Rich Results Test et Google Search Console
Internationalisation (5 points)
21. Balises hreflang avec toutes les variantes de locale + x-default
22. lang sur la balise <html> cohérent avec la locale de la page
23. Sitemap inclut toutes les variantes de locale pour chaque URL
24. Middleware next-intl avec localePrefix: 'as-needed' pour éviter les URLs en double
25. robots.txt sans blocage involontaire des chemins localisés (/en/, /de/, etc.)
#Automatisation dans le pipeline CI/CD
# .github/workflows/seo-audit.yml
name: SEO & Performance Audit
on:
pull_request:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci && npm run build
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
http://localhost:3000/
http://localhost:3000/blog
http://localhost:3000/services/refonte-b2b
budgetPath: ./budget.json
uploadArtifacts: true
- name: Validate JSON-LD
run: npx schema-dts-gen --validate dist/
- name: Check canonical URLs
run: npx broken-link-checker http://localhost:3000 --filter-level 3
Cette automatisation garantit que chaque PR est auditée sur les métriques SEO critiques avant merge. Les régressions sont détectées immédiatement, au coût d'un aller-retour CI — et non après mise en production, où le coût de correction est 5 à 10 fois plus élevé.