Nehos Groupe

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.

Dashboard SEO technique Next.js — App Router, Core Web Vitals et données structurées

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

Artisan
Startup
PME / TPE
ETI
Grand Groupe
C
Chokri Siala
··geo-aeo

#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 automatiquesnext/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.

ModeTTFBFraîcheurCrawlabilitéCas d'usage SEO
SSG< 20 msAu buildExcellentePages stables, landing
SSR100-500 msTemps réelBonnePages auth, personnalisées
ISR< 20 msPériodiqueExcellenteBlog, catalogue produit
PPR< 50 msShell statique + streamExcellentePages 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 :

  1. Négociation de format : génère AVIF si le navigateur le supporte, WebP sinon, JPEG en fallback
  2. Srcset responsive : génère automatiquement des variantes pour les tailles d'écran courantes
  3. Lazy loading : ajoute loading="lazy" et decoding="async" par défaut
  4. Optimisation serveur : transforme l'image à la demande et met en cache le résultat
  5. Dimension enforcement : oblige à déclarer width et height pour 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

OutilUsage principalFréquence
Lighthouse CIPerformance, accessibilité, best practicesÀ chaque PR
Google Search ConsoleDonnées terrain CrUX, erreurs de crawl, couverture sitemapHebdomadaire
Screaming FrogCrawl technique complet, redirections, canonical, hreflangMensuel / avant launch
PageSpeed InsightsLCP, INP, CLS données terrain + labAd hoc
Rich Results TestValidation JSON-LD, eligibilité aux rich snippetsAprès ajout schema
Chrome DevToolsDiagnostic LCP, Long Tasks, réseauAd hoc
Vercel AnalyticsRUM Web Vitals en productionContinu

#Checklist 25 points SEO technique Next.js

Métadonnées (5 points)

  1. metadataBase défini dans app/layout.tsx avec l'URL de production
  2. title.template configuré pour les pages enfants
  3. generateMetadata implémenté sur toutes les pages dynamiques
  4. Balises og:image avec dimensions 1200×630 sur toutes les pages indexables
  5. robots meta tag avec max-image-preview: large et max-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é.

Questions & Réponses

Questions fréquentes sur le SEO technique Next.js

Oui, Next.js avec l'App Router génère du HTML côté serveur que Googlebot peut lire sans exécuter JavaScript. Contrairement aux SPA purs (Create React App, Vite sans SSR), l'App Router rend le contenu des pages directement dans le HTML initial. Cependant, plusieurs configurations sont nécessaires pour un SEO optimal : `metadataBase` dans le layout racine pour les URLs absolues, un `sitemap.ts` dynamique, et l'absence de `noindex` non intentionnel sur les pages à indexer. Sans ces éléments, le site est techniquement crawlable mais sous-optimisé.
L'objet `metadata` statique exporté depuis un fichier `page.tsx` est évalué au build et ne peut pas dépendre de données dynamiques. Il est idéal pour les pages dont le titre et la description sont fixés : pages de service, pages de catégorie, pages légales. La fonction `generateMetadata` est asynchrone et reçoit les paramètres de route — elle peut effectuer des appels API ou CMS pour personnaliser les métadonnées par page. Elle est obligatoire pour les pages dynamiques comme les articles de blog, les fiches produit ou les pages de cas client. Les deux s'héritent du layout parent, permettant un système de valeurs par défaut centralisé.
Oui, de manière significative sur deux dimensions. Premièrement, le TTFB : avec le PPR, le shell statique de la page (contenant le titre H1, l'image hero et le contenu principal) est servi depuis le CDN en moins de 50 ms, contre 100 à 500 ms pour une page SSR classique calculée sur serveur. Googlebot mesure le TTFB et un TTFB élevé peut retarder le crawl des pages lentes. Deuxièmement, le LCP : l'élément LCP fait partie du shell statique et est disponible quasi-immédiatement, ce qui améliore les scores Core Web Vitals. Les parties dynamiques (recommendations, données personnalisées) arrivent en streaming sans impacter le LCP ni le crawl du contenu principal.
La méthode recommandée en Next.js App Router est d'utiliser la propriété `alternates.languages` dans la fonction `generateMetadata`. Pour chaque page, retournez un objet `languages` mappant les codes hreflang (fr-FR, en-GB, etc.) aux URLs absolues correspondantes, plus `x-default` pointant vers la version de référence. Côté middleware, next-intl gère le routing avec `localePrefix: 'as-needed'` pour éviter les URLs en double (la locale par défaut n'a pas de préfixe). Le sitemap doit inclure toutes les variantes de locale pour chaque URL. Les erreurs les plus courantes : hreflang relatif au lieu d'absolu, `x-default` manquant, et incohérence entre les balises hreflang d'une page vers l'autre (chaque page doit pointer vers toutes les variantes, y compris elle-même).
Oui, Screaming Frog reste indispensable même sur un projet Next.js bien configuré, pour trois raisons. Premièrement, il détecte les problèmes invisibles en développement : chaînes de redirections, balises canonical incorrectes après déploiement, hreflang incohérents entre pages, pages orphelines non liées. Deuxièmement, il crawle le site comme Googlebot le ferait réellement — avec JavaScript rendering optionnel — et révèle ce que le robot voit vraiment. Troisièmement, il permet d'auditer des éléments que Lighthouse ne couvre pas : structure des ancres, profondeur de crawl, duplication de title/description entre pages. Nous l'utilisons systématiquement avant chaque mise en production et après chaque migration de contenu importante.
Oui, de plus en plus. Les AI Overviews de Google utilisent les données structurées comme signaux de confiance pour identifier les entités, les faits vérifiables et les réponses à des questions précises. Un schema FAQPage bien implémenté augmente la probabilité que les questions-réponses soient reprises dans les AI Overviews. Le schema Article avec `datePublished`, `author` et `publisher` permet à Google d'évaluer la fraîcheur et la crédibilité du contenu. Le schema Organization dans le layout racine aide à consolider l'entité de marque dans le Knowledge Graph — ce qui améliore la présence dans les réponses IA qui mentionnent votre entreprise. En 2026, les données structurées sont aussi importantes pour le GEO (Generative Engine Optimization) que pour le SEO classique.
La solution est de lire `process.env.NEXT_PUBLIC_BASE_URL` pour construire les URLs du sitemap, et de s'assurer que cette variable d'environnement est différente en staging et en production. En production : `NEXT_PUBLIC_BASE_URL=https://nehos-groupe.com`. En staging : `NEXT_PUBLIC_BASE_URL=https://staging.nehos-groupe.com`. Dans le code du sitemap, utilisez cette variable plutôt qu'une URL hardcodée. Pour aller plus loin, ajoutez un `robots.ts` sur l'environnement de staging qui retourne `Disallow: /` pour tous les user-agents, ce qui empêche toute indexation accidentelle des contenus de préproduction par Googlebot.
Réserver un audit