Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/fixed-387-sitemap-redirect-urls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- `sitemap.xml` no longer lists redirect-only URLs (`/`, `/en`, `/docs` and
`/docs/<project>`); their priority moved to the canonical targets
(hivecommons/docs#387).
86 changes: 48 additions & 38 deletions src/__tests__/sitemap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
});
});
61 changes: 18 additions & 43 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<project>` only redirect, so the canonical
* redirect targets carry their priority instead.
*/
const PRIORITY_OVERRIDES = new Map<string, number>([
[`${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[];
Expand Down Expand Up @@ -54,34 +65,6 @@ export default function sitemap(): MetadataRoute.Sitemap {
const entries: MetadataRoute.Sitemap = [];
const seen = new Set<string>();

// --- 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",
Expand All @@ -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}/`;

Expand All @@ -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,
});
}
}
Expand Down
Loading