<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Federico Sciuca</title>
    <description>The latest articles on DEV Community by Federico Sciuca (@federico_sciuca).</description>
    <link>https://dev.to/federico_sciuca</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810845%2F64ebcf19-7890-47e4-8951-f46627f989fa.jpeg</url>
      <title>DEV Community: Federico Sciuca</title>
      <link>https://dev.to/federico_sciuca</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/federico_sciuca"/>
    <language>en</language>
    <item>
      <title>The Next.js SEO Bug That Made Google Ignore My Entire Site (And How I Found It)</title>
      <dc:creator>Federico Sciuca</dc:creator>
      <pubDate>Sat, 07 Mar 2026 03:51:49 +0000</pubDate>
      <link>https://dev.to/federico_sciuca/the-nextjs-seo-bug-that-made-google-ignore-my-entire-site-and-how-i-found-it-2mg0</link>
      <guid>https://dev.to/federico_sciuca/the-nextjs-seo-bug-that-made-google-ignore-my-entire-site-and-how-i-found-it-2mg0</guid>
      <description>&lt;p&gt;I shipped a full-featured AI travel planner. Three languages. 230+ pages. Then I realised that Google couldn't find a single one.&lt;/p&gt;

&lt;p&gt;This is the story of how I went from zero indexed pages to 176 in three weeks and the one Next.js configuration line that changed everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Context
&lt;/h2&gt;

&lt;p&gt;I'm not a developer. I like building things and try new tools. SEO was always that thing I'd "figure out later". Famous last words.&lt;br&gt;
I know SEO at a very high level but working into Marketing Performance I know the importance of a well indexed website and of the keywords but I thoughts that was mostly it. Research queries and build good content around them.&lt;/p&gt;

&lt;p&gt;This time was the time I would have to "figure it out!".&lt;/p&gt;

&lt;p&gt;As I was saying, I build things to learn my way through new tools and technologies.&lt;br&gt;
The app is called &lt;a href="https://monkeytravel.app" rel="noopener noreferrer"&gt;MonkeyTravel&lt;/a&gt;. It uses AI to generate personalised travel itineraries — day-by-day plans with activities, restaurants, hotels, and budget breakdowns. It works in English, Spanish, and Italian. I built it because planning group trips with friends was always chaos, and I wanted something smarter. As it usually happens, I built for myself, but this time I didn't want to send it to the massive Projects Graveyard.&lt;/p&gt;

&lt;p&gt;The app itself worked great. People who found it loved it.&lt;/p&gt;

&lt;p&gt;The problem? Nobody could find it.&lt;br&gt;
I had to figure it out! And this is just the beginning of it!&lt;/p&gt;
&lt;h2&gt;
  
  
  Phase 1: "Why Isn't Google Showing My Site?"
&lt;/h2&gt;

&lt;p&gt;I'll be honest, when I first checked Google Search Console, I expected to see... something. I'd been live for weeks. Instead: a flat line. Zero impressions. Zero clicks. Zero-indexed pages.&lt;/p&gt;

&lt;p&gt;My first instinct was to blame Google. "It takes time," I told myself. So I waited another week. Still zero.&lt;/p&gt;

&lt;p&gt;That's when I actually looked at my setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Outdated sitemap&lt;/li&gt;
&lt;li&gt;❌ No canonical tags&lt;/li&gt;
&lt;li&gt;❌ No hreflang tags (despite 3 languages)&lt;/li&gt;
&lt;li&gt;❌ Little structured data&lt;/li&gt;
&lt;li&gt;❌ Default robots.txt from &lt;code&gt;create-next-app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;❌ No meta descriptions on half the pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Basically, I'd built a beautiful house and forgotten to put a number on the door. I did the same on my mailbox recently, and I was surprised that the Postman didn't deliver my fresh new American driving license! But let's focus on the SEO instead of my poor decisions :D.&lt;/p&gt;
&lt;h2&gt;
  
  
  Phase 2: The Foundations (Boring but Necessary)
&lt;/h2&gt;

&lt;p&gt;I spent a weekend adding the basics. Nothing revolutionary, just what every site needs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sitemap:&lt;/strong&gt; Next.js makes this easy with &lt;code&gt;app/sitemap.ts&lt;/code&gt;. Mine generates URLs for all static pages, blog posts, and destination pages across all 3 locales. Dynamic content from Supabase gets included too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified version of my sitemap&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MetadataRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sitemap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://monkeytravel.app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="c1"&gt;// Blog posts × 3 languages&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogSlugs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAllSlugs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blogPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;blogSlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;changeFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;staticPages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;blogPages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;destinationPages&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Canonical + Hreflang:&lt;/strong&gt; This is where multilingual sites get tricky. Every page needs to say "I'm the official URL" AND "here are my other language versions." I used &lt;code&gt;generateMetadata()&lt;/code&gt; in each page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/es/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/it/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Structured Data:&lt;/strong&gt; JSON-LD schemas for Organization, WebSite, SoftwareApplication, Article, and TouristDestination. I built a small utility for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;jsonLdScriptProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/ld+json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; After submitting the sitemap, Google discovered all my URLs within 48 hours. But "discovered" ≠ "indexed." Most pages sat in the "Discovered — currently not indexed" queue.&lt;/p&gt;

&lt;p&gt;After a week: &lt;strong&gt;12 pages indexed.&lt;/strong&gt; Progress, but painfully slow.&lt;/p&gt;

&lt;p&gt;The real issue? Google Search Console doesn't look like it is giving much feedback or reasoning around the rejection of a page!&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3: Content That Google Actually Wants
&lt;/h2&gt;

&lt;p&gt;I realised my site was mostly an app behind a login wall. Google had very little public content to index. So I went aggressive on content:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;50 blog posts&lt;/strong&gt; covering real travel topics, itinerary guides, destination comparisons, budget travel tips, seasonal recommendations. Each one in 3 languages = 150 blog pages. Tedious process, but facilitated A LOT by the whole engine that the app is actually built on!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;20 destination landing pages&lt;/strong&gt; (Paris, Tokyo, Bali, Barcelona, etc.) with climate data, AI itinerary previews, and cross-links to blog posts. × 3 languages = 60 pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5 SEO landing pages&lt;/strong&gt; targeting specific search intents: &lt;code&gt;/free-ai-trip-planner&lt;/code&gt;, &lt;code&gt;/group-trip-planner&lt;/code&gt;, &lt;code&gt;/budget-trip-planner&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;But here's the thing that surprised me: &lt;strong&gt;internal linking mattered more than the content itself.&lt;/strong&gt; Pages that were cross-linked from multiple other pages got indexed WAY faster than orphan pages. I added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"From the Blog" sections on every landing page&lt;/li&gt;
&lt;li&gt;"Related destinations" on every destination page&lt;/li&gt;
&lt;li&gt;Blog → destination links and destination → blog links&lt;/li&gt;
&lt;li&gt;A region filter on the blog index (Europe, Asia, Americas, Africa)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After two weeks: &lt;strong&gt;78 pages indexed.&lt;/strong&gt; The curve was accelerating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 4: The Bug That Almost Ruined Everything
&lt;/h2&gt;

&lt;p&gt;Then Google Search Console showed a new error on my homepage:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Duplicate without user-selected canonical"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Google was rejecting my homepage. It was choosing &lt;code&gt;www.monkeytravel.app&lt;/code&gt; as canonical instead of &lt;code&gt;monkeytravel.app&lt;/code&gt;. Despite having:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;301 redirects from www → non-www (in both middleware AND Vercel config)&lt;/li&gt;
&lt;li&gt;Correct canonical tags in the HTML&lt;/li&gt;
&lt;li&gt;All URLs in the sitemap using non-www&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I checked everything twice. The redirects worked. The HTML had the right tags. I verified with curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://monkeytravel.app/ | &lt;span class="nb"&gt;grep &lt;/span&gt;canonical
&amp;lt;&lt;span class="nb"&gt;link &lt;/span&gt;&lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"canonical"&lt;/span&gt; &lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://monkeytravel.app"&lt;/span&gt;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tag was right there. So why was Google saying "User-declared canonical: &lt;strong&gt;None&lt;/strong&gt;"?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery
&lt;/h2&gt;

&lt;p&gt;I stared at this for hours before it clicked. The key was in how I verified it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; waits for the complete response. Googlebot doesn't.&lt;/p&gt;

&lt;p&gt;In Next.js 15.2+, &lt;code&gt;generateMetadata()&lt;/code&gt; streams metadata asynchronously. The &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tags aren't in the initial HTML payload but they're injected via the stream after the body starts rendering. When Googlebot parses the initial response, &lt;strong&gt;the canonical tag literally doesn't exist yet.&lt;/strong&gt; Or at least this is what I think I figured out jumping between AI, documentations etc.&lt;/p&gt;

&lt;p&gt;I confirmed by looking at the raw initial HTML before streaming completes: no &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt; anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: One Config Option
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;htmlLimitedBots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/Googlebot|Google-InspectionTool|Bingbot|Yandex/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;trailingSlash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;htmlLimitedBots&lt;/code&gt; tells Next.js: "When a crawler visits, disable streaming. Send the full HTML with all metadata synchronously."&lt;/p&gt;

&lt;p&gt;That's it. One regex. Fixed the entire problem.&lt;/p&gt;

&lt;p&gt;I also changed my root layout canonical from &lt;code&gt;"/"&lt;/code&gt; to &lt;code&gt;"./"&lt;/code&gt; so every page gets a self-referencing canonical instead of all pages pointing to the homepage (a subtle but important distinction).&lt;/p&gt;

&lt;p&gt;Deployed. Requested re-indexing. Within days: &lt;strong&gt;176 pages indexed.&lt;/strong&gt;&lt;br&gt;
Still not all the 230+ pages but we are getting there!&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Week 0&lt;/th&gt;
&lt;th&gt;Week 1&lt;/th&gt;
&lt;th&gt;Week 2&lt;/th&gt;
&lt;th&gt;Week 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pages indexed&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;176&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total pages in sitemap&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~50&lt;/td&gt;
&lt;td&gt;~225&lt;/td&gt;
&lt;td&gt;~230&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blog posts (per language)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured data schemas&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I Got Wrong (So You Don't Have To)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. I didn't set up &lt;code&gt;htmlLimitedBots&lt;/code&gt; from day one.&lt;/strong&gt; This should be in every Next.js project that cares about SEO. The metadata streaming issue is completely silent, everything looks fine when you check manually. Only crawlers are affected. I thought it was a "content volume" issue... not really!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. I treated SEO as a "later" problem.&lt;/strong&gt; Every week I delayed the sitemap and canonical tags was a week of potential crawling wasted. Google's queue doesn't move faster just because you're impatient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. I underestimated internal linking.&lt;/strong&gt; Cross-linked pages got indexed 3-4x faster than isolated pages. If you have related content, link it. Google follows links.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. I built multilingual support but forgot hreflang.&lt;/strong&gt; Having 3 language versions without hreflang means Google might treat them as duplicate content instead of translations. Costly mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Tricks That Helped
&lt;/h2&gt;

&lt;p&gt;A few things that saved me time during this sprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI-assisted blog content:&lt;/strong&gt; I used AI to draft blog post structures, then edited and localized them. For 50 posts × 3 languages, doing everything manually would have taken months and study an extra language or find more international collaborators.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated cross-linking:&lt;/strong&gt; I wrote a script that analyzed blog post topics and destination pages, then generated internal link suggestions. Much better than trying to mentally map 200+ pages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prompt engineering for i18n:&lt;/strong&gt; Instead of translating English content, I had the AI generate locale-native content. "Write about Paris for an Italian audience" produces much better content than "translate this Paris article to Italian."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd Tell Past Me
&lt;/h2&gt;

&lt;p&gt;Start with these on day one, before you write a single feature:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;htmlLimitedBots&lt;/code&gt; in next.config.ts&lt;/li&gt;
&lt;li&gt;Sitemap generation&lt;/li&gt;
&lt;li&gt;Canonical tags on every page&lt;/li&gt;
&lt;li&gt;Submit to Google Search Console&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else, blog posts, structured data, internal linking, matters, but these four things are the foundation. Skip them and nothing else works.&lt;/p&gt;

&lt;p&gt;129 pages are still in Google's queue. Based on the trajectory, they'll be indexed within a couple of weeks (hopefully). Then the real game starts: actually ranking for competitive keywords.&lt;br&gt;
That will be FUN&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://monkeytravel.app" rel="noopener noreferrer"&gt;MonkeyTravel&lt;/a&gt; is free to use — drop a destination, get a personalised AI itinerary in seconds. Built with Next.js, Supabase, and hosted on Vercel. Any feedback is more than welcome! Let's learn something new together&lt;/em&gt;&lt;/p&gt;




</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
