SEO technique pour les développeurs : ce que votre framework ne règle pas tout seul
Je suis développeur autant que SEO. Et l'un des malentendus les plus répandus que je rencontre chez les devs, c'est de croire que leur framework gère le SEO.
Next.js, Nuxt, Astro, SvelteKit — ces outils font des choses remarquables. Le rendu côté serveur, la génération statique, la gestion des métadonnées. Mais ils ne font pas du SEO. Ils vous donnent les outils pour faire du SEO. Ce n'est pas la même chose.
Voici ce que les frameworks ne règlent pas — et comment le corriger.
Ce que les frameworks gèrent bien
Soyons honnêtes. Next.js App Router, par exemple, rend plusieurs problèmes SEO classiques beaucoup plus simples :
- Métadonnées dynamiques via
generateMetadata()— titre, description, OG par page - Rendu serveur — Googlebot voit du HTML, pas du JavaScript à exécuter
- Optimisation des images —
<Image>gère automatiquement WebP, srcset, lazy loading - Font display —
next/fontinjectefont-display: swappar défaut
C'est réel. Ces frameworks sont objectivement meilleurs pour le SEO que les SPA React classiques des années 2015-2020.
Mais voilà ce qu'ils ne font pas.
1. Les canonical — votre responsabilité entière
Next.js génère des métadonnées. Il ne décide pas quelle URL est la version canonique de votre page. C'est votre boulot.
Les pièges classiques avec Next.js :
// ❌ Canonical auto-référençant absent
export const metadata: Metadata = {
title: 'Ma page',
description: '...',
// canonical manquant → Google doit deviner
}
// ✅ Canonical explicite
export const metadata: Metadata = {
title: 'Ma page',
description: '...',
alternates: {
canonical: 'https://votresite.com/ma-page',
},
}
Et si vos URLs changent selon des query params (?sort=price, ?page=2), vous devez décider explicitement :
- Les pages de pagination : canonical vers la première page ? Ou canonical self-referencing +
rel="next"/rel="prev"? - Les pages avec filtres : canonical vers l'URL sans filtres pour éviter la duplication
// Page de catégorie e-commerce avec filtres
export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {
const canonical = `https://votresite.com/categorie/robes` // URL propre sans filtres
return {
alternates: { canonical },
}
}
2. Les H1 multiples — un classique du composant-first
La pensée composant, c'est le mode de raisonnement naturel en React/Vue. Chaque composant est indépendant. Le problème : un composant <HeroSection> qui a son propre <h1>, combiné avec un composant <PageHeader> qui a aussi son <h1>, vous donne deux H1 sur la même page sans que personne ne l'ait décidé.
// ❌ Deux composants qui s'ignorent mutuellement
// HeroSection.tsx
export function HeroSection({ title }: { title: string }) {
return <h1>{title}</h1> // H1 numéro 1
}
// PageHeader.tsx
export function PageHeader({ heading }: { heading: string }) {
return <h1 className="page-title">{heading}</h1> // H1 numéro 2
}
Solution : établissez une règle dans votre équipe — un seul composant peut rendre un <h1>, et ce composant est celui de la page, pas d'un composant réutilisable.
// ✅ H1 toujours dans la page, jamais dans un composant générique
// page.tsx
export default function Page() {
return (
<>
<h1>Le vrai titre de cette page</h1>
<HeroSection /> {/* Hero sans h1 — utilise un <p> ou <div> */}
</>
)
}
// HeroSection.tsx — maintenant sans h1
export function HeroSection() {
return <div className="hero">...</div>
}
3. Les redirects — les frameworks vous donnent la syntaxe, pas la logique
next.config.js vous permet de définir des redirects. Mais le framework ne sait pas :
- Quelles anciennes URLs existent
- Lesquelles ont des backlinks
- Lesquelles sont indexées
Une migration sans audit préalable des redirects existants, c'est un désastre SEO en cours d'exécution.
Ce que je vois souvent après une migration Next.js :
// next.config.js — redirects bien intentionnés mais incomplets
module.exports = {
redirects: async () => [
{ source: '/blog/:slug', destination: '/articles/:slug', permanent: true },
// ❌ Mais les 50 anciennes URLs de catégories ? Pas migrées.
// ❌ Et les URLs avec paramètres GET ? Pas gérées.
// ❌ Et les URLs en /fr/ de l'ancien site multilingue ? Oubliées.
],
}
Protocol avant toute migration :
- Exportez toutes vos URLs indexées depuis Google Search Console
- Exportez tous vos backlinks depuis Ahrefs/Majestic
- Croisez avec votre nouveau plan d'URL
- Chaque URL avec des backlinks ou du trafic doit avoir un redirect 301 vers son équivalent
4. Le rendu JavaScript — SSR ne garantit pas la crawlabilité
Next.js App Router avec Server Components : le HTML est rendu serveur, Googlebot voit tout. C'est vrai.
Mais dès que vous avez des composants client ('use client') qui chargent du contenu en data fetching côté client, ce contenu est invisible pour le crawl.
// ❌ Ce contenu ne sera pas crawlé par Google
'use client'
export function ProductList() {
const [products, setProducts] = useState([])
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts)
// Google voit une liste vide à l'index
}, [])
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
// ✅ Fetch serveur — contenu visible dans le HTML initial
// Server Component dans app/
export async function ProductList() {
const products = await fetch('https://api.example.com/products').then(r => r.json())
return <ul>{products.map((p: Product) => <li key={p.id}>{p.name}</li>)}</ul>
}
La règle : tout contenu que vous voulez indexé doit être dans le HTML initial. Si c'est chargé côté client après l'hydratation, Google peut ne jamais le voir.
5. Les Core Web Vitals — les frameworks aident, ils ne résolvent pas
<Image> de Next.js gère le lazy loading et le format WebP. C'est bien. Mais il ne :
- Décide pas quelle image est le LCP et doit avoir
priority - Empêche pas les images oversized (1200px affichées en 200px)
- Gère pas les fonts tierces qui bloquent le rendu
// ❌ Toutes les images avec le même composant sans distinction
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} />
<Image src="/thumbnail.jpg" alt="Produit" width={200} height={200} />
// ✅ LCP image avec priority, le reste en lazy
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority /> // LCP
<Image src="/thumbnail.jpg" alt="Produit" width={200} height={200} /> // lazy par défaut
Pour les fonts externes (Google Fonts, Typekit), next/font gère le font-display. Mais si vous chargez une font via un <link> manuel dans le <head>, vous bloquez probablement le rendu.
6. Le sitemap — générez-le, ne le laissez pas au hasard
Next.js 13+ supporte les sitemaps dynamiques via app/sitemap.ts. Mais vous devez l'écrire. Et surtout, vous devez décider quelles pages inclure — pas toutes.
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts() // vos articles de blog
const products = await getProducts() // vos produits
return [
{
url: 'https://votresite.com',
lastModified: new Date(),
priority: 1,
},
...posts.map(post => ({
url: `https://votresite.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
priority: 0.8,
})),
// ❌ Ne pas inclure : pages de pagination, pages avec noindex, pages de recherche
]
}
Ce que beaucoup font : inclure toutes les URLs dans le sitemap, y compris les pages de résultats de recherche, les pages de filtres e-commerce, les pages d'erreur. C'est contre-productif — le sitemap doit contenir uniquement les pages que vous voulez que Google indexe.
7. Les données structurées — le framework ne les génère pas
Aucun framework ne génère automatiquement du schema.org. C'est un oubli quasi-universel dans les projets Next.js.
La façon la plus propre en App Router :
// Dans votre page ou layout
const schema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.date,
author: {
'@id': 'https://votresite.com/a-propos#auteur',
'@type': 'Person',
name: 'Votre Nom',
},
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
{/* reste de la page */}
</>
)
Les frameworks modernes vous donnent une excellente base. Mais le SEO technique reste une discipline à part entière qui demande des décisions conscientes — pas une feature qui s'active automatiquement.
Mon conseil : auditez votre site après chaque migration majeure. Les régressions SEO post-migration sont la source de perte de trafic numéro un que je vois chez les clients qui switchent de framework.
Pharone détecte automatiquement les problèmes décrits dans cet article. Lancez un audit sur votre site Next.js, Nuxt ou Astro pour avoir un état des lieux complet.
Consultant SEO technique, 15 ans d'expérience (Vanksen, Peugeot, MACIF). Fondateur de Pharone.ia.