Skip to content

Rule · SEO

Crawler receives <title>/canonical/description inside <body> (Next.js streaming metadata)

seo/metadata-in-bodyerrorvoid seoupdated

Why it matters

On request-time routes Next.js 16 streams metadata into <body> for any user agent not in htmlLimitedBots, and the default list excludes Googlebot and all AI crawlers. Google only accepts rel=canonical in <head> (it stays in <body> even after rendering), and non-rendering AI crawlers may miss the title.

How to fix it

Prerender the metadata (static params or 'use cache' in generateMetadata), or extend htmlLimitedBots with Googlebot and the AI crawlers. Setting htmlLimitedBots replaces Next's default list, so keep it.

Example

ts
// next.config.ts — verified on Next 16.3.5: bots get <head> metadata and in-order HTML, browsers keep streaming
import type { NextConfig } from 'next'

// Copy of Next's default list (html-bots.ts); re-diff on upgrades.
const NEXT_DEFAULT_HTML_BOTS =
  "[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight"

const EXTRA_BOTS = [
  'Googlebot', 'GPTBot', 'OAI-SearchBot', 'ChatGPT-User',
  'ClaudeBot', 'Claude-SearchBot', 'Claude-User', 'Claude-Code',
  'PerplexityBot', 'Perplexity-User', 'meta-externalagent', 'meta-externalfetcher',
  'Amazonbot', 'DuckAssistBot', 'MistralAI-User', 'CCBot', 'Bytespider',
].join('|')

const nextConfig: NextConfig = {
  htmlLimitedBots: new RegExp(NEXT_DEFAULT_HTML_BOTS + '|' + EXTRA_BOTS, 'i'),
}
export default nextConfig

References

void seo reports 60 rules in this category. What search engines need from the raw response: status codes, titles and descriptions in <head>, self-referencing canonicals, one h1, crawlable links, Open Graph, icons, robots.txt and a sitemap with real dates.