The Next.js SEO Bug That Made Google Ignore My Entire Site (And How I Found It)
Source: Dev.to
Some Context
I’m not a developer. I like building things and trying new tools. SEO was always that thing I’d “figure out later.” Famous last words.
I know SEO at a very high level, but working in Marketing Performance I understand the importance of a well‑indexed website and the right keywords. I thought that was mostly it—research queries and build good content around them.
This time I had to figure it out!
As I was saying, I build things to learn my way through new tools and technologies.
The app is called MonkeyTravel. 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 it for myself, but this time I didn’t want it to end up in the massive Projects Graveyard.
The app itself worked great. People who found it loved it.
The problem? Nobody could find it.
I had to figure it out! And this is just the beginning of it.
Phase 1: “Why Isn’t Google Showing My Site?”
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.
My first instinct was to blame Google. “It takes time,” I told myself. I waited another week. Still zero.
That’s when I actually looked at my setup:
- ❌ Outdated sitemap
- ❌ No canonical tags
- ❌ No hreflang tags (despite 3 languages)
- ❌ Little structured data
- ❌ Default
robots.txtfromcreate-next-app - ❌ No meta descriptions on half the pages
Basically, I’d built a beautiful house and forgotten to put a number on the door. Let’s focus on the SEO instead of my poor decisions. 😄
Phase 2: The Foundations (Boring but Necessary)
I spent a weekend adding the basics. Nothing revolutionary, just what every site needs.
Sitemap
Next.js makes this easy with app/sitemap.ts. Mine generates URLs for all static pages, blog posts, and destination pages across all three locales. Dynamic content from Supabase gets included too.
// Simplified version of my sitemap
export default async function sitemap(): Promise {
const baseUrl = "https://monkeytravel.app";
const locales = ["en", "es", "it"];
// Blog posts × 3 languages
const blogSlugs = getAllSlugs();
const blogPages = blogSlugs.flatMap(slug =>
locales.map(locale => ({
url:
locale === "en"
? `${baseUrl}/blog/${slug}`
: `${baseUrl}/${locale}/blog/${slug}`,
changeFrequency: "monthly" as const,
priority: 0.7,
}))
);
return [...staticPages, ...blogPages, ...destinationPages];
}Canonical + Hreflang
Multilingual sites need both a canonical URL and a set of alternate language URLs. I used generateMetadata() in each page:
alternates: {
canonical:
locale === "en"
? `${BASE_URL}/${slug}`
: `${BASE_URL}/${locale}/${slug}`,
languages: {
en: `${BASE_URL}/${slug}`,
es: `${BASE_URL}/es/${slug}`,
it: `${BASE_URL}/it/${slug}`,
"x-default": `${BASE_URL}/${slug}`,
},
},Structured Data
JSON‑LD schemas for Organization, WebSite, SoftwareApplication, Article, and TouristDestination. I built a small utility for this:
export function jsonLdScriptProps(data: object) {
return {
type: "application/ld+json",
dangerouslySetInnerHTML: {
__html: JSON.stringify(data),
},
};
}Result
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.
After a week: 12 pages indexed. Progress, but painfully slow.
The real issue? Google Search Console doesn’t give much feedback on why a page is rejected.
Phase 3: Content That Google Actually Wants
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.
50 blog posts covering real travel topics, itinerary guides, destination comparisons, budget travel tips, seasonal recommendations.
× 3 languages = 150 blog pages.20 destination landing pages (Paris, Tokyo, Bali, Barcelona, etc.) with climate data, AI itinerary previews, and cross‑links to blog posts.
× 3 languages = 60 pages.5 SEO landing pages targeting specific search intents:
/free-ai-trip-planner,/group-trip-planner,/budget-trip-planner, etc.
What surprised me: internal linking mattered more than the content itself. Pages that were cross‑linked from multiple other pages got indexed way faster than orphan pages. I added:
- “From the Blog” sections on every landing page
- “Related destinations” on every destination page
- Blog ↔ destination links (both directions)
- A region filter on the blog index (Europe, Asia, Americas, Africa)
After two weeks: 78 pages indexed. The curve was accelerating.
Phase 4: The Bug That Almost Ruined Everything
Then Google Search Cons…
(The story continues…)
Duplicate without user‑selected canonical
Google was rejecting my homepage. It was choosing www.monkeytravel.app as canonical instead of monkeytravel.app, even though I had:
- 301 redirects from www → non‑www (both in middleware and Vercel config)
- Correct canonical tags in the HTML
- All URLs in the sitemap using non‑www
I double‑checked everything. The redirects worked, the HTML had the right tags, and curl confirmed it:
$ curl -s https://monkeytravel.app/ | grep canonicalSo why was Google saying “User‑declared canonical: None”?
The Discovery
I stared at this for hours before it clicked. The key was how I verified it.
curlwaits for the complete response.- Googlebot does not.
In Next.js 15.2+, generateMetadata() streams metadata asynchronously. The <link rel="canonical"> tags aren’t in the initial HTML payload; they’re injected via the stream after the body starts rendering. When Googlebot parses the initial response, the canonical tag literally doesn’t exist yet (or at least that’s what I concluded after digging through AI answers and docs).
I confirmed by looking at the raw initial HTML before streaming completes – there was no <link rel="canonical"> anywhere.
The Fix: One Config Option
// next.config.ts
const nextConfig: NextConfig = {
// Disable streaming for crawlers so the full HTML (including metadata)
// is sent synchronously.
htmlLimitedBots: /Googlebot|Google-InspectionTool|Bingbot|Yandex/i,
trailingSlash: false,
};
export default nextConfig;htmlLimitedBots tells Next.js: “When a crawler visits, disable streaming. Send the full HTML with all metadata synchronously.”
That single regex solved the whole problem.
I also changed my root‑layout canonical from "/" to "./" so every page gets a self‑referencing canonical instead of all pages pointing to the homepage (a subtle but important distinction).
After deploying and requesting re‑indexing, the results came quickly:
- 176 pages indexed within a few days.
- Still not all 230+ pages, but the trend is clear.
The Numbers
| Metric | Week 0 | Week 1 | Week 2 | Week 3 |
|---|---|---|---|---|
| Pages indexed | 0 | 12 | 78 | 176 |
| Total pages in sitemap | 0 | ~50 | ~225 | ~230 |
| Blog posts (per language) | 0 | 1 | 15 | 50 |
| Structured data schemas | 0 | 3 | 5 | 5 |
What I Got Wrong (So You Don’t Have To)
htmlLimitedBotsnot set from day 1 – Every Next.js project that cares about SEO should have this. Metadata streaming is silent; it looks fine when you check manually, but crawlers see a different picture.- Treating SEO as a “later” problem – Delaying sitemap and canonical tags wastes a week of potential crawling each time. Google’s queue doesn’t speed up because you’re impatient.
- Underestimating internal linking – Cross‑linked pages got indexed 3‑4× faster than isolated ones. Link related content wherever possible.
- Missing hreflang for multilingual support – Having three language versions without hreflang caused Google to treat them as duplicate content rather than translations.
AI Tricks That Helped
- AI‑assisted blog content – Drafted structures with AI, then edited and localized. For 50 posts × 3 languages, this saved months of manual work.
- Automated cross‑linking – Wrote a script that analyzed topics and suggested internal links, far more efficient than manual mapping.
- Prompt engineering for i18n – Instead of translating English articles, I asked the AI to write for the target audience (e.g., “Write about Paris for an Italian audience”), yielding more natural content.
What I’d Tell Past Me
Start with these on day 1, before writing a single feature:
htmlLimitedBotsinnext.config.ts- Sitemap generation
- Canonical tags on every page
- Submit the site to Google Search Console
Everything else—blog posts, structured data, internal linking—matters, but these four are the foundation. Skip them and nothing else works.
Update: 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. That will be FUN.
MonkeyTravel 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.