Métadonnées Next.js dans le body : neuf conditions testées
Neaptide · 12 octobre 2026 · 8 min de lecture
Test de neuf configurations sous Next.js 15.5.22 : comparaison entre HTML brut et DOM navigateur, et impact du streaming sur la balise canonical.
Dans cet article

Retrouver des métadonnées après la balise fermante </head> peut résulter directement du streaming de Next.js. Sur notre build de production local, les métadonnées asynchrones apparaissaient dans <body> pour deux profils de requête et dans <head> pour un troisième. Si leurs valeurs figuraient bien dans chacune des réponses HTTP complètes, cela ne suffit pas à établir qu’un moteur de recherche accepte chaque balise à cet emplacement.
Commencez par identifier la balise concernée, le profil de requête et la représentation du document que vous inspectez. Notre expérience décrit le comportement d’envoi du serveur. Pour la balise canonical, tenez également compte de la règle de Google qui exige sa présence dans <head> : repérer la bonne URL quelque part dans le document ne suffit pas à valider l’implémentation.
Ce que nous avons testé
Le 12 octobre 2026, nous avons testé Next.js 15.5.22 avec React et React DOM 19.2.6 sous Node v25.2.1. Le croisement de trois modes de génération avec trois profils HTTP User-Agent explicites a produit neuf conditions, exécutées deux fois chacune. Nous avons archivé les 18 réponses HTTP 200 complètes, leurs en-têtes et leur HTML : les deux passages concordaient exactement sur le nombre, la valeur et l’emplacement des balises.
Les trois modes comparés étaient un objet metadata statique, une fonction generateMetadata asynchrone avec la configuration par défaut, puis cette même fonction accompagnée de htmlLimitedBots: /.*/. Les deux routes utilisaient force-dynamic : le terme « statique » qualifie ici l’export des métadonnées, et non une page prérendue. La fonction asynchrone marquait une pause volontaire de 250 ms, sans appel externe : ce délai sert uniquement à rendre la séquence d’envoi observable et ne reflète ni une charge réelle ni un score de performance.
Comment le profil de requête modifie la réponse HTML

| Mode de métadonnées | UA type navigateur | UA Googlebot | UA Twitterbot |
|---|---|---|---|
| Objet statique · par défaut | head | head | head |
| Fonction async · par défaut | body | body | head |
| Fonction async · bloquant | head | head | head |
Le script de collecte envoyait une chaîne proche de Chrome 134, Googlebot/2.1 ou Twitterbot/1.0. Il s’agit de simples en-têtes HTTP, et non de visites réelles de ces services. Chaque requête précisait Accept: text/html et Accept-Encoding: identity, sans cookie ni en-tête de navigation client. L’archive téléchargeable contient les chaînes et requêtes exactes.
Dans le navigateur, les balises async par défaut restent dans body
Nous avons également chargé les trois modes dans le navigateur connecté, dont le User-Agent reçu annonçait Chrome 154. Dans le DOM rendu, title, description et canonical se trouvaient bien dans <head> pour les modes statique et bloquant, mais demeuraient dans <body> avec le mode asynchrone par défaut : aucune remontée vers <head> n’a été observée. Ces trois chargements constituent des observations distinctes des 18 requêtes HTTP et ne reproduisent pas le rendu de Googlebot.
| Mode | DOM rendu : les trois balises |
|---|---|
| Objet statique · par défaut | head |
| Fonction async · par défaut | body |
| Fonction async · bloquant | head |
Pourquoi la balise canonical exige un arbitrage dédié
Next.js documente l’envoi en streaming des métadonnées dans <body> ainsi qu’un envoi bloquant réservé aux robots aux capacités HTML limitées, ce qui explique la répartition observée. La documentation de Google sur la canonicalisation indique toutefois que le lien HTML canonical est accepté dans <head>, et son exemple d’injection JavaScript l’insère lui aussi dans document.head. La documentation générale du framework ne tranche donc pas cette exigence propre à la balise canonical.
Sur notre banc d’essai asynchrone par défaut, la balise canonical reste dans <body>, tant dans la réponse HTTP collectée que dans le DOM du navigateur. Nous ne pouvons pas en déduire si Google l’accepte ou l’ignore : l’application tournait en local, portait une directive noindex et n’a jamais été soumise à l’indexation. Traitez ce cas comme un point de vérification obligatoire sur votre route déployée. Si la balise canonical demeure hors de <head>, adoptez une implémentation conforme à l’emplacement documenté par Google, puis recontrôlez le résultat. La configuration bloquante ci-dessous a replacé la balise dans <head> sur notre banc d’essai ; il s’agit d’une option testée, et non d’une mesure de progression SEO.
Contrôle ciblé sur votre propre route
- Enregistrez la réponse complète servie en production pour l’URL concernée, avec les en-têtes de requête et la version déployée. Inspectez les véritables éléments HTML title, description et canonical : une chaîne présente dans une charge utile de script React ne constitue pas une seconde balise HTML.
- Inspectez séparément le DOM rendu dans le navigateur : relevez les valeurs, les éventuels doublons et le rattachement à <head> ou à <body>. Voir le bon titre dans l’onglet du navigateur ne prouve rien quant au lien canonical.
- Comparez les profils pertinents pour le robot ou le service concerné, en distinguant bien les observations issues d’un navigateur, d’un en-tête User-Agent simulé et d’un véritable service externe : simuler un User-Agent ne teste que la sélection de réponse de votre serveur.
- Appliquez la règle propre à chaque balise. Pour la balise canonical, inspectez la page déployée via les outils Google disponibles et ses rapports de canonicalisation, tout en veillant à ce que le lien HTML figure bien dans <head>. Une capture locale du DOM ne révèle pas l’URL canonique retenue par Google.
- Modifiez uniquement le paramètre pertinent, recompilez et répétez exactement les mêmes vérifications. Si vous modifiez le streaming des métadonnées, évaluez aussi l’impact sur le chargement avec votre charge réelle ; notre délai artificiel ne fournit aucune estimation des performances en production.
La configuration effectivement testée
Pour le troisième mode, nous avons recompilé la même application avec l’option suivante, qui a placé les trois balises dans <head> pour l’ensemble des profils testés. Cette option remplace la liste de robots par défaut, et l’expression /.*/ intercepte tous les User-Agent : conservez le reste de votre configuration et mesurez la portée du changement avant de l’adopter.
// next.config.mjs
export default {
htmlLimitedBots: /.*/,
};Activer cette option change le mode d’envoi des métadonnées sur l’ensemble de l’application. Notre expérience n’a testé ni une expression régulière restreinte à certains robots, ni les caches de production, ni la navigation entre routes, et elle ne justifie pas de basculer tous les sites Next.js vers des métadonnées bloquantes.
Reproduire l’expérience
Téléchargez l’application de test et son fichier de verrouillage des dépendances. Le README détaille les commandes de build et de démarrage pour les deux configurations, ainsi que l’exécution du collecteur Python. Lancez les deux serveurs dans des terminaux séparés, puis exécutez le collecteur depuis la racine de l’archive extraite. Maintenez ce projet en local : il comporte un délai artificiel, une directive noindex et des URL canoniques réservées sur example.test. L’archive de preuves regroupe les réponses HTML brutes et les captures du DOM ; results.json rassemble l’intégralité des profils et des observations analysées.
Consulter les preuves et les données
- Protocole, résultats et sources · JSON
- Méthode et limites · TXT
- Banc d’essai reproductible · ZIP
- Preuves HTTP brutes et captures DOM · ZIP
Ce test porte sur une seule version logicielle, deux builds locaux et trois chargements dans un navigateur. Nous n’avons testé ni CDN, ni robots distants réels, ni navigation côté client, ni échecs de génération des métadonnées, ni Cache Components, et nous n’avons mesuré ni l’indexation, ni les positions, ni les citations par les moteurs IA, ni le trafic, la conversion ou les Core Web Vitals. Une même personne a assuré la collecte et la vérification des relevés : recontrôlez systématiquement le comportement sur votre propre version et votre déploiement.