---
title: "Crawler receives <title>/canonical/description inside <body> (Next.js streaming metadata) (seo/metadata-in-body)"
description: "seo/metadata-in-body: On request-time routes Next.js 16 streams metadata into <body> for any user agent not in htmlLimitedBots, and the default list excludes…"
canonical: https://void-design.vercel.app/rules/seo/metadata-in-body
lastModified: 2026-09-16
---

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

`seo/metadata-in-body` · severity **error** · category SEO · detected by `void seo` and `void audit`

## 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

- https://nextjs.org/docs/app/api-reference/config/next-config-js/htmlLimitedBots
- https://nextjs.org/docs/app/api-reference/functions/generate-metadata
- https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls

## More seo rules

`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.

- `title-missing` No single non-empty <title> in <head> — [seo/title-missing](https://void-design.vercel.app/rules/seo/title-missing)
- `title-length` Title length outside 10–60 characters, or keyword-stuffed — [seo/title-length](https://void-design.vercel.app/rules/seo/title-length)
- `meta-description-missing` No meta description in <head> — [seo/meta-description-missing](https://void-design.vercel.app/rules/seo/meta-description-missing)
- `meta-description-length` Meta description length outside 50–160 characters — [seo/meta-description-length](https://void-design.vercel.app/rules/seo/meta-description-length)
- `canonical-missing` No single rel=canonical in <head> — [seo/canonical-missing](https://void-design.vercel.app/rules/seo/canonical-missing)
- `canonical-invalid` Canonical URL is relative or contains a fragment — [seo/canonical-invalid](https://void-design.vercel.app/rules/seo/canonical-invalid)
- `canonical-broken` Canonical points to a URL that returns 4xx/5xx — [seo/canonical-broken](https://void-design.vercel.app/rules/seo/canonical-broken)
- `canonical-conflict` Canonical target redirects, is noindex, or canonicalises elsewhere — [seo/canonical-conflict](https://void-design.vercel.app/rules/seo/canonical-conflict)

Detected by `void seo` and `void audit`. Explain it in a terminal: `void rules seo/metadata-in-body`
