Skip to content
Open
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
30 changes: 24 additions & 6 deletions src/pages/[...product]/llms.txt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,32 @@ export const getStaticPaths = (async () => {
const directory = await getCollection("directory");
const docs = await getCollection("docs");

return directory
// Deduplicate by URL path: multiple directory entries may share the same
// 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<string, (typeof directory)[number]>();
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;

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;
Expand Down
17 changes: 16 additions & 1 deletion src/pages/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
21 changes: 15 additions & 6 deletions src/pages/changelog/rss/[product].xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<typeof getStaticPaths>;
Expand Down
47 changes: 43 additions & 4 deletions src/util/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,30 @@ export async function getChangelogs({
}: GetChangelogsOptions): Promise<Array<CollectionEntry<"changelog">>> {
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<string, number>();
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<string, string> = {
"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);
Expand All @@ -126,9 +146,28 @@ export async function getChangelogs({
e.data.products.push(product);
}

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,
id: slug,
id: dedupedId,
};
}),
);
Expand Down