SEO Next.js : Le Guide Complet 2026 pour Ranker #1 Google
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 :
- Metadata API complète
- Sitemap dynamique (1,200 pages)
- Internal linking strategy
- Core Web Vitals < seuils Google
- Schema.org (Organization + Articles)
- 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)
-
metadataougenerateMetadatachaque page -
titleunique (<60 chars) -
descriptionunique (150-160 chars) -
keywordspertinents (5-10) -
openGraphcomplet (title, description, image) -
twittercard configurée -
canonicalURL définie -
robotsdirectives (index/noindex)
2. Sitemap & Robots (1h)
-
sitemap.tsdynamique créé - Toutes pages publiques incluses
-
lastModifieddates correctes -
priorityvalues logiques -
robots.tsconfigured - Vercel
/sitemap.xmlaccessible - Google Search Console sitemap submitted
3. Images (2h)
- Toutes images via
next/image -
alttext descriptif partout -
prioritysur LCP images -
width/heightexplicit - 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 :
- Lighthouse (Chrome DevTools)
- PageSpeed Insights
- Screaming Frog (crawl site)
Monitoring :
- Google Search Console
- Ahrefs (backlinks, keywords)
- Semrush (competitor analysis)
Testing :
- Rich Results Test (Schema.org)
- Mobile-Friendly Test
- Core Web Vitals Report
Conclusion
Next.js 15 + App Router = SEO excellence si bien configuré.
Checklist essentiels :
- ✅ Metadata API complète
- ✅ Sitemap + Robots dynamiques
- ✅ Core Web Vitals <seuils
- ✅ Schema.org (JSON-LD)
- ✅ 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 :