Google indexed Next.js pages with an empty title and no canonical
Summary
Telling Next.js to send Googlebot finished metadata did not guarantee that Google's stored copy of the page had it. Search Console's live test gave no warning, and the cause is still unconfirmed.
Sites that need the title and canonical in the head should stop metadata streaming for every visitor or make the metadata static. Then check the crawled page view of indexed URLs, not only the live test.
A r/TechSEO thread reports that Google stored two of three test pages from a new Next.js site with an empty <title> and no meta description, canonical or hreflang tags. Next.js is Vercel’s React framework. The poster’s team had already added Googlebot to htmlLimitedBots, the Next.js setting meant to stop metadata streaming for chosen crawlers. The third page came through correctly. Search Console’s live test showed the metadata present and correct.
What streaming metadata does
Next.js builds a page’s title and meta tags with a function called generateMetadata. According to the Next.js generateMetadata documentation, when the page can be prerendered and the function does nothing dynamic, the metadata lands in the initial HTML. Otherwise it “can be streamed after sending the initial UI.” The Reddit poster quotes the Next.js docs on where it goes: the tags are appended to the <body>, and Next.js says it has verified that bots which run JavaScript, such as Googlebot, read them correctly.
The poster was not convinced. Some tags are ignored outside the <head>, and Google renders JavaScript in a second wave, so the poster wanted them in the raw server HTML. The htmlLimitedBots documentation describes the escape hatch: user agents that match the setting get “blocking metadata,” meaning the server waits for the metadata and puts it in the head. The default list covers Google agents such as AdsBot-Google and Mediapartners-Google but not Googlebot itself. The poster reads that gap as intended, given the docs’ claim that Googlebot handles streamed tags.
Why the fix did not hold
The thread does not settle the cause. The poster offered three explanations: the two pages had not been through Google’s rendering pass yet, generateMetadata timed out, or the config was wrong. The poster judged the config unlikely, since one page worked.
A Next.js GitHub discussion the poster found describes a similar symptom on Next.js 14.2.35. The raw HTML carried an empty <title></title>, with the real value arriving later in the JavaScript payload. Community members there blamed a race between streaming and Googlebot’s rendering, and suggested prerendering routes. One Reddit commenter called it a known, acknowledged bug, but the discussion itself has no reply from a Next.js maintainer.
Setting htmlLimitedBots replaces the Next.js default list rather than adding to it, according to the docs. The thread does not show the poster’s regex, but a pattern containing only Googlebot would put Bingbot and the Google agents on the default list back on streaming.
The poster also reported what Search Console did with tags outside the head. Google honored a noindex in the body but did not pick up canonicals or hreflang there, based on URL Inspection results. Titles and descriptions were harder to confirm.
The safer course is to make head tags the same for every visitor, whichever crawler asks. Two commenters said much the same: one team dropped generateMetadata and wrote its own component for titles, descriptions, Open Graph, canonicals and hreflang, and another commenter advised skipping streaming and making metadata generation fast instead. A separate case study on Next.js SEO issues at a government jobs aggregator covers other ways the framework can trip up indexing.
What to do
- Check indexed URLs with “View Crawled Page” in URL Inspection. The live test showed correct metadata on the poster’s pages and still missed the problem.
- Turn off metadata streaming for everyone if head tags must be reliable. The htmlLimitedBots docs give this config to fully disable it:
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
htmlLimitedBots: /.*/,
}
export default config
- If you keep a custom bot list instead, copy the default patterns into your regex so you do not drop them. The htmlLimitedBots docs link to the full list.
- Move metadata that does not depend on the request into the static
metadataobject, which the generateMetadata docs recommend for that case.