nlqdb

Blog ·

Your sitemap is advertising redirects — and your canonical tag points at one

A static host that serves route/index.html 307-redirects the bare path. Our sitemap advertised 27 redirecting URLs and every canonical tag pointed at one. The fix is one path-normalize helper.

Run curl -sI over the URLs in your own sitemap. We did, and 27 of ours answered 307 Temporary Redirect — every route on the site. The canonical tag was worse: each page declared a canonical URL that redirected to the page itself. We were telling crawlers, on every single page, that the authoritative copy lives one hop away.

How a static host quietly forks every URL in two

The mechanics are mundane, which is why this survives review. A static build writes each route as route/index.html. The host serves /route/ directly — and answers the bare /route with a 307 to the slash form. So every route on the site has two URLs: one real, one a redirect. Which one your HTML advertises depends on whoever typed the link.

Our templates typed the bare form everywhere it matters most: the canonical tag, og:url, the sitemap generator, and llms.txt — the four surfaces whose entire job is to name the authoritative URL. All four advertised the redirect.

Why a self-referential redirecting canonical is worth fixing

  • A canonical pointing at a redirect is a mixed signal. The tag exists to end URL ambiguity; making crawlers resolve a hop to find the 'canonical' copy reintroduces exactly the ambiguity it was meant to close.
  • Sitemap URLs are supposed to be final. Search engines tolerate redirects there, but every hop is a chance for a URL to get indexed in the form you didn't pick — and split whatever authority the page earns across two spellings.
  • AI crawlers fetch exactly what you wrote. llms.txt consumers and answer-engine bots are literal; an extra 307 on every fetch is latency you chose, on the surface built for machines.

The fix is a policy plus one helper, not a link hunt

Chasing individual links is the wrong altitude. The durable fix was three moves: pick the slash form as policy (trailingSlash: "always", so the build itself rejects the other spelling); route every emitted URL — canonical, og:url, sitemap, llms.txt — through one path-normalize helper in the head layout and URL generators; and re-audit with curl -sI over every sitemap URL, expecting nothing but 200s.

The one-helper rule is what made the follow-up cheap: once advertised URLs were normalized in one place, sweeping the remaining 1,100+ bare-path hrefs inside page bodies was a mechanical pass, and a link check in the build now fails on any regression. (nlqdb is the database you talk to; this is one of the engineering notes from building it in the open.)

The general lesson: every surface that names a URL — canonical, og:url, sitemap, llms.txt, internal hrefs — should call the same normalize function. If two of them can disagree about your own address, they eventually will, and you'll advertise the disagreement to every crawler that visits.

Try nlqdb in 30 seconds

No sign-in. The anonymous database lasts 72 hours; adopt it with one click if you keep it.

Start with a goal →

More posts: browse the blog.