From 48c8edcd352081c0e401c885d588e945e0229bc1 Mon Sep 17 00:00:00 2001 From: mvm Date: Thu, 6 Aug 2026 12:08:36 -0500 Subject: [PATCH 1/2] [Routes] Eliminate route collision warnings from shadowed stubs, duplicate changelog slugs, RSS conflicts, and llms.txt dupes Filter content stubs with explicit src/pages routes from getDocsStaticPaths (ai/models, workers-ai/models, ruleset-engine/.../reference, waf/.../changelog). Deduplicate changelog post slugs by prefixing with product folder when entries from different folders share the same date-filename. Skip area RSS group slugs that collide with product IDs in [area].xml.ts getStaticPaths. Deduplicate llms.txt static paths by URL when multiple directory entries share the same entry.url (e.g. SDK variants). Results: - "Could not render" route conflict warnings: 9 -> 0 - No redirects needed (area RSS collision resolved by skipping colliding slugs) - Pages built: 8803 (was 8802) --- src/pages/[...product]/llms.txt.ts | 7 +++++++ src/pages/[...slug].astro | 17 +++++++++++++++- src/pages/changelog/rss/[area].xml.ts | 29 +++++++++++++++++---------- src/util/changelog.ts | 9 ++++++++- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/pages/[...product]/llms.txt.ts b/src/pages/[...product]/llms.txt.ts index 91a7183c069..b5a61b8d0b4 100644 --- a/src/pages/[...product]/llms.txt.ts +++ b/src/pages/[...product]/llms.txt.ts @@ -19,6 +19,10 @@ export const getStaticPaths = (async () => { const directory = await getCollection("directory"); const docs = await getCollection("docs"); + // Deduplicate by URL path: multiple directory entries may share the same + // entry.url (e.g. SDK variants). Keep only the first per URL. + const seen = new Set(); + return directory .map((entry) => { const productUrl = entry.data.entry?.url; @@ -31,6 +35,9 @@ export const getStaticPaths = (async () => { const urlPath = productUrl.slice(1, -1); if (!urlPath) return null; + if (seen.has(urlPath)) return null; + seen.add(urlPath); + const prefix = urlPath; const pages = docs.filter( (e) => diff --git a/src/pages/[...slug].astro b/src/pages/[...slug].astro index 83e79ee19e5..241375a46b0 100644 --- a/src/pages/[...slug].astro +++ b/src/pages/[...slug].astro @@ -22,7 +22,22 @@ import { const NOINDEX_PRODUCTS = ["email-security"]; export const prerender = true; -export const getStaticPaths = getDocsStaticPaths; +export const getStaticPaths = (async (...args) => { + const paths = await getDocsStaticPaths(...args); + // Content stubs that have a corresponding explicit src/pages route. + // The explicit page wins; filtering them here avoids duplicate-slug warnings. + const shadowed = new Set([ + "ai/models", + "workers-ai/models", + "ruleset-engine/rules-language/fields/reference", + "waf/change-log/changelog", + ]); + return paths.filter((p) => { + const slug = p.params.slug; + if (!slug) return true; + return !shadowed.has(slug); + }); +}) as typeof getDocsStaticPaths; const { entry, Content, headings } = await getDocsPageProps(Astro, { partialHeadings: { diff --git a/src/pages/changelog/rss/[area].xml.ts b/src/pages/changelog/rss/[area].xml.ts index 95d7274af73..6b8e38a420a 100644 --- a/src/pages/changelog/rss/[area].xml.ts +++ b/src/pages/changelog/rss/[area].xml.ts @@ -21,23 +21,30 @@ export const prerender = true; const slugifyArea = (value: string) => value.replaceAll(" ", "-").toLowerCase(); export const getStaticPaths = (async () => { - const products = await getCollection("directory", (e) => - Boolean(e.data.entry?.group), - ); + const [products, directory] = await Promise.all([ + getCollection("directory", (e) => Boolean(e.data.entry?.group)), + getCollection("directory"), + ]); + + // Product IDs that would collide with area slugs — the product route + // owns those URLs, so skip them here to avoid route conflicts. + const productIds = new Set(directory.map((e) => e.id)); const areas = Object.entries( Object.groupBy(products, (p) => p.data.entry!.group!), ); - return areas.map(([area, products]) => { - if (!products) - throw new Error(`[Changelog] No products attributed to "${area}"`); + return areas + .map(([area, products]) => { + if (!products) + throw new Error(`[Changelog] No products attributed to "${area}"`); - return { - params: { area: slugifyArea(area) }, - props: { title: area, products }, - }; - }); + return { + params: { area: slugifyArea(area) }, + props: { title: area, products }, + }; + }) + .filter((p) => !productIds.has(p.params.area)); }) satisfies GetStaticPaths; type Props = InferGetStaticPropsType; diff --git a/src/util/changelog.ts b/src/util/changelog.ts index 903e9c45160..4e95ca41272 100644 --- a/src/util/changelog.ts +++ b/src/util/changelog.ts @@ -108,6 +108,7 @@ export async function getChangelogs({ }: GetChangelogsOptions): Promise>> { let entries = await getCollection("changelog"); + const slugCounts = new Map(); entries = await Promise.all( entries.map(async (e) => { const slug = e.id.split("/").slice(1).join("/"); @@ -126,9 +127,15 @@ export async function getChangelogs({ e.data.products.push(product); } + // Deduplicate: when entries from different product folders share + // the same slug, prefix later occurrences with the folder name. + const count = slugCounts.get(slug) ?? 0; + slugCounts.set(slug, count + 1); + const dedupedId = count === 0 ? slug : `${folder}/${slug}`; + return { ...e, - id: slug, + id: dedupedId, }; }), ); From f71bf0c4a9857a6e1ff901ec2c656ba1063c9877 Mon Sep 17 00:00:00 2001 From: mvm Date: Fri, 7 Aug 2026 12:18:51 -0500 Subject: [PATCH 2/2] [Routes] Fix route collision semantics: preserve area RSS, canonical changelog owner, and generic llms.txt entry --- src/pages/[...product]/llms.txt.ts | 33 ++++++++++------ src/pages/changelog/rss/[area].xml.ts | 29 ++++++-------- src/pages/changelog/rss/[product].xml.ts | 21 ++++++++--- src/util/changelog.ts | 48 ++++++++++++++++++++---- 4 files changed, 88 insertions(+), 43 deletions(-) diff --git a/src/pages/[...product]/llms.txt.ts b/src/pages/[...product]/llms.txt.ts index b5a61b8d0b4..2c28ae2e31a 100644 --- a/src/pages/[...product]/llms.txt.ts +++ b/src/pages/[...product]/llms.txt.ts @@ -20,24 +20,35 @@ export const getStaticPaths = (async () => { const docs = await getCollection("docs"); // Deduplicate by URL path: multiple directory entries may share the same - // entry.url (e.g. SDK variants). Keep only the first per URL. - const seen = new Set(); + // entry.url (e.g. SDK variants). For shared URLs, prefer the generic + // entry (e.g. `sdk`) over language-specific variants (e.g. `go-sdk`). + const CANONICAL_IDS = new Set(["sdk"]); + + const entriesByUrl = new Map(); + for (const entry of directory) { + const productUrl = entry.data.entry?.url; + if (!productUrl || productUrl === "/" || productUrl.includes("#")) { + continue; + } + if (isDisallowedByRobots(productUrl)) continue; + + const urlPath = productUrl.slice(1, -1); + if (!urlPath) continue; - return directory + const existing = entriesByUrl.get(urlPath); + if (!existing || CANONICAL_IDS.has(entry.id)) { + entriesByUrl.set(urlPath, entry); + } + } + + return [...entriesByUrl.values()] .map((entry) => { const productUrl = entry.data.entry?.url; - if (!productUrl || productUrl === "/" || productUrl.includes("#")) { - return null; - } - - if (isDisallowedByRobots(productUrl)) return null; + if (!productUrl) return null; const urlPath = productUrl.slice(1, -1); if (!urlPath) return null; - if (seen.has(urlPath)) return null; - seen.add(urlPath); - const prefix = urlPath; const pages = docs.filter( (e) => diff --git a/src/pages/changelog/rss/[area].xml.ts b/src/pages/changelog/rss/[area].xml.ts index 6b8e38a420a..95d7274af73 100644 --- a/src/pages/changelog/rss/[area].xml.ts +++ b/src/pages/changelog/rss/[area].xml.ts @@ -21,30 +21,23 @@ export const prerender = true; const slugifyArea = (value: string) => value.replaceAll(" ", "-").toLowerCase(); export const getStaticPaths = (async () => { - const [products, directory] = await Promise.all([ - getCollection("directory", (e) => Boolean(e.data.entry?.group)), - getCollection("directory"), - ]); - - // Product IDs that would collide with area slugs — the product route - // owns those URLs, so skip them here to avoid route conflicts. - const productIds = new Set(directory.map((e) => e.id)); + const products = await getCollection("directory", (e) => + Boolean(e.data.entry?.group), + ); const areas = Object.entries( Object.groupBy(products, (p) => p.data.entry!.group!), ); - return areas - .map(([area, products]) => { - if (!products) - throw new Error(`[Changelog] No products attributed to "${area}"`); + return areas.map(([area, products]) => { + if (!products) + throw new Error(`[Changelog] No products attributed to "${area}"`); - return { - params: { area: slugifyArea(area) }, - props: { title: area, products }, - }; - }) - .filter((p) => !productIds.has(p.params.area)); + return { + params: { area: slugifyArea(area) }, + props: { title: area, products }, + }; + }); }) satisfies GetStaticPaths; type Props = InferGetStaticPropsType; diff --git a/src/pages/changelog/rss/[product].xml.ts b/src/pages/changelog/rss/[product].xml.ts index 3875c3557d9..40f239821e8 100644 --- a/src/pages/changelog/rss/[product].xml.ts +++ b/src/pages/changelog/rss/[product].xml.ts @@ -6,6 +6,7 @@ import rss from "@astrojs/rss"; import { getCollection } from "astro:content"; import { config } from "virtual:nimbus/config"; import { getChangelogs, getRSSItems } from "~/util/changelog"; +import { groups } from "~/util/directory"; import type { APIRoute, @@ -16,15 +17,23 @@ import type { export const prerender = true; +const slugifyArea = (value: string) => value.replaceAll(" ", "-").toLowerCase(); + export const getStaticPaths = (async () => { const directory = await getCollection("directory"); - return directory.map((entry) => { - return { - params: { product: entry.id }, - props: { product: entry }, - }; - }); + // Area group slugs that would collide with product IDs — the area + // route owns those URLs, so skip them here to avoid route conflicts. + const areaSlugs = new Set(groups.map(slugifyArea)); + + return directory + .filter((entry) => !areaSlugs.has(entry.id)) + .map((entry) => { + return { + params: { product: entry.id }, + props: { product: entry }, + }; + }); }) satisfies GetStaticPaths; type Props = InferGetStaticPropsType; diff --git a/src/util/changelog.ts b/src/util/changelog.ts index 4e95ca41272..8ddfe690d7e 100644 --- a/src/util/changelog.ts +++ b/src/util/changelog.ts @@ -108,11 +108,30 @@ export async function getChangelogs({ }: GetChangelogsOptions): Promise>> { let entries = await getCollection("changelog"); + // First pass: extract slug + folder for every entry (synchronous, + // deterministic — collection order is alphabetical by file path). + const parsed = entries.map((e) => { + const slug = e.id.split("/").slice(1).join("/"); + const folder = e.id.split("/")[0]; + return { entry: e, slug, folder }; + }); + + // Identify which slugs appear more than once. const slugCounts = new Map(); + for (const { slug } of parsed) { + slugCounts.set(slug, (slugCounts.get(slug) ?? 0) + 1); + } + + // For duplicate slugs, the first folder in alphabetical order owns the + // bare slug; later folders are prefixed. This is deterministic because + // `getCollection` returns entries in alphabetical file-path order. + // Explicit overrides can force a specific canonical owner. + const CANONICAL_OWNERS: Record = { + "2026-04-01-l4-transport-telemetry-fields": "workers", + }; + entries = await Promise.all( - entries.map(async (e) => { - const slug = e.id.split("/").slice(1).join("/"); - const folder = e.id.split("/")[0]; + parsed.map(async ({ entry: e, slug, folder }) => { const product = { collection: "directory", id: folder } as const; const isValidProduct = await getEntry(product); @@ -127,11 +146,24 @@ export async function getChangelogs({ e.data.products.push(product); } - // Deduplicate: when entries from different product folders share - // the same slug, prefix later occurrences with the folder name. - const count = slugCounts.get(slug) ?? 0; - slugCounts.set(slug, count + 1); - const dedupedId = count === 0 ? slug : `${folder}/${slug}`; + const count = slugCounts.get(slug) ?? 1; + const canonicalOwner = CANONICAL_OWNERS[slug]; + let dedupedId: string; + + if (count <= 1) { + dedupedId = slug; + } else if (canonicalOwner && folder === canonicalOwner) { + dedupedId = slug; + } else if (canonicalOwner) { + dedupedId = `${folder}/${slug}`; + } else { + // No explicit owner: first folder alphabetically wins. + const folders = parsed + .filter((p) => p.slug === slug) + .map((p) => p.folder); + const firstFolder = folders.sort()[0]; + dedupedId = folder === firstFolder ? slug : `${folder}/${slug}`; + } return { ...e,