From c8d9f80d683d537a70e756538b450eb20b2dfacd Mon Sep 17 00:00:00 2001 From: sanath Date: Wed, 9 Sep 2026 11:28:59 +0530 Subject: [PATCH] Keep subpath sitemap root entries consistent --- .changeset/tidy-sitemap-root.md | 5 ++ packages/nimbus-docs/src/integration.ts | 15 ++++ .../test/integration-sitemap.test.ts | 79 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 .changeset/tidy-sitemap-root.md create mode 100644 packages/nimbus-docs/test/integration-sitemap.test.ts diff --git a/.changeset/tidy-sitemap-root.md b/.changeset/tidy-sitemap-root.md new file mode 100644 index 00000000..087fcef5 --- /dev/null +++ b/.changeset/tidy-sitemap-root.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/nimbus-docs": patch +--- + +Avoid duplicate root sitemap entries in directory builds deployed under a base path without a trailing slash. diff --git a/packages/nimbus-docs/src/integration.ts b/packages/nimbus-docs/src/integration.ts index 706be073..84c22242 100644 --- a/packages/nimbus-docs/src/integration.ts +++ b/packages/nimbus-docs/src/integration.ts @@ -986,6 +986,21 @@ export function nimbus( ), }), }); + const configureSitemap = sitemapIntegration.hooks["astro:config:done"]; + sitemapIntegration.hooks["astro:config:done"] = async (options) => { + const { config: astroConfig } = options; + // Sitemap joins the empty home-page path to base verbatim, but + // adds a slash for the same root's route in directory builds. + const base = + astroConfig.build.format === "directory" && + astroConfig.trailingSlash !== "never" + ? `${astroConfig.base.replace(/\/$/, "")}/` + : astroConfig.base; + await configureSitemap?.({ + ...options, + config: { ...astroConfig, base }, + }); + }; integrationsToAdd.push(sitemapIntegration); } diff --git a/packages/nimbus-docs/test/integration-sitemap.test.ts b/packages/nimbus-docs/test/integration-sitemap.test.ts new file mode 100644 index 00000000..e1a97157 --- /dev/null +++ b/packages/nimbus-docs/test/integration-sitemap.test.ts @@ -0,0 +1,79 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { test } from "node:test"; +import type { AstroIntegration } from "astro"; +import { nimbus } from "../src/integration.js"; + +for (const base of ["/", "/my-project", "/my-project/"]) { + test(`sitemap emits one root entry with base ${base}`, async () => { + const root = await mkdtemp(path.join(tmpdir(), "nimbus-sitemap-")); + const directory = pathToFileURL(root + path.sep); + const logger = { + warn() {}, + info() {}, + error(message: string) { + throw new Error(message); + }, + }; + const config = { + root: directory, + srcDir: new URL("src/", directory), + cacheDir: new URL(".cache/", directory), + site: "https://example.test", + base, + trailingSlash: "ignore", + build: { format: "directory" }, + }; + let integrations: AstroIntegration[] = []; + const integration = nimbus( + { site: config.site, title: "Test" }, + { + validateMdx: false, + admonitions: false, + markdown: { processor: {} as never }, + }, + ); + await integration.hooks["astro:config:setup"]!({ + config, + logger, + updateConfig(update: { integrations: AstroIntegration[] }) { + integrations = update.integrations; + return {}; + }, + } as never); + const sitemap = integrations.find( + (item) => item.name === "@astrojs/sitemap", + )!; + await sitemap.hooks["astro:config:done"]!({ config } as never); + assert.equal(config.base, base, "the app's base must remain unchanged"); + await sitemap.hooks["astro:routes:resolved"]!({ + routes: [ + { + type: "page", + pathname: "/", + generate: () => "/", + }, + ], + } as never); + await sitemap.hooks["astro:build:done"]!({ + dir: directory, + pages: [{ pathname: "" }, { pathname: "guide/" }], + logger, + } as never); + const xml = await readFile(new URL("sitemap-0.xml", directory), "utf8"); + const urls = [...xml.matchAll(/(.*?)<\/loc>/g)].map( + (match) => match[1], + ); + const prefix = base.replace(/\/$/, ""); + assert.deepEqual( + urls.sort(), + [ + `https://example.test${prefix}/`, + `https://example.test${prefix}/guide/`, + ].sort(), + ); + }); +}