SEO Next.js : Le Guide Complet 2026 pour Ranker #1 Google

Brandon Sueur18 min

Next.js 15 + App Router = SEO game-changer. Mais mal configuré = 0 trafic Google.

Ce guide couvre toutes les techniques SEO Next.js 2026 : Metadata API, sitemaps dynamiques, robots.txt, Open Graph, Schema.org, Core Web Vitals, et optimisations avancées.

Résultat : +340% trafic organique en 6 mois (cas réel client Hulli Studio)

TL;DR : Checklist SEO Next.js

Optimisation Impact SEO Difficulté Temps
Metadata API 🟢 Critique 🟢 Facile 30min
Sitemap.xml dynamique 🟢 Critique 🟡 Moyen 1h
Robots.txt 🟡 Important 🟢 Facile 10min
Open Graph images 🟡 Important 🟡 Moyen 2h
Schema.org (JSON-LD) 🟡 Important 🟡 Moyen 1h
Core Web Vitals 🟢 Critique 🔴 Difficile 4-8h
Internal linking 🟡 Important 🟢 Facile 1h
Canonical URLs 🟢 Critique 🟢 Facile 15min
Alt text images 🟡 Important 🟢 Facile 30min

Total setup SEO complet : 10-15 heures

Metadata API Next.js 15 (Fondation SEO)

Metadata Statique (Pages Fixes)

// app/about/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'À Propos - Hulli Studio | Agence Next.js Paris',
  description: 'Hulli Studio, agence spécialisée Next.js et React à Paris. 50+ projets SaaS, e-commerce et dashboards livrés depuis 2023.',
  keywords: ['agence nextjs paris', 'développement react', 'saas development'],
  authors: [{ name: 'Brandon Sueur', url: 'https://hulli.studio' }],
  creator: 'Hulli Studio',
  publisher: 'Hulli Studio',
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
  openGraph: {
    type: 'website',
    locale: 'fr_FR',
    url: 'https://hulli.studio/about',
    title: 'À Propos - Hulli Studio',
    description: 'Agence Next.js spécialisée React et TypeScript à Paris.',
    siteName: 'Hulli Studio',
    images: [
      {
        url: 'https://hulli.studio/og/about.jpg',
        width: 1200,
        height: 630,
        alt: 'Hulli Studio - Agence Next.js Paris',
      },
    ],
  },
  twitter: {
    card: 'summary_large_image',
    site: '@hullystudio',
    creator: '@brandonsueur',
    title: 'À Propos - Hulli Studio',
    description: 'Agence Next.js spécialisée React et TypeScript à Paris.',
    images: ['https://hulli.studio/og/about.jpg'],
  },
  alternates: {
    canonical: 'https://hulli.studio/about',
    languages: {
      'fr-FR': 'https://hulli.studio/fr/about',
      'en-US': 'https://hulli.studio/en/about',
    },
  },
  verification: {
    google: 'google-site-verification-code',
    yandex: 'yandex-verification',
    bing: 'bing-verification',
  },
}

export default function AboutPage() {
  return <div>About content</div>
}

Metadata Dynamique (Pages Générées)

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import db from '@/lib/db'

interface Props {
  params: { slug: string }
}

// ✅ Générer metadata pour chaque article
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await db.query.posts.findFirst({
    where: (posts, { eq }) => eq(posts.slug, params.slug),
  })

  if (!post) {
    return {
      title: 'Article non trouvé',
    }
  }

  const publishedTime = post.publishedAt?.toISOString()
  const modifiedTime = post.updatedAt?.toISOString()

  return {
    title: `${post.title} | Hulli Studio Blog`,
    description: post.excerpt,
    keywords: post.keywords,
    authors: [{ name: post.author }],
    openGraph: {
      type: 'article',
      locale: 'fr_FR',
      url: `https://hulli.studio/blog/${post.slug}`,
      title: post.title,
      description: post.excerpt,
      publishedTime,
      modifiedTime,
      authors: [post.author],
      images: [
        {
          url: post.ogImage || post.featuredImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.ogImage || post.featuredImage],
    },
    alternates: {
      canonical: `https://hulli.studio/blog/${post.slug}`,
    },
  }
}

export default async function BlogPostPage({ params }: Props) {
  const post = await db.query.posts.findFirst({
    where: (posts, { eq }) => eq(posts.slug, params.slug),
  })

  if (!post) notFound()

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Metadata partagé (Layout)

// app/layout.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  metadataBase: new URL('https://hulli.studio'),
  title: {
    template: '%s | Hulli Studio',
    default: 'Hulli Studio - Agence Next.js & React Paris',
  },
  description: 'Agence spécialisée Next.js, React et TypeScript à Paris. Développement SaaS, e-commerce et dashboards sur-mesure.',
  applicationName: 'Hulli Studio',
  referrer: 'origin-when-cross-origin',
  keywords: ['nextjs', 'react', 'typescript', 'agence paris', 'saas'],
  colorScheme: 'light dark',
  themeColor: [
    { media: '(prefers-color-scheme: light)', color: '#ffffff' },
    { media: '(prefers-color-scheme: dark)', color: '#000000' },
  ],
  viewport: {
    width: 'device-width',
    initialScale: 1,
    maximumScale: 5,
  },
  manifest: '/site.webmanifest',
  icons: {
    icon: [
      { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
      { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
    ],
    apple: [
      { url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
    ],
    other: [
      {
        rel: 'mask-icon',
        url: '/safari-pinned-tab.svg',
        color: '#000000',
      },
    ],
  },
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr">
      <body>{children}</body>
    </html>
  )
}

Sitemap.xml Dynamique

// app/sitemap.ts
import { MetadataRoute } from 'next'
import db from '@/lib/db'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://hulli.studio'

  // Pages statiques
  const routes = ['', '/about', '/services', '/contact'].map((route) => ({
    url: `${baseUrl}${route}`,
    lastModified: new Date(),
    changeFrequency: 'monthly' as const,
    priority: route === '' ? 1 : 0.8,
  }))

  // Articles blog dynamiques
  const posts = await db.query.posts.findMany({
    where: (posts, { eq }) => eq(posts.status, 'published'),
    columns: {
      slug: true,
      updatedAt: true,
    },
  })

  const postRoutes = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }))

  // Pages projets
  const projects = await db.query.portfolioItems.findMany({
    columns: {
      slug: true,
      updatedAt: true,
    },
  })

  const projectRoutes = projects.map((project) => ({
    url: `${baseUrl}/projects/${project.slug}`,
    lastModified: project.updatedAt,
    changeFrequency: 'monthly' as const,
    priority: 0.6,
  }))

  return [...routes, ...postRoutes, ...projectRoutes]
}

Résultat : https://hulli.studio/sitemap.xml auto-généré

Sitemap Images

// app/sitemap.ts (extended)
import { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await db.query.posts.findMany()

  return posts.map((post) => ({
    url: `https://hulli.studio/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly',
    priority: 0.7,
    images: [post.featuredImage], // ✅ Images SEO
  }))
}

Robots.txt

// app/robots.ts
import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  const baseUrl = 'https://hulli.studio'

  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/', '/dashboard/', '/_next/'],
      },
      {
        userAgent: 'Googlebot',
        allow: '/',
        disallow: ['/api/', '/admin/'],
      },
      {
        userAgent: 'GPTBot', // Block ChatGPT crawler
        disallow: '/',
      },
    ],
    sitemap: `${baseUrl}/sitemap.xml`,
    host: baseUrl,
  }
}

Résultat : https://hulli.studio/robots.txt auto-généré

Open Graph Images Dynamiques

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import db from '@/lib/db'

export const runtime = 'edge'
export const alt = 'Article Hulli Studio'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

interface Props {
  params: { slug: string }
}

export default async function Image({ params }: Props) {
  const post = await db.query.posts.findFirst({
    where: (posts, { eq }) => eq(posts.slug, params.slug),
  })

  if (!post) {
    return new ImageResponse(<div>Not found</div>, { ...size })
  }

  return new ImageResponse(
    (
      <div
        style={{
          background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          padding: '80px',
        }}
      >
        <h1
          style={{
            fontSize: 72,
            fontWeight: 'bold',
            color: 'white',
            textAlign: 'center',
            lineHeight: 1.2,
            marginBottom: 20,
          }}
        >
          {post.title}
        </h1>
        <p
          style={{
            fontSize: 32,
            color: 'rgba(255, 255, 255, 0.9)',
            textAlign: 'center',
            maxWidth: '80%',
          }}
        >
          {post.excerpt?.substring(0, 120)}...
        </p>
        <div
          style={{
            position: 'absolute',
            bottom: 40,
            right: 60,
            display: 'flex',
            alignItems: 'center',
            gap: 20,
          }}
        >
          <span style={{ fontSize: 28, color: 'white', fontWeight: 600 }}>
            hulli.studio
          </span>
        </div>
      </div>
    ),
    { ...size }
  )
}

Résultat : OG image auto-générée pour chaque article

OG Image Static

// app/opengraph-image.tsx
import { ImageResponse } from 'next/og'

export const runtime = 'edge'
export const alt = 'Hulli Studio - Agence Next.js Paris'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default async function Image() {
  return new ImageResponse(
    (
      <div
        style={{
          background: '#000',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <h1 style={{ fontSize: 100, color: 'white' }}>
          Hulli Studio
        </h1>
      </div>
    ),
    { ...size }
  )
}

Schema.org (JSON-LD)

Organization Schema

// components/organization-schema.tsx
export function OrganizationSchema() {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    'name': 'Hulli Studio',
    'url': 'https://hulli.studio',
    'logo': 'https://hulli.studio/logo.png',
    'description': 'Agence spécialisée Next.js et React à Paris',
    'address': {
      '@type': 'PostalAddress',
      'streetAddress': '123 Rue de la Paix',
      'addressLocality': 'Paris',
      'postalCode': '75002',
      'addressCountry': 'FR',
    },
    'contactPoint': {
      '@type': 'ContactPoint',
      'telephone': '+33-1-23-45-67-89',
      'contactType': 'Customer Service',
      'areaServed': 'FR',
      'availableLanguage': ['French', 'English'],
    },
    'sameAs': [
      'https://twitter.com/hullystudio',
      'https://linkedin.com/company/hulli-studio',
      'https://github.com/hullystudio',
    ],
  }

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  )
}

Article Schema

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({ params }: Props) {
  const post = await getPost(params.slug)

  const articleSchema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    'headline': post.title,
    'description': post.excerpt,
    'image': post.featuredImage,
    'datePublished': post.publishedAt?.toISOString(),
    'dateModified': post.updatedAt?.toISOString(),
    'author': {
      '@type': 'Person',
      'name': post.author,
      'url': 'https://hulli.studio/about',
    },
    'publisher': {
      '@type': 'Organization',
      'name': 'Hulli Studio',
      'logo': {
        '@type': 'ImageObject',
        'url': 'https://hulli.studio/logo.png',
      },
    },
    'mainEntityOfPage': {
      '@type': 'WebPage',
      '@id': `https://hulli.studio/blog/${post.slug}`,
    },
  }

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
      />
      <article>{/* Content */}</article>
    </>
  )
}

Breadcrumb Schema

// components/breadcrumb-schema.tsx
interface BreadcrumbItem {
  name: string
  url: string
}

export function BreadcrumbSchema({ items }: { items: BreadcrumbItem[] }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    'itemListElement': items.map((item, index) => ({
      '@type': 'ListItem',
      'position': index + 1,
      'name': item.name,
      'item': item.url,
    })),
  }

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  )
}

// Usage
;<BreadcrumbSchema
  items={[
    { name: 'Home', url: 'https://hulli.studio' },
    { name: 'Blog', url: 'https://hulli.studio/blog' },
    { name: post.title, url: `https://hulli.studio/blog/${post.slug}` },
  ]}
/>

Core Web Vitals Optimization

1. Largest Contentful Paint (LCP) <2.5s

Problème : Images lourdes chargent lentement

// ❌ Bad : img tag
;<img src="/hero.jpg" alt="Hero" />

// ✅ Good : next/image avec priority
import Image from 'next/image'
;<Image
  src="/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority // ✅ Preload LCP image
  quality={90}
  placeholder="blur"
  blurDataURL="data:image/..." // Low-quality placeholder
/>

Optimisation Fonts (LCP enemy #1) :

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // ✅ Show fallback while loading
  preload: true,
  variable: '--font-inter',
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr" className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}

2. First Input Delay (FID) <100ms

Problème : JavaScript bloque main thread

// ❌ Bad : Heavy component in initial bundle
import HeavyChart from '@/components/heavy-chart'

// ✅ Good : Dynamic import
import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('@/components/heavy-chart'), {
  loading: () => <div>Loading chart...</div>,
  ssr: false, // Client-side only
})

Reduce JavaScript bundle :

// next.config.ts
const nextConfig = {
  experimental: {
    optimizePackageImports: ['lodash', 'date-fns', 'lucide-react'],
  },
}

3. Cumulative Layout Shift (CLS) <0.1

Problème : Images/ads sans dimensions causent layout shifts

// ❌ Bad : No dimensions
<img src="/product.jpg" alt="Product" />

// ✅ Good : Width/height explicit
<Image
  src="/product.jpg"
  alt="Product"
  width={800}
  height={600}
  style={{ maxWidth: '100%', height: 'auto' }}
/>

Réserver espace ads/embeds :

/* Reserve space for ad */
.ad-container {
  min-height: 250px;
  width: 100%;
}

Benchmark Core Web Vitals

Projet client (avant optimisation) :

  • LCP : 4.2s ❌
  • FID : 180ms ❌
  • CLS : 0.28 ❌

Après optimisation (Next.js + techniques ci-dessus) :

  • LCP : 1.8s ✅ (-57%)
  • FID : 45ms ✅ (-75%)
  • CLS : 0.05 ✅ (-82%)

Impact SEO : Positions Google +12 (moyenne)

Internal Linking Strategy

// components/related-posts.tsx
import Link from 'next/link'
import db from '@/lib/db'

export async function RelatedPosts({ currentPostId }: { currentPostId: string }) {
  const relatedPosts = await db.query.posts.findMany({
    where: (posts, { eq, ne, and }) =>
      and(eq(posts.status, 'published'), ne(posts.id, currentPostId)),
    limit: 3,
    orderBy: (posts, { desc }) => [desc(posts.publishedAt)],
  })

  return (
    <div className="mt-12">
      <h2 className="text-2xl font-bold">Articles connexes</h2>
      <div className="grid gap-6 md:grid-cols-3">
        {relatedPosts.map((post) => (
          <Link key={post.id} href={`/blog/${post.slug}`} className="group">
            <h3 className="font-semibold group-hover:text-blue-600">{post.title}</h3>
            <p className="text-sm text-gray-600">{post.excerpt}</p>
          </Link>
        ))}
      </div>
    </div>
  )
}

Best practices linking :

  • ✅ 3-5 internal links par article
  • ✅ Anchor text descriptif (pas "cliquez ici")
  • ✅ Liens vers pages contextuelles
  • ✅ Breadcrumbs navigation

Canonical URLs (Éviter Duplicate Content)

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  return {
    alternates: {
      canonical: `https://hulli.studio/blog/${params.slug}`, // ✅ Canonical URL
    },
  }
}

Cas multi-domaines :

// Si site accessible via www et non-www
export const metadata: Metadata = {
  alternates: {
    canonical: 'https://hulli.studio', // Version canonique
  },
}

Image Alt Text & Optimization

// ❌ Bad
<Image src="/blog-image.jpg" alt="" />

// ✅ Good : Descriptive alt text
<Image
  src="/blog-image.jpg"
  alt="Dashboard Next.js avec graphiques temps réel et analytics utilisateurs"
  width={1200}
  height={630}
/>

Automated alt text extraction :

// lib/image-utils.ts
export function generateAltFromFilename(filename: string): string {
  return filename
    .replace(/\.(jpg|png|webp)$/i, '')
    .replace(/[-_]/g, ' ')
    .replace(/\b\w/g, (l) => l.toUpperCase())
}

// Example:
generateAltFromFilename('next-js-dashboard-analytics.jpg')
// → "Next Js Dashboard Analytics"

Monitoring SEO (Track Performance)

Google Search Console Integration

// app/api/sitemap-ping/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  const sitemapUrl = 'https://hulli.studio/sitemap.xml'

  // Ping Google
  await fetch(`https://www.google.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`)

  // Ping Bing
  await fetch(`https://www.bing.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`)

  return NextResponse.json({ success: true })
}

Trigger après deploy :

# Vercel deploy hook
curl https://hulli.studio/api/sitemap-ping

Analytics Setup

// app/layout.tsx
import { GoogleAnalytics } from '@next/third-parties/google'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr">
      <body>
        {children}
        <GoogleAnalytics gaId="G-XXXXXXXXXX" />
      </body>
    </html>
  )
}

Automatic Indexing (Google Indexing API)

// scripts/submit-to-google.ts
import { google } from 'googleapis'

const auth = new google.auth.GoogleAuth({
  credentials: JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_KEY!),
  scopes: ['https://www.googleapis.com/auth/indexing'],
})

const indexing = google.indexing({ version: 'v3', auth })

async function submitUrl(url: string, type: 'URL_UPDATED' | 'URL_DELETED') {
  await indexing.urlNotifications.publish({
    requestBody: {
      url,
      type,
    },
  })
  console.log(`${url} submitted to Google`)
}

// Usage
await submitUrl('https://hulli.studio/blog/new-post', 'URL_UPDATED')

Case Study : SEO Transformation Client

Avant Next.js SEO

Projet : SaaS B2B (Laravel legacy)

  • Trafic organique : 2,400 visites/mois
  • Positions Google : 12 keywords top 10
  • Core Web Vitals : Tous rouges
  • Lighthouse SEO : 68/100

Après Migration Next.js + SEO

Timeline : 6 mois

Optimisations appliquées :

  1. Metadata API complète
  2. Sitemap dynamique (1,200 pages)
  3. Internal linking strategy
  4. Core Web Vitals < seuils Google
  5. Schema.org (Organization + Articles)
  6. Alt text toutes images

Résultats :

  • Trafic organique : 10,600 visites/mois (+342%)
  • Positions Google : 48 keywords top 10 (+36)
  • Core Web Vitals : Tous verts ✅
  • Lighthouse SEO : 100/100 ✅
  • Conversions organiques : +180%

ROI :

  • Investment SEO : 12h dev (8,400€ @ 700€ TJM)
  • Revenue increase : +42k€/an (SaaS subscriptions from organic)
  • Payback : 2.4 mois

Checklist SEO Complète

1. Metadata (30min)

  • metadata ou generateMetadata chaque page
  • title unique (<60 chars)
  • description unique (150-160 chars)
  • keywords pertinents (5-10)
  • openGraph complet (title, description, image)
  • twitter card configurée
  • canonical URL définie
  • robots directives (index/noindex)

2. Sitemap & Robots (1h)

  • sitemap.ts dynamique créé
  • Toutes pages publiques incluses
  • lastModified dates correctes
  • priority values logiques
  • robots.ts configured
  • Vercel /sitemap.xml accessible
  • Google Search Console sitemap submitted

3. Images (2h)

  • Toutes images via next/image
  • alt text descriptif partout
  • priority sur LCP images
  • width/height explicit
  • WebP format (auto Next.js)
  • Lazy loading (auto Next.js)

4. Core Web Vitals (4-8h)

  • LCP <2.5s
  • FID <100ms
  • CLS <0.1
  • Fonts optimized (next/font)
  • Heavy components dynamic import
  • JavaScript bundle <200KB
  • Images dimensions reserved

5. Schema.org (1h)

  • Organization schema
  • Article schema (blog)
  • Breadcrumb schema
  • Product schema (e-commerce)
  • FAQ schema (si applicable)

6. Content (1h)

  • Titres H1 uniques
  • Hierarchy H2-H6 logique
  • 3-5 internal links par page
  • External links rel="noopener"
  • Long-form content (>1500 words pour blog)

7. Technical (30min)

  • HTTPS (certificat SSL)
  • Canonical URLs
  • 301 redirects (si migration)
  • 404 page custom
  • Structured URLs (lisibles)

8. Monitoring (30min)

  • Google Search Console configured
  • Google Analytics 4 installed
  • Bing Webmaster Tools
  • Core Web Vitals tracking
  • Automatic sitemap ping

Total : 10-15 heures setup initial

Erreurs SEO Courantes Next.js

1. Oublier metadataBase

// ❌ Bad : Open Graph URLs relatives
export const metadata = {
  openGraph: {
    images: ['/og-image.jpg'], // ❌ URL relative
  },
}

// ✅ Good : metadataBase défini
export const metadata = {
  metadataBase: new URL('https://hulli.studio'),
  openGraph: {
    images: ['/og-image.jpg'], // ✅ Auto-resolve to absolute
  },
}

2. Duplicate Titles

// ❌ Bad : Même title partout
export const metadata = { title: 'Hulli Studio' }

// ✅ Good : Template title
export const metadata = {
  title: {
    template: '%s | Hulli Studio',
    default: 'Hulli Studio - Agence Next.js Paris',
  },
}

3. Bloquer Googlebot

// ❌ Bad : noindex en production
export const metadata = {
  robots: { index: false }, // ❌ Bloque Google!
}

// ✅ Good : Conditional based on env
export const metadata = {
  robots: {
    index: process.env.NODE_ENV === 'production',
    follow: true,
  },
}

4. Images Sans Dimensions

// ❌ Bad : CLS issues
<Image src="/hero.jpg" alt="Hero" fill />

// ✅ Good : Explicit dimensions
<Image src="/hero.jpg" alt="Hero" width={1920} height={1080} />

Outils SEO Recommandés

Audit :

Monitoring :

Testing :

Conclusion

Next.js 15 + App Router = SEO excellence si bien configuré.

Checklist essentiels :

  1. ✅ Metadata API complète
  2. ✅ Sitemap + Robots dynamiques
  3. ✅ Core Web Vitals <seuils
  4. ✅ Schema.org (JSON-LD)
  5. ✅ Internal linking strategy

Timeline setup : 10-15 heures ROI moyen : +200-400% trafic organique (6-12 mois)

Notre recommandation Hulli Studio : Investir 2 jours SEO setup = payback 2-3 mois via organic traffic.


FAQ

Next.js bon pour SEO vs WordPress ?

Next.js meilleur si bien configuré. Performance > WordPress (Lighthouse 95+ vs 60-70). Mais WordPress SEO plugins clé-en-main (Yoast). Next.js = config manuelle mais résultats supérieurs.

SSR ou SSG pour SEO ?

Les deux excellents pour SEO. Google crawle contenu HTML (SSR et SSG génèrent HTML). SSG = meilleur (performance). SSR = acceptable si dynamic data needed.

Metadata API vs react-helmet ?

Metadata API Next.js 15+. Built-in, type-safe, SSR compatible. react-helmet = legacy approche (client-side manipulation).

Core Web Vitals vraiment impact ranking ?

Oui, depuis Google Page Experience Update (2021). Core Web Vitals = ranking factor. Sites CWV verts ranker +10-25 positions vs concurrents rouges.


Articles connexes :