Enzo Ruffin

PortFolio 2026

Comment Next.js gère le SEO

Metadata, canonical, hreflang, sitemap, images OpenGraph, JSON-LD : ce que Next.js fait vraiment pour le référencement, et comment je m'en sers sur ce portfolio.

14 min de lecture
Enzo Ruffin
SEO

Next.js a la réputation d'être bon pour le référencement. C'est vrai, mais mal formulé. Next fournit des outils, il ne produit pas de résultats : le rendu côté serveur, une API de métadonnées, des fichiers de convention pour le sitemap et le robots, et la génération d'images de partage. Le reste, canonical corrects, hreflang cohérents, données structurées, performance, reste à la charge du développeur. Ce site est un cas intéressant parce qu'il cumule les contraintes : bilingue, avec un blog, et déployé en export statique sur un hébergement mutualisé, donc sans le moindre serveur Node pour rattraper quoi que ce soit au moment de la requête. Voici ce que Next apporte, et ce qu'il a fallu construire autour.

Tout part du rendu

Le premier atout de Next n'est pas une balise, c'est le fait que la page arrive déjà écrite. Avec l'App Router, les pages sont des Server Components : elles lisent leurs données pendant la construction et produisent du HTML complet. Un robot qui demande une URL de ce site reçoit le titre, les paragraphes, les liens et les métadonnées dans la réponse, sans avoir à exécuter la moindre ligne de JavaScript. Ça compte, parce que Google exécute bien le JavaScript, mais dans une seconde passe, avec un budget limité et sans garantie de délai. Ici la configuration est réglée sur output export : chaque route devient un fichier HTML posé sur le disque, animations WebGL comprises, puisque tout ce qui bouge est isolé dans des composants clients qui s'hydratent par-dessus un contenu déjà lisible.

L'API Metadata, et le seul endroit qui la connaît

Chaque page exporte soit un objet metadata, soit une fonction generateMetadata quand le contenu dépend de l'URL, ce qui est le cas d'un article de blog. Next assemble ensuite le head : title, description, canonical, OpenGraph, Twitter. Sur ce portfolio, aucune page n'écrit ses métadonnées à la main. Toutes appellent une seule fonction, pageMetadata, qui reçoit la langue, le titre, la description et la paire d'URL des deux versions linguistiques. Elle en déduit le canonical, les hreflang, la carte OpenGraph et la carte Twitter. L'intérêt n'est pas d'écrire moins de code, il est d'avoir un seul endroit à corriger le jour où une convention change, au lieu de dix fichiers qui divergent doucement.

Premier piège : Next remplace openGraph, il ne le fusionne pas

Celui-là coûte cher parce qu'il ne se voit pas. Les métadonnées d'un layout parent sont héritées par ses pages, sauf pour les objets imbriqués : dès qu'une page déclare son propre openGraph, l'objet parent est remplacé en entier, pas complété. Résultat, le siteName et l'image déclarés au niveau du layout racine disparaissent du head de toutes les pages qui personnalisent leur titre de partage. Le head reste valide, l'aperçu de lien perd juste son nom de site, et rien n'échoue. La parade tient en une ligne de discipline : pageMetadata réinjecte systématiquement le siteName, la locale OpenGraph et la locale alternative, et aucune page n'a le droit d'écrire un openGraph de son côté.

Deuxième piège : ne jamais déclarer icons dans metadata

Next propose deux mécanismes pour les icônes. Les conventions de fichiers, où poser app/icon.png et app/apple-icon.png suffit à ce que les balises soient générées avec le bon hash. Et le champ icons de l'objet metadata. Le second écrase silencieusement le premier. Déclarer icons pour ajouter une seule taille fait disparaître du head les fichiers de convention, et avec eux l'icône d'écran d'accueil iOS. Sur ce site, le champ est volontairement absent et un commentaire explique pourquoi, parce que c'est typiquement la ligne qu'un futur moi réécrirait en croyant bien faire.

Canonical et hreflang, avec une seule source de vérité

Le champ alternates de l'API Metadata produit le canonical et les liens de langue. Encore faut-il que les URL soient justes. Sur ce site, le français vit à la racine et l'anglais sous /en, avec des chemins traduits : /mentions-legales d'un côté, /en/legal-notice de l'autre. Une seule table décrit cette correspondance, et elle alimente à la fois les liens du site, le sélecteur de langue, les hreflang et le sitemap. Les articles, eux, gardent le même slug dans les deux langues : c'est la clé qui permet d'apparier les deux versions sans table supplémentaire. Le français sert de x-default, puisqu'il est la version servie par défaut. La Search Console signale son absence comme une erreur, et il doit être déclaré à deux endroits, dans les balises de la page et dans le sitemap.

sitemap.ts, robots.ts, manifest.ts

Ce sont des fichiers de convention : on exporte une fonction, Next génère le XML ou le JSON correspondant. Le sitemap de ce site n'a aucune liste écrite à la main. Il parcourt les routes statiques dans les deux langues, puis le tableau des articles, et pose sur chaque entrée ses alternates. Publier un article ajoute donc quatre lignes au sitemap sans que personne y touche. Deux détails valent le détour. Le lastmod des pages fixes est une constante mise à jour à la main, surtout pas un new Date() : un site qui annonce toutes ses pages modifiées à chaque déploiement apprend aux robots à ignorer son lastmod. Et le robots.txt autorise explicitement /_next/, parce que bloquer le JavaScript et le CSS revient à faire indexer une page blanche, tout en interdisant les fichiers .txt, qui sont les payloads du routeur client et feraient autant de doublons illisibles dans les résultats.

Les images de partage, générées au build

Next transforme un fichier opengraph-image.tsx en image PNG, à partir de vrai JSX. Sur ce site, deux générateurs suffisent : une carte de marque pour les pages, et une carte titrée pour les articles, qui reprend le titre et la catégorie. Chaque route porte son fichier, dix au total en comptant les deux langues, et celui des articles l'emporte sur celui de la section parce qu'il est plus spécifique. Le point à ne pas rater : on ne déclare jamais l'URL de cette image dans l'objet metadata. Next l'y branche lui-même, hash de build compris. L'écrire à la main revient à figer une URL qui changera au prochain déploiement, et un aperçu de lien cassé ne se voit que le jour où quelqu'un partage la page.

Les données structurées, là où Next ne fait rien

Le JSON-LD n'a pas d'API dédiée. On l'écrit dans un script balisé, rendu côté serveur. C'est la partie où il y a le plus à gagner, parce qu'elle décrit à Google ce que la page est, et pas seulement ce qu'elle contient. L'accueil porte trois blocs : une fiche Person complète, un WebSite et un ProfilePage qui dit explicitement que cette page est la fiche d'une personne. Le blog déclare un Blog, chaque article un BlogPosting avec sa date de publication, son nombre de mots, sa durée de lecture convertie en durée ISO, sa catégorie et la mention qu'il est lisible sans compte. Chaque page intérieure ajoute un fil d'Ariane, que Google affiche à la place de l'URL dans ses résultats. Les entités portent des identifiants stables, du genre #person et #website, ce qui permet à un article de dire qu'il est écrit par la personne décrite ailleurs sur le site plutôt que de déclarer un homonyme à chaque page. Avec une nuance apprise en route : Google ne suit pas un identifiant d'une page à l'autre, donc le nœud auteur reste court mais autonome partout où il apparaît.

Le bilingue, sans dupliquer le site

Les deux langues sont deux arbres de routes dans le même projet, chacun avec son layout et son attribut lang sur la balise html. Les composants partagés reçoivent la langue en paramètre et lisent leurs textes dans des fichiers de messages. Le sélecteur de langue est le détail que la plupart des sites ratent : il pointe l'équivalent exact de la page courante, article compris, et pas la racine de l'autre langue. Renvoyer un lecteur anglophone d'un article vers la page d'accueil anglaise casse le signal qu'on vient justement d'envoyer avec les hreflang, en plus d'être désagréable.

Ce que Next ne peut plus faire en export statique

Un export statique n'a ni middleware, ni redirections calculées à la requête, ni en-têtes personnalisés. Tout ce que Next gère habituellement au runtime doit être repris ailleurs, et sur un hébergement Apache mutualisé, cet ailleurs s'appelle .htaccess. Il porte ici quatre choses qui touchent directement au référencement. L'hôte canonique, pour que www et le domaine nu ne servent pas deux versions indexables de la même page. La redirection vers HTTPS. Un X-Robots-Tag noindex posé sur l'URL de préproduction de l'hébergeur, qui sinon se retrouverait indexée en double du domaine. Et la négociation de langue, faite à l'ancienne, avec l'en-tête Accept-Language et le cookie posé par le sélecteur, en 302 pour ne pas graver une préférence dans le cache des navigateurs.

Le piège qui met tout le site en 403

Celui-là mérite sa section, parce qu'il ne se manifeste qu'une fois en ligne. L'export produit pour une même route un fichier et un dossier : out/blog.html contient la page, out/blog/ contient les articles. À la requête /blog, Apache voit d'abord le dossier, ajoute un slash, y cherche un index.html qui n'existe pas, et répond 403. Toutes les pages du site sont concernées, l'accueil excepté, ce qui donne un site apparemment en ligne dont chaque lien mène à une erreur. La règle de réécriture qui sert le .html correspondant fait trois lignes, et c'est la partie du fichier que je ne touche jamais sans la retester. Même famille de problème pour les images OpenGraph : elles sortent de l'export sans extension, donc Apache leur donne un type par défaut, et aucun aperçu de lien ne s'affiche tant qu'on ne leur force pas un Content-Type image/png.

La performance fait partie du référencement

Les Core Web Vitals ne font pas monter un site à eux seuls, mais une page lente perd des visiteurs avant même la question du classement. Le gain le plus net sur ce projet n'est pas venu d'un réglage d'image, il est venu d'un fichier d'index. Les dossiers de composants exportaient un baril, et importer ce baril depuis une page chargeait tous les modules du dossier : l'accueil, le blog et la page contact embarquaient Three.js pour un composant qui ne sert que sur les pages d'article. Cinq cent cinquante kilooctets de JavaScript analysés pour rien, sur les trois pages qui reçoivent le plus de trafic. Le correctif tient en un changement d'imports, ciblés sur les modules concrets. Le second point est le LCP : l'image de hero est une balise img avec fetchPriority élevé, ce qui suffit à React pour émettre lui-même un preload responsive, mieux placé dans le head qu'un preload écrit à la main.

Comment je vérifie que ça tient

Le SEO d'un site statique a un avantage rare : tout est vérifiable sur le disque, avant même de déployer. Après un build, j'ouvre les fichiers de out et je cherche dans le head ce qui doit y être : un seul canonical, trois liens de langue dont le x-default, le siteName OpenGraph présent sur toutes les pages, les icônes de convention encore là, et le JSON-LD complet. Les blocs de données structurées passent au test des résultats enrichis de Google. Le sitemap se relit à l'œil : le nombre d'entrées doit correspondre au nombre de pages fois deux langues. Ensuite, la Search Console prend le relais, avec deux rapports qui méritent une visite régulière, celui de l'indexation et celui de l'internationalisation, qui râle vite et à raison quand un hreflang ne pointe pas vers une page qui lui répond.

Ce que j'en retiens

Next.js règle la partie mécanique du référencement, et il la règle bien : du HTML complet, des métadonnées typées, un sitemap et un robots générés, des images de partage produites au build. Ce qu'il ne règle pas, ce sont les décisions. Quelles URL sont canoniques, comment les langues s'apparient, ce que le site déclare être, ce qu'on accepte de charger sur la page d'accueil. Ces choix-là ne sont pas dans un plugin, et ce sont exactement ceux qui font la différence entre un site techniquement propre et un site que les moteurs comprennent. Sur ce portfolio, la règle que je me suis donnée est simple : une seule source de vérité pour les URL, une seule fonction pour les métadonnées, et rien de recopié à la main dans une page.

14 min de lecture
Enzo Ruffin