Give Google a canonical it can read, and stop redirects outvoting it

Fixkamo-marketing
Shipped
September 10, 2026 at 7:17 PM UTC
Author
Kamo
Commit
301ad72

Search Console began excluding pages as "Duplicate without user-selected canonical", "Duplicate, Google chose different canonical than user" and "Alternate page with proper canonical tag". Measured against production, four separate things fed those. Metadata in the body. Next 16 streams generateMetadata() into <body> on a dynamic route for any user agent outside htmlLimitedBots, and its built-in list leaves out Googlebot. The knowledge-base routes are the dynamic ones: /help-center, /kamo-documentation, /kamo-api-reference and every article under them, in 22 locales, 308 articles in the sitemap alone. Fetched as Googlebot, each carried its <title>, canonical and hreflang ~100 KB after </head>, and Google does not accept a canonical outside <head>, so those pages had none. URL Inspection never showed it: its Google-InspectionTool agent is on the list, so the live test got the head Googlebot did not. htmlLimitedBots is now /.*/ and every client gets metadata in <head>. The prerendered routes resolve metadata at build time and do not change. Untranslated articles claiming a language. The KB service answers a locale it has no translation for with the English article, and the backfill is per article: /ja/help-center/crm/accounts/ what-an-account-record-holds is the English page word for word, while its /fr twin is French. It declared itself canonical and Japanese, and Google overruled it in favour of /en. An article the service returned in English now points its canonical at the English original, the treatment app/lib/seo.ts already gives an untranslated locale. Temporary redirects. next-intl sends /, every unprefixed path and every miscased one (/EN/pricing) to a locale with a 307, which tells Google to keep the source URL, so it kept choosing / over /en. The redirect into the default locale, the one a crawler with no cookie or Accept-Language receives, is now a 308, with Cache-Control: no-store so a browser does not hold it past a later language change. Redirects into any other locale stay 307; they depend on who is asking. NEXT_LOCALE and the referral cookies still ride the redirect. The apex host. og:url on the homepage in all 22 locales, and on every 404, was https://kamocrm.com, inherited from the locale layout, beside a canonical naming www. It is gone from the layout; pages with a canonical already set og:url to it. The Organization, WebSite, SoftwareApplication and BreadcrumbList markup, the RSS feed and /api/sitemap also named the apex, which 308s to www and then redirected again into /en. They now use www and the locale's canonical path. BreadcrumbJsonLd takes the locale as a prop rather than calling getLocale(): 25 of its 31 callers never call setRequestLocale, where getLocale() reads request headers, and a build with it prerendered 179 routes instead of 188. "Alternate page with proper canonical tag" is Google confirming a canonical that works: /ta/* (untranslated, canonical to /en), the ?discount=, ?holiday= and ?self= variants, and now the untranslated KB articles. Those stay as they are. Verified: tsc clean; a production build prerenders 188 routes with all six static-route sentinels intact, and the built /en, /en/about, /en/pricing and /en/features carry no apex URL and their canonical inside <head>. Driven through the middleware as NextRequests: /, /pricing, /EN/pricing and /?discount=<guid> answer 308 into /en with no-store and set NEXT_LOCALE, kdsc_vid and kdsc_src; Accept-Language fr and NEXT_LOCALE=de still get a 307 to their own locale. check-discount-cookie, check-register-links, check-product-offers and the other guards pass.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing