Skip to content

fix(build): sitemap emits served route for index pages, not /index - #12

Merged
nc9 merged 1 commit into
mainfrom
fix/sitemap-index-canonical-urls
Jul 17, 2026
Merged

fix(build): sitemap emits served route for index pages, not /index#12
nc9 merged 1 commit into
mainfrom
fix/sitemap-index-canonical-urls

Conversation

@nc9

@nc9 nc9 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

The defect

Generated sitemap.xml publishes /index for a root index.mdx. That URL 307s to /, so Google classifies it as "Page with redirect" and excludes it: wasted crawl budget plus a duplicate signal against the real home page.

Redirects and canonicals were already correct. The defect was isolated to the build-time URL emitters, which derived URLs from the source file path instead of the served route.

Live evidence (docs.squirrelscan.com, a Tangly-built site)

Request Result
sitemap.xml contains <loc>https://docs.squirrelscan.com/index</loc>
GET /index 307/
GET / 200, <link rel="canonical" href="https://docs.squirrelscan.com/">

Root cause

scanPages derives slugs from the file path and collapses index with .replace(/\/index$/, ""). That regex needs a leading slash, so nested cli/index.mdx correctly collapses to cli, but a root index.mdx slugifies to the bare string index. It renders at / and is only also routable at /index.

Seo.astro already special-cased this for the canonical tag. The sitemap/llms emitters did not, and there was no shared helper keeping the two in sync.

The fix

Add pageRouteForSlug / pagePathForSlug to @tanglydocs/schema — the shared leaf both tangly and the themes depend on, exposed via a ./page-path subpath (same rationale as resolve-site: themes must not import tangly, and the subpath keeps zod out of theme bundles).

Route every published URL through it, so the sitemap can't drift from the canonical again:

  • generateSitemap
  • generateLlmsTxt / generateLlmsFullTxt — same bug, also linked /index
  • writePageMarkdown's URL: preamble — same bug
  • Seo.astro's canonical — replaces its inlined special case

The helper also collapses a trailing /index itself. That duplicates what scanPages already does to nested slugs, but means any caller holding a raw file-derived slug resolves the same route rather than depending on an upstream regex.

Not changed: og card filenames stay slug-keyed (dist/og/<slug>.png, home → index.png). That's a filename, deliberately the inverse mapping of a route, and routing it through the helper would have pointed og:image at a non-existent file. Verified /og/index.png still emits and resolves.

Trailing-slash decision

No trailing slash, matching the existing Seo.astro behavior exactly (.replace(/\/+$/, "") || "/") rather than inventing a convention. Root → /; under --base /docs, home → /docs (not /docs/). The emitted sitemap entry is now byte-identical to the canonical tag.

Verification

Built a fixture with both a root index.mdx and a subdirectory cli/index.mdx, at the pre-fix and post-fix commits:

# pre-fix                          # post-fix
<loc>https://…/index</loc>         <loc>https://…/</loc>
<loc>https://…/cli</loc>           <loc>https://…/cli</loc>
- [Home](/index)                   - [Home](/)
URL: /index   (index.md)           URL: /   (index.md)

Post-fix canonical on / is https://docs.example.com/ — an exact match with the sitemap entry.

Note on the subdirectory case

Worth recording: subdirectory index.mdx was never actually broken in the sitemap. scanPages already collapses cli/indexcli, and the pre-fix build confirms it emitted /cli correctly. The GET /cli/index → 307 you can observe live is just Astro also routing that path; the sitemap never contained it. Only the root case was ever wrong. Tests cover both regardless, to pin the behavior.

Tests

  • packages/schema/src/page-path.test.ts — root, nested, base-prefixed, stray slashes, and index appearing inside a segment name (/indexing must not be stripped).
  • packages/tangly/src/build-outputs/index.test.ts — fixture gains root index.mdx + subdirectory cli/index.mdx; asserts sitemap and llms.txt emit served routes, with and without a base prefix.

bun run ci green (format, lint, build, typecheck, tests — 22 lint warnings all pre-existing, 0 errors).

Root index.mdx renders at `/` but slugifies to the bare string `index`
(scan-pages' collapse regex needs a leading `/`, so only nested
foo/index.mdx collapses). The sitemap/llms.txt emitters derived URLs
straight from the slug, publishing `/index` — which 307s to `/`. Google
classifies redirecting sitemap URLs as "Page with redirect" and drops
them.

Add pageRouteForSlug/pagePathForSlug to @tanglydocs/schema (the shared
leaf, same rationale as resolve-site) and route sitemap, llms.txt,
llms-full.txt, agent .md preambles and Seo.astro's canonical through it,
so published URLs can't diverge from the canonical again.

og card filenames stay slug-keyed (dist/og/<slug>.png, home =>
index.png) — deliberately the inverse mapping, kept separate.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
tangly-docs 21b0e3c Commit Preview URL

Branch Preview URL
Jul 17 2026, 06:58 AM

@nc9
nc9 merged commit 5a7ae97 into main Jul 17, 2026
18 checks passed
@nc9
nc9 deleted the fix/sitemap-index-canonical-urls branch July 17, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant