From 849672601a0377d23197b1a7ce3ee68c828c96a6 Mon Sep 17 00:00:00 2001 From: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 08:57:24 -0400 Subject: [PATCH] fix(sitemap): drop redirect-only URLs in favour of their canonical targets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- .../fixed-387-sitemap-redirect-urls.md | 3 + src/__tests__/sitemap.test.ts | 86 +++++++++++-------- src/app/sitemap.ts | 61 ++++--------- 3 files changed, 69 insertions(+), 81 deletions(-) create mode 100644 changelog.d/fixed-387-sitemap-redirect-urls.md diff --git a/changelog.d/fixed-387-sitemap-redirect-urls.md b/changelog.d/fixed-387-sitemap-redirect-urls.md new file mode 100644 index 0000000..1b972dd --- /dev/null +++ b/changelog.d/fixed-387-sitemap-redirect-urls.md @@ -0,0 +1,3 @@ +- `sitemap.xml` no longer lists redirect-only URLs (`/`, `/en`, `/docs` and + `/docs/`); their priority moved to the canonical targets + (hivecommons/docs#387). diff --git a/src/__tests__/sitemap.test.ts b/src/__tests__/sitemap.test.ts index d20f362..c6870ea 100644 --- a/src/__tests__/sitemap.test.ts +++ b/src/__tests__/sitemap.test.ts @@ -5,34 +5,58 @@ import sitemap from "../app/sitemap"; const SITE_URL = "https://docs.hivecommons.dev"; +const PROJECT_IDS = [ + "hive", + "hotshot", + "pluk", + "rationguard", + "promptargs", + "dibs", + "spektacular", +]; +const REDIRECT_ONLY_URLS = [ + SITE_URL, + `${SITE_URL}/en`, + `${SITE_URL}/docs`, + ...PROJECT_IDS.map(id => `${SITE_URL}/docs/${id}`), +]; + describe("sitemap", () => { - it("uses absolute production URLs for marketing and docs landing routes", () => { + it("uses absolute production URLs for docs routes", () => { const urls = sitemap().map(entry => entry.url); - expect(urls).toContain(SITE_URL); - expect(urls).toContain(`${SITE_URL}/en`); - expect(urls).toContain(`${SITE_URL}/docs`); - expect( - urls.every(url => url.startsWith(`${SITE_URL}/`) || url === SITE_URL) - ).toBe(true); + expect(urls.every(url => url.startsWith(`${SITE_URL}/docs/`))).toBe(true); }); - it("includes project roots and markdown-backed docs routes", () => { + it("omits redirect-only URLs", () => { const urls = sitemap().map(entry => entry.url); + expect(urls).not.toEqual(expect.arrayContaining([SITE_URL])); + for (const url of REDIRECT_ONLY_URLS) { + expect(urls).not.toContain(url); + } + }); + + it("includes canonical targets with carried-over priorities and no duplicates", () => { + const entries = sitemap(); + const urls = entries.map(entry => entry.url); + const byUrl = new Map(entries.map(entry => [entry.url, entry])); + + expect( + byUrl.get(`${SITE_URL}/docs/community/what-is-hive-commons`)?.priority + ).toBe(1.0); + for (const project of PROJECT_IDS) { + expect( + byUrl.get(`${SITE_URL}/docs/${project}/overview/introduction`)?.priority + ).toBe(0.9); + } expect(urls).toEqual( expect.arrayContaining([ - `${SITE_URL}/docs/hive`, `${SITE_URL}/docs/hive/overview/architecture`, `${SITE_URL}/docs/hive/reference/architecture-decision-records/0001-record-architecture-decisions`, - `${SITE_URL}/docs/hotshot`, - `${SITE_URL}/docs/pluk`, - `${SITE_URL}/docs/rationguard`, - `${SITE_URL}/docs/promptargs`, - `${SITE_URL}/docs/dibs`, - `${SITE_URL}/docs/spektacular`, ]) ); + expect(new Set(urls).size).toBe(urls.length); }); it("includes shared general sections once and omits unlinked file-path routes", () => { @@ -89,10 +113,9 @@ describe("sitemap generated from fixture content", () => { const byUrl = new Map(entries.map(entry => [entry.url, entry])); const arch = byUrl.get(`${SITE_URL}/docs/hive/overview/architecture`); - expect(byUrl.get(SITE_URL)?.priority).toBe(1.0); - expect(byUrl.get(`${SITE_URL}/en`)?.changeFrequency).toBe("monthly"); - expect(byUrl.get(`${SITE_URL}/docs`)?.priority).toBe(0.9); - expect(byUrl.get(`${SITE_URL}/docs/hive`)?.priority).toBe(0.9); + for (const url of REDIRECT_ONLY_URLS) { + expect(byUrl.has(url)).toBe(false); + } expect(arch?.priority).toBe(0.7); expect(arch?.changeFrequency).toBe("weekly"); expect(arch?.lastModified).toEqual(fs.statSync(archPath).mtime); @@ -129,28 +152,15 @@ describe("sitemap generated from fixture content", () => { expect(stamp).toBeLessThanOrEqual(after); }); - it("omits generated routes for missing content while retaining project roots", () => { + it("omits generated routes and redirect-only URLs for missing content", () => { fixtureRoot = fs.mkdtempSync(path.join(realCwd, ".sitemap-empty-")); vi.spyOn(process, "cwd").mockReturnValue(fixtureRoot); - const entries = sitemap(); + const urls = sitemap().map(entry => entry.url); - expect(entries.some(entry => entry.url.includes("/docs/hive/"))).toBe( - false - ); - expect(entries.map(entry => entry.url)).toEqual( - expect.arrayContaining([ - SITE_URL, - `${SITE_URL}/en`, - `${SITE_URL}/docs`, - `${SITE_URL}/docs/hive`, - `${SITE_URL}/docs/hotshot`, - `${SITE_URL}/docs/pluk`, - `${SITE_URL}/docs/rationguard`, - `${SITE_URL}/docs/promptargs`, - `${SITE_URL}/docs/dibs`, - `${SITE_URL}/docs/spektacular`, - ]) - ); + expect(urls.some(url => url.includes("/docs/hive/"))).toBe(false); + for (const url of REDIRECT_ONLY_URLS) { + expect(urls).not.toContain(url); + } }); }); diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index eac6d34..3456644 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -8,18 +8,29 @@ const SITE_URL = "https://docs.hivecommons.dev"; /** Weekly update frequency for docs content */ const DOCS_CHANGE_FREQ = "weekly" as const; -/** Monthly update frequency for marketing pages */ -const MARKETING_CHANGE_FREQ = "monthly" as const; -/** Priority for the homepage */ +/** Priority for the effective site landing page */ const HOMEPAGE_PRIORITY = 1.0; -/** Priority for top-level marketing pages */ -const MARKETING_PRIORITY = 0.8; -/** Priority for docs landing / project root pages */ +/** Priority for project landing pages */ const DOCS_ROOT_PRIORITY = 0.9; /** Priority for individual docs pages */ const DOCS_PAGE_PRIORITY = 0.7; +/** + * `/`, `/en`, `/docs` and `/docs/` only redirect, so the canonical + * redirect targets carry their priority instead. + */ +const PRIORITY_OVERRIDES = new Map([ + [`${SITE_URL}/docs/community/what-is-hive-commons`, HOMEPAGE_PRIORITY], + ...Object.values(PROJECTS).map( + project => + [ + `${SITE_URL}/docs/${project.basePath}/overview/introduction`, + DOCS_ROOT_PRIORITY, + ] as [string, number] + ), +]); + type PageMapNode = { route?: string; children?: PageMapNode[]; @@ -54,34 +65,6 @@ export default function sitemap(): MetadataRoute.Sitemap { const entries: MetadataRoute.Sitemap = []; const seen = new Set(); - // --- Homepage --- - entries.push({ - url: SITE_URL, - lastModified: new Date(), - changeFrequency: MARKETING_CHANGE_FREQ, - priority: HOMEPAGE_PRIORITY, - }); - - // --- Marketing / locale pages --- - const marketingPages = ["/en"]; - - for (const page of marketingPages) { - entries.push({ - url: `${SITE_URL}${page}`, - lastModified: new Date(), - changeFrequency: MARKETING_CHANGE_FREQ, - priority: MARKETING_PRIORITY, - }); - } - - // --- Docs landing page --- - entries.push({ - url: `${SITE_URL}/docs`, - lastModified: new Date(), - changeFrequency: DOCS_CHANGE_FREQ, - priority: DOCS_ROOT_PRIORITY, - }); - // --- Project-specific docs --- const projectIds: ProjectId[] = [ "hive", @@ -94,14 +77,6 @@ export default function sitemap(): MetadataRoute.Sitemap { ]; for (const projectId of projectIds) { - // Add project root entry - entries.push({ - url: `${SITE_URL}/docs/${PROJECTS[projectId].basePath}`, - lastModified: new Date(), - changeFrequency: DOCS_CHANGE_FREQ, - priority: DOCS_ROOT_PRIORITY, - }); - const { pageMap, routeMap, contentPath } = buildPageMap(projectId); const projectPrefix = `/docs/${PROJECTS[projectId].basePath}/`; @@ -122,7 +97,7 @@ export default function sitemap(): MetadataRoute.Sitemap { path.join(isProjectRoute ? contentPath : docsContentPath, file) ), changeFrequency: DOCS_CHANGE_FREQ, - priority: DOCS_PAGE_PRIORITY, + priority: PRIORITY_OVERRIDES.get(url) ?? DOCS_PAGE_PRIORITY, }); } }