From 7c10b66928e3c763236d7e731fd82c16d42b6403 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Tue, 29 Sep 2026 09:59:33 -0400 Subject: [PATCH 01/10] GRO-740 make layout dynamic --- ...DefaultNewLayout.astro => default-new.astro} | 0 .../{DefaultLayout.astro => default.astro} | 0 src/layouts/{OldDocsLayout.astro => docs.astro} | 2 +- src/layouts/{ErrorLayout.astro => error.astro} | 0 .../{NewDocsLayout.astro => new-docs.astro} | 2 +- src/pages/docs/404.astro | 2 +- src/pages/docs/[version]/[...slug].astro | 17 ++++++++++++++--- src/pages/docs/all.astro | 2 +- 8 files changed, 18 insertions(+), 7 deletions(-) rename src/layouts/{DefaultNewLayout.astro => default-new.astro} (100%) rename src/layouts/{DefaultLayout.astro => default.astro} (100%) rename src/layouts/{OldDocsLayout.astro => docs.astro} (97%) rename src/layouts/{ErrorLayout.astro => error.astro} (100%) rename src/layouts/{NewDocsLayout.astro => new-docs.astro} (98%) diff --git a/src/layouts/DefaultNewLayout.astro b/src/layouts/default-new.astro similarity index 100% rename from src/layouts/DefaultNewLayout.astro rename to src/layouts/default-new.astro diff --git a/src/layouts/DefaultLayout.astro b/src/layouts/default.astro similarity index 100% rename from src/layouts/DefaultLayout.astro rename to src/layouts/default.astro diff --git a/src/layouts/OldDocsLayout.astro b/src/layouts/docs.astro similarity index 97% rename from src/layouts/OldDocsLayout.astro rename to src/layouts/docs.astro index ba6a98eb00..5b1264b2d6 100644 --- a/src/layouts/OldDocsLayout.astro +++ b/src/layouts/docs.astro @@ -2,7 +2,7 @@ import VersionNotice from "@/components/chrome/VersionNotice.astro"; import LiquidInclude from "@/components/LiquidInclude.astro"; import { getLiquidRenderer } from "@/lib/liquid/liquidRenderer"; -import DefaultLayout from "./DefaultLayout.astro"; +import DefaultLayout from "./default.astro"; type Props = { page: { diff --git a/src/layouts/ErrorLayout.astro b/src/layouts/error.astro similarity index 100% rename from src/layouts/ErrorLayout.astro rename to src/layouts/error.astro diff --git a/src/layouts/NewDocsLayout.astro b/src/layouts/new-docs.astro similarity index 98% rename from src/layouts/NewDocsLayout.astro rename to src/layouts/new-docs.astro index fa0f0dd9cf..9436c2e784 100644 --- a/src/layouts/NewDocsLayout.astro +++ b/src/layouts/new-docs.astro @@ -7,7 +7,7 @@ import VersionSelector from "@/components/chrome/VersionSelector.astro"; import LiquidInclude from "@/components/LiquidInclude.astro"; import { UNIFY_ENABLED_PAGES } from "@/constants"; import { getLiquidRenderer } from "@/lib/liquid/liquidRenderer"; -import DefaultNewLayout from "./DefaultNewLayout.astro"; +import DefaultNewLayout from "./default-new.astro"; type Props = { page: { diff --git a/src/pages/docs/404.astro b/src/pages/docs/404.astro index 1dfb8b4d97..159671043e 100644 --- a/src/pages/docs/404.astro +++ b/src/pages/docs/404.astro @@ -1,7 +1,7 @@ --- import path from "node:path"; import { fileURLToPath } from "node:url"; -import ErrorLayout from "@/layouts/ErrorLayout.astro"; +import ErrorLayout from "@/layouts/error.astro"; const page = { title: "Page not found", url: "/docs/404" }; const dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/src/pages/docs/[version]/[...slug].astro b/src/pages/docs/[version]/[...slug].astro index 7b7ba1df61..227c565446 100644 --- a/src/pages/docs/[version]/[...slug].astro +++ b/src/pages/docs/[version]/[...slug].astro @@ -2,14 +2,24 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { DOCS_SRC_ROOT, METABASE_REPO_PATH } from "@/constants"; -import NewDocsLayout from "@/layouts/NewDocsLayout.astro"; -import OldDocsLayout from "@/layouts/OldDocsLayout.astro"; import { constructDocMetadata } from "@/lib/docs/constructDocMetadata"; import { resolveDocUrl } from "@/lib/docs/resolveDoc"; import { rewriteDocLinks } from "@/lib/docs/rewriteDocLinks"; import { baseCtx, getLiquidRenderer } from "@/lib/liquid/liquidRenderer"; import { getMarkdownRenderer } from "@/lib/markdown/markdownRenderer"; import { getCollection, type DataEntryMap } from "astro:content"; +import type { AstroComponentFactory } from "astro/runtime/server/index.js"; + +// Keyed by filename, e.g. src/layouts/new-docs.astro → "new-docs", so a doc's +// `layout` frontmatter value selects the component. +const layouts = Object.fromEntries( + Object.entries( + import.meta.glob("/src/layouts/*.astro", { + eager: true, + import: "default", + }), + ).map(([file, component]) => [path.basename(file, ".astro"), component]), +); type Props = | { kind: "md"; doc: DataEntryMap["docs"][number] } @@ -86,7 +96,8 @@ if (kind === "html") { renderedHtml = renderResult.code; } -const Layout = doc.data.layout === "docs" ? OldDocsLayout : NewDocsLayout; +// TODO: There's probably a better way to handle docs-api than the Fragment fallback +const Layout = layouts[doc.data.layout] || Fragment; --- {kind === "html" ? ( diff --git a/src/pages/docs/all.astro b/src/pages/docs/all.astro index 953d3147a9..b2c8aacab8 100644 --- a/src/pages/docs/all.astro +++ b/src/pages/docs/all.astro @@ -1,7 +1,7 @@ --- import path from "node:path"; import { fileURLToPath } from "node:url"; -import NewDocsLayout from "@/layouts/NewDocsLayout.astro"; +import NewDocsLayout from "@/layouts/new-docs.astro"; import { baseCtx } from "@/lib/liquid/liquidRenderer"; const { site } = baseCtx; From 0a776d7c4f280428cd7fec68ee4a31783de4ec6e Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Tue, 29 Sep 2026 15:01:28 -0400 Subject: [PATCH 02/10] GRO-740 add category layout --- src/components/categories/CategoryCards.astro | 24 ++++++ .../categories/CategoryHeader.astro | 61 +++++++++++++++ src/components/categories/ReadTime.astro | 14 ++++ src/icons/clock.svg | 4 + src/layouts/category.astro | 75 +++++++++++++++++++ src/pages/docs/[version]/[...slug].astro | 4 +- 6 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/components/categories/CategoryCards.astro create mode 100644 src/components/categories/CategoryHeader.astro create mode 100644 src/components/categories/ReadTime.astro create mode 100644 src/icons/clock.svg create mode 100644 src/layouts/category.astro diff --git a/src/components/categories/CategoryCards.astro b/src/components/categories/CategoryCards.astro new file mode 100644 index 0000000000..9b4660fc55 --- /dev/null +++ b/src/components/categories/CategoryCards.astro @@ -0,0 +1,24 @@ +--- +import ReadTime from "./ReadTime.astro"; + +export type PageEntry = { + url: string; + title: string; + summary: string; + readTime?: number; +}; + +type Props = { + pages: PageEntry[]; +}; + +const { pages } = Astro.props; +--- + +{pages?.map((pageEntry) => ( + +

{pageEntry.title}

+

{pageEntry.summary}

+ {!!pageEntry.readTime && } +
+))} diff --git a/src/components/categories/CategoryHeader.astro b/src/components/categories/CategoryHeader.astro new file mode 100644 index 0000000000..40eb18e70d --- /dev/null +++ b/src/components/categories/CategoryHeader.astro @@ -0,0 +1,61 @@ +--- +interface Props { + title: string; + summary?: string; + image?: string; +} + +const { title, summary, image } = Astro.props; +--- + +
+

{title}

+ {summary &&
{summary}
} + {image && {title}} +
+ + diff --git a/src/components/categories/ReadTime.astro b/src/components/categories/ReadTime.astro new file mode 100644 index 0000000000..8a89a1a3c3 --- /dev/null +++ b/src/components/categories/ReadTime.astro @@ -0,0 +1,14 @@ +--- +import Clock from "@/icons/clock.svg"; + +interface Props { + minutes: number; +} + +const { minutes } = Astro.props; +--- + +

+ + {minutes} min read +

diff --git a/src/icons/clock.svg b/src/icons/clock.svg new file mode 100644 index 0000000000..3313889275 --- /dev/null +++ b/src/icons/clock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/layouts/category.astro b/src/layouts/category.astro new file mode 100644 index 0000000000..99a08832db --- /dev/null +++ b/src/layouts/category.astro @@ -0,0 +1,75 @@ +--- +import CategoryCards, { + type PageEntry, +} from "@/components/categories/CategoryCards.astro"; +import CategoryHeader from "@/components/categories/CategoryHeader.astro"; +import type { DocPage } from "@/lib/docs/constructDocMetadata"; +import { getNavForVersion, type NavNode } from "@/lib/docs/nav"; +import { resolveDocUrl } from "@/lib/docs/resolveDoc"; +import { getCollection } from "astro:content"; +import NewDocsLayout from "./new-docs.astro"; + +type Props = { + page: DocPage; + dirname: string; +}; + +const { page, dirname } = Astro.props; + +function getReadTime(body?: string, minimum = 0): number | undefined { + if (!body) return undefined; + + // Match Jekyll's `number_of_words` filter, which counts whitespace-separated + // tokens in the unrendered collection content. + const words = body.trim().split(/\s+/).filter(Boolean).length; + return Math.max(minimum, Math.floor(words / 238)); +} + +// Nav urls have no trailing slash, but index pages resolve to one. +const normalizeUrl = (url: string) => url.replace(/\/+$/, ""); + +const findNavNode = ( + nodes: NavNode[], + targetUrl: string, +): NavNode | undefined => { + for (const node of nodes) { + if (node.url && normalizeUrl(node.url) === targetUrl) return node; + const match = node.pages && findNavNode(node.pages, targetUrl); + if (match) return match; + } +}; + +// page.url is `/docs//` +const version = page.url.split("/")[2]; +const navNode = findNavNode( + getNavForVersion(version).categories, + normalizeUrl(page.url), +); + +const docsByUrl = new Map( + (await getCollection("docs")).map((doc) => [ + normalizeUrl(resolveDocUrl({ id: doc.id }).url), + doc, + ]), +); + +const pages: PageEntry[] = (navNode?.pages ?? []) + .filter((node): node is NavNode & { url: string } => Boolean(node.url)) + .map((node) => { + const doc = docsByUrl.get(normalizeUrl(node.url)); + if (!doc) { + throw new Error(`Doc not found: ${node.url}`); + } + return { + url: node.url, + title: doc.data.title ?? node.name, + summary: doc.data.summary, + readTime: getReadTime(doc.body, 1), + }; + }); +--- + + + + + diff --git a/src/pages/docs/[version]/[...slug].astro b/src/pages/docs/[version]/[...slug].astro index 227c565446..01d3a6aa23 100644 --- a/src/pages/docs/[version]/[...slug].astro +++ b/src/pages/docs/[version]/[...slug].astro @@ -2,6 +2,7 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { DOCS_SRC_ROOT, METABASE_REPO_PATH } from "@/constants"; +import NewDocsLayout from "@/layouts/new-docs.astro"; import { constructDocMetadata } from "@/lib/docs/constructDocMetadata"; import { resolveDocUrl } from "@/lib/docs/resolveDoc"; import { rewriteDocLinks } from "@/lib/docs/rewriteDocLinks"; @@ -96,8 +97,7 @@ if (kind === "html") { renderedHtml = renderResult.code; } -// TODO: There's probably a better way to handle docs-api than the Fragment fallback -const Layout = layouts[doc.data.layout] || Fragment; +const Layout = layouts[doc.data.layout] || NewDocsLayout; --- {kind === "html" ? ( From 8c099757f1f2c0e1fde445836b24c6e9128ea1a9 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 30 Sep 2026 17:07:44 -0400 Subject: [PATCH 03/10] GRO-740 style image --- .../categories/CategoryHeader.astro | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/src/components/categories/CategoryHeader.astro b/src/components/categories/CategoryHeader.astro index 40eb18e70d..fdf4db28ff 100644 --- a/src/components/categories/CategoryHeader.astro +++ b/src/components/categories/CategoryHeader.astro @@ -8,54 +8,39 @@ interface Props { const { title, summary, image } = Astro.props; --- -
+

{title}

{summary &&
{summary}
} - {image && {title}} + {image && }
From f3d9926111e0f67152057398adc932120a874389 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 1 Oct 2026 14:05:07 -0400 Subject: [PATCH 04/10] GRO-740 hide mobile "On this page" toggle if the menu is empty --- src/components/chrome/PageNav.astro | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/chrome/PageNav.astro b/src/components/chrome/PageNav.astro index b558219ed6..ff73f4d652 100644 --- a/src/components/chrome/PageNav.astro +++ b/src/components/chrome/PageNav.astro @@ -82,6 +82,10 @@ } } + #toc-toggle:has(+ #sub-navigation-content:empty) { + display: none; /* Hide the toggle if the menu is empty */ + } + @media (max-width: 991.98px) { #toc-toggle { appearance: none; From a43bf544d5417d403acf969a4fdf51ee9fe0f3db Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 1 Oct 2026 15:34:27 -0400 Subject: [PATCH 05/10] GRO-740 simplify and fix category urls --- .../categories/CategoryHeader.astro | 4 +- src/components/chrome/Breadcrumb.astro | 63 ++++++------------- src/lib/docs/nav.ts | 17 +++++ 3 files changed, 39 insertions(+), 45 deletions(-) diff --git a/src/components/categories/CategoryHeader.astro b/src/components/categories/CategoryHeader.astro index fdf4db28ff..a2e8aec6bf 100644 --- a/src/components/categories/CategoryHeader.astro +++ b/src/components/categories/CategoryHeader.astro @@ -2,10 +2,10 @@ interface Props { title: string; summary?: string; - image?: string; + category_image?: string; } -const { title, summary, image } = Astro.props; +const { title, summary, category_image: image } = Astro.props; ---
diff --git a/src/components/chrome/Breadcrumb.astro b/src/components/chrome/Breadcrumb.astro index bcdcf6ee8a..37d58cd736 100644 --- a/src/components/chrome/Breadcrumb.astro +++ b/src/components/chrome/Breadcrumb.astro @@ -1,4 +1,5 @@ --- +import { getCategoryUrl } from "@/lib/docs/nav"; import { baseCtx } from "@/lib/liquid/liquidRenderer"; type Props = { @@ -15,25 +16,25 @@ type Props = { const { page } = Astro.props; const { site } = baseCtx; -const slugify = (str: string) => - str - .toLowerCase() - .trim() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); - -const version = page.version === "latest" ? site.docs_version : page.version; const latestOrExplicitVersion = site.docs_version === page.version && page.url.includes("/docs/latest/") ? "latest" : (page.version ?? ""); -const categorySlug = slugify(page.category ?? ""); -const categorySlugPart = - categorySlug === "troubleshooting-guide" - ? "/" - : categorySlug === "api" - ? "api-documentation" - : "/start"; + +const getFallbackCategoryUrl = (version: string, url: string) => { + // page.url is /docs///... + const categorySlug = url.split("/")[3] ?? ""; + const fallbackCategoryPath = + { + api: "api-documentation", + "troubleshooting-guide": "troubleshooting-guide/", + }[categorySlug] ?? `${categorySlug}/start`; + return `/docs/${version}/${fallbackCategoryPath}`; +}; + +const categoryUrl = + getCategoryUrl(latestOrExplicitVersion, page.category ?? "") ?? + getFallbackCategoryUrl(latestOrExplicitVersion, page.url); ---
@@ -55,10 +56,7 @@ const categorySlugPart = ) : ( page.show_category_breadcrumb && ( @@ -66,30 +64,9 @@ const categorySlugPart = )} ) : ( - <> - {page.title === "Search" ? ( - - ) : ( - - )} - - {page.category === "Api" && ( - - )} - {page.title !== "README" && ( - - )} - + )} diff --git a/src/lib/docs/nav.ts b/src/lib/docs/nav.ts index 845fbdd978..26324b8bde 100644 --- a/src/lib/docs/nav.ts +++ b/src/lib/docs/nav.ts @@ -45,3 +45,20 @@ export const getNavForVersion = (version: string): Nav => { const shouldCache = import.meta.env.MODE !== "development"; return shouldCache ? (navCache[version] ??= computeNav()) : computeNav(); }; + +// Finds the url of the nav section (a node with child pages) named `category`. +export const getCategoryUrl = ( + version: string, + category: string, +): string | undefined => { + const target = category.toLowerCase(); + const find = (nodes: NavNode[] = []): NavNode | undefined => { + for (const node of nodes) { + if (node.url && node.pages && node.name.toLowerCase() === target) + return node; + const match = find(node.pages); + if (match) return match; + } + }; + return find(getNavForVersion(version).categories)?.url; +}; From 5eb3bd86ef962fc19c6d5bbd63d3f2849ccf99ec Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 1 Oct 2026 17:11:21 -0400 Subject: [PATCH 06/10] GRO-740 hide mobile toc-toggle with js because simpler but also because the degraded experience is moved to the edge case, not the default --- src/components/chrome/PageNav.astro | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/chrome/PageNav.astro b/src/components/chrome/PageNav.astro index ff73f4d652..65b27503d9 100644 --- a/src/components/chrome/PageNav.astro +++ b/src/components/chrome/PageNav.astro @@ -20,6 +20,7 @@ }); const headers = Array.from(document.querySelectorAll("h2")); + if (!headers.length) button.remove(); const links = new Map(); let activeLink: HTMLAnchorElement | null = null; @@ -82,10 +83,6 @@ } } - #toc-toggle:has(+ #sub-navigation-content:empty) { - display: none; /* Hide the toggle if the menu is empty */ - } - @media (max-width: 991.98px) { #toc-toggle { appearance: none; From 8da7d66e01be11a65f3516ad6e3adc32a5c45b89 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Thu, 1 Oct 2026 17:32:14 -0400 Subject: [PATCH 07/10] GRO-740 performance improvements --- src/layouts/category.astro | 61 +++++++++++++------------------------- src/lib/docs/nav.ts | 25 ++++++++++------ src/lib/docs/resolveDoc.ts | 5 ++++ 3 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/layouts/category.astro b/src/layouts/category.astro index 99a08832db..711d0bf502 100644 --- a/src/layouts/category.astro +++ b/src/layouts/category.astro @@ -4,9 +4,9 @@ import CategoryCards, { } from "@/components/categories/CategoryCards.astro"; import CategoryHeader from "@/components/categories/CategoryHeader.astro"; import type { DocPage } from "@/lib/docs/constructDocMetadata"; -import { getNavForVersion, type NavNode } from "@/lib/docs/nav"; -import { resolveDocUrl } from "@/lib/docs/resolveDoc"; -import { getCollection } from "astro:content"; +import { findNavNode, getNavForVersion, type NavNode } from "@/lib/docs/nav"; +import { docIdFromUrl } from "@/lib/docs/resolveDoc"; +import { getEntry } from "astro:content"; import NewDocsLayout from "./new-docs.astro"; type Props = { @@ -25,48 +25,27 @@ function getReadTime(body?: string, minimum = 0): number | undefined { return Math.max(minimum, Math.floor(words / 238)); } -// Nav urls have no trailing slash, but index pages resolve to one. -const normalizeUrl = (url: string) => url.replace(/\/+$/, ""); - -const findNavNode = ( - nodes: NavNode[], - targetUrl: string, -): NavNode | undefined => { - for (const node of nodes) { - if (node.url && normalizeUrl(node.url) === targetUrl) return node; - const match = node.pages && findNavNode(node.pages, targetUrl); - if (match) return match; - } -}; - -// page.url is `/docs//` -const version = page.url.split("/")[2]; const navNode = findNavNode( - getNavForVersion(version).categories, - normalizeUrl(page.url), + getNavForVersion(page.latest ? "latest" : page.version).categories, + (node) => !!node.url && node.url === page.url, ); -const docsByUrl = new Map( - (await getCollection("docs")).map((doc) => [ - normalizeUrl(resolveDocUrl({ id: doc.id }).url), - doc, - ]), +const pages: PageEntry[] = await Promise.all( + (navNode?.pages ?? []) + .filter((node): node is NavNode & { url: string } => Boolean(node.url)) + .map(async (node) => { + const doc = await getEntry("docs", docIdFromUrl(node.url)); + if (!doc) { + throw new Error(`Doc not found: ${node.url}`); + } + return { + url: node.url, + title: doc.data.title ?? node.name, + summary: doc.data.summary, + readTime: getReadTime(doc.body, 1), + }; + }), ); - -const pages: PageEntry[] = (navNode?.pages ?? []) - .filter((node): node is NavNode & { url: string } => Boolean(node.url)) - .map((node) => { - const doc = docsByUrl.get(normalizeUrl(node.url)); - if (!doc) { - throw new Error(`Doc not found: ${node.url}`); - } - return { - url: node.url, - title: doc.data.title ?? node.name, - summary: doc.data.summary, - readTime: getReadTime(doc.body, 1), - }; - }); --- diff --git a/src/lib/docs/nav.ts b/src/lib/docs/nav.ts index 26324b8bde..842ef51b5d 100644 --- a/src/lib/docs/nav.ts +++ b/src/lib/docs/nav.ts @@ -46,19 +46,26 @@ export const getNavForVersion = (version: string): Nav => { return shouldCache ? (navCache[version] ??= computeNav()) : computeNav(); }; +// Depth-first search for the first node matching `predicate`. +export const findNavNode = ( + nodes: NavNode[] = [], + predicate: (node: NavNode) => boolean, +): NavNode | undefined => { + for (const node of nodes) { + if (predicate(node)) return node; + const match = findNavNode(node.pages, predicate); + if (match) return match; + } +}; + // Finds the url of the nav section (a node with child pages) named `category`. export const getCategoryUrl = ( version: string, category: string, ): string | undefined => { const target = category.toLowerCase(); - const find = (nodes: NavNode[] = []): NavNode | undefined => { - for (const node of nodes) { - if (node.url && node.pages && node.name.toLowerCase() === target) - return node; - const match = find(node.pages); - if (match) return match; - } - }; - return find(getNavForVersion(version).categories)?.url; + return findNavNode( + getNavForVersion(version).categories, + (node) => !!node.url && !!node.pages && node.name.toLowerCase() === target, + )?.url; }; diff --git a/src/lib/docs/resolveDoc.ts b/src/lib/docs/resolveDoc.ts index 7a0e5df449..d92f7517c4 100644 --- a/src/lib/docs/resolveDoc.ts +++ b/src/lib/docs/resolveDoc.ts @@ -21,3 +21,8 @@ export const resolveDocUrl = ({ separatorIndex !== -1 ? resolvedId.slice(separatorIndex + 1) : ""; return { version, slug, url: `/docs/${version}/${slug}` }; }; + +// Inverse of resolveDocUrl for `.md` docs: maps `/docs//` back +// to the collection id, with index pages ending in `/index`. +export const docIdFromUrl = (url: string): string => + url.replace(/^\/docs\//, "").replace(/\/$/, "/index"); From 1d6e4a2301eae590a9015b5d3a9b83653ecb552d Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Fri, 2 Oct 2026 10:29:53 -0400 Subject: [PATCH 08/10] GRO-740 simplify how layout is chosen --- .../{category.astro => CategoryLayout.astro} | 2 +- .../{default.astro => DefaultLayout.astro} | 0 ...fault-new.astro => DefaultNewLayout.astro} | 0 .../{error.astro => ErrorLayout.astro} | 0 .../{new-docs.astro => NewDocsLayout.astro} | 2 +- .../{docs.astro => OldDocsLayout.astro} | 2 +- src/pages/docs/404.astro | 2 +- src/pages/docs/[version]/[...slug].astro | 21 +++++++------------ src/pages/docs/all.astro | 2 +- 9 files changed, 13 insertions(+), 18 deletions(-) rename src/layouts/{category.astro => CategoryLayout.astro} (96%) rename src/layouts/{default.astro => DefaultLayout.astro} (100%) rename src/layouts/{default-new.astro => DefaultNewLayout.astro} (100%) rename src/layouts/{error.astro => ErrorLayout.astro} (100%) rename src/layouts/{new-docs.astro => NewDocsLayout.astro} (98%) rename src/layouts/{docs.astro => OldDocsLayout.astro} (97%) diff --git a/src/layouts/category.astro b/src/layouts/CategoryLayout.astro similarity index 96% rename from src/layouts/category.astro rename to src/layouts/CategoryLayout.astro index 711d0bf502..0f1cc24425 100644 --- a/src/layouts/category.astro +++ b/src/layouts/CategoryLayout.astro @@ -7,7 +7,7 @@ import type { DocPage } from "@/lib/docs/constructDocMetadata"; import { findNavNode, getNavForVersion, type NavNode } from "@/lib/docs/nav"; import { docIdFromUrl } from "@/lib/docs/resolveDoc"; import { getEntry } from "astro:content"; -import NewDocsLayout from "./new-docs.astro"; +import NewDocsLayout from "./NewDocsLayout.astro"; type Props = { page: DocPage; diff --git a/src/layouts/default.astro b/src/layouts/DefaultLayout.astro similarity index 100% rename from src/layouts/default.astro rename to src/layouts/DefaultLayout.astro diff --git a/src/layouts/default-new.astro b/src/layouts/DefaultNewLayout.astro similarity index 100% rename from src/layouts/default-new.astro rename to src/layouts/DefaultNewLayout.astro diff --git a/src/layouts/error.astro b/src/layouts/ErrorLayout.astro similarity index 100% rename from src/layouts/error.astro rename to src/layouts/ErrorLayout.astro diff --git a/src/layouts/new-docs.astro b/src/layouts/NewDocsLayout.astro similarity index 98% rename from src/layouts/new-docs.astro rename to src/layouts/NewDocsLayout.astro index 9436c2e784..fa0f0dd9cf 100644 --- a/src/layouts/new-docs.astro +++ b/src/layouts/NewDocsLayout.astro @@ -7,7 +7,7 @@ import VersionSelector from "@/components/chrome/VersionSelector.astro"; import LiquidInclude from "@/components/LiquidInclude.astro"; import { UNIFY_ENABLED_PAGES } from "@/constants"; import { getLiquidRenderer } from "@/lib/liquid/liquidRenderer"; -import DefaultNewLayout from "./default-new.astro"; +import DefaultNewLayout from "./DefaultNewLayout.astro"; type Props = { page: { diff --git a/src/layouts/docs.astro b/src/layouts/OldDocsLayout.astro similarity index 97% rename from src/layouts/docs.astro rename to src/layouts/OldDocsLayout.astro index 5b1264b2d6..ba6a98eb00 100644 --- a/src/layouts/docs.astro +++ b/src/layouts/OldDocsLayout.astro @@ -2,7 +2,7 @@ import VersionNotice from "@/components/chrome/VersionNotice.astro"; import LiquidInclude from "@/components/LiquidInclude.astro"; import { getLiquidRenderer } from "@/lib/liquid/liquidRenderer"; -import DefaultLayout from "./default.astro"; +import DefaultLayout from "./DefaultLayout.astro"; type Props = { page: { diff --git a/src/pages/docs/404.astro b/src/pages/docs/404.astro index 159671043e..1dfb8b4d97 100644 --- a/src/pages/docs/404.astro +++ b/src/pages/docs/404.astro @@ -1,7 +1,7 @@ --- import path from "node:path"; import { fileURLToPath } from "node:url"; -import ErrorLayout from "@/layouts/error.astro"; +import ErrorLayout from "@/layouts/ErrorLayout.astro"; const page = { title: "Page not found", url: "/docs/404" }; const dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/src/pages/docs/[version]/[...slug].astro b/src/pages/docs/[version]/[...slug].astro index 01d3a6aa23..f87809bf30 100644 --- a/src/pages/docs/[version]/[...slug].astro +++ b/src/pages/docs/[version]/[...slug].astro @@ -2,7 +2,9 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { DOCS_SRC_ROOT, METABASE_REPO_PATH } from "@/constants"; -import NewDocsLayout from "@/layouts/new-docs.astro"; +import CategoryLayout from "@/layouts/CategoryLayout.astro"; +import NewDocsLayout from "@/layouts/NewDocsLayout.astro"; +import OldDocsLayout from "@/layouts/OldDocsLayout.astro"; import { constructDocMetadata } from "@/lib/docs/constructDocMetadata"; import { resolveDocUrl } from "@/lib/docs/resolveDoc"; import { rewriteDocLinks } from "@/lib/docs/rewriteDocLinks"; @@ -11,17 +13,6 @@ import { getMarkdownRenderer } from "@/lib/markdown/markdownRenderer"; import { getCollection, type DataEntryMap } from "astro:content"; import type { AstroComponentFactory } from "astro/runtime/server/index.js"; -// Keyed by filename, e.g. src/layouts/new-docs.astro → "new-docs", so a doc's -// `layout` frontmatter value selects the component. -const layouts = Object.fromEntries( - Object.entries( - import.meta.glob("/src/layouts/*.astro", { - eager: true, - import: "default", - }), - ).map(([file, component]) => [path.basename(file, ".astro"), component]), -); - type Props = | { kind: "md"; doc: DataEntryMap["docs"][number] } | { kind: "html"; doc: DataEntryMap["docsHtml"][number] }; @@ -97,7 +88,11 @@ if (kind === "html") { renderedHtml = renderResult.code; } -const Layout = layouts[doc.data.layout] || NewDocsLayout; +const layouts: Record = { + docs: OldDocsLayout, + category: CategoryLayout, +}; +const Layout = layouts[doc.data.layout] ?? NewDocsLayout; --- {kind === "html" ? ( diff --git a/src/pages/docs/all.astro b/src/pages/docs/all.astro index b2c8aacab8..953d3147a9 100644 --- a/src/pages/docs/all.astro +++ b/src/pages/docs/all.astro @@ -1,7 +1,7 @@ --- import path from "node:path"; import { fileURLToPath } from "node:url"; -import NewDocsLayout from "@/layouts/new-docs.astro"; +import NewDocsLayout from "@/layouts/NewDocsLayout.astro"; import { baseCtx } from "@/lib/liquid/liquidRenderer"; const { site } = baseCtx; From 2ef90cb6651e33613f2e493be2a03aefb2794989 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Fri, 2 Oct 2026 11:38:53 -0400 Subject: [PATCH 09/10] GRO-740 cleanup: getCategoryUrl doesn't recurse indefinitely since there's no need + type fix --- src/components/chrome/Breadcrumb.astro | 9 +++++---- src/lib/docs/constructDocMetadata.ts | 2 +- src/lib/docs/nav.ts | 11 +++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/chrome/Breadcrumb.astro b/src/components/chrome/Breadcrumb.astro index 37d58cd736..8c5bbc3278 100644 --- a/src/components/chrome/Breadcrumb.astro +++ b/src/components/chrome/Breadcrumb.astro @@ -24,11 +24,12 @@ const latestOrExplicitVersion = const getFallbackCategoryUrl = (version: string, url: string) => { // page.url is /docs///... const categorySlug = url.split("/")[3] ?? ""; + const legacyMapping: Record = { + api: "api-documentation", + "troubleshooting-guide": "troubleshooting-guide/", + }; const fallbackCategoryPath = - { - api: "api-documentation", - "troubleshooting-guide": "troubleshooting-guide/", - }[categorySlug] ?? `${categorySlug}/start`; + legacyMapping[categorySlug] ?? `${categorySlug}/start`; return `/docs/${version}/${fallbackCategoryPath}`; }; diff --git a/src/lib/docs/constructDocMetadata.ts b/src/lib/docs/constructDocMetadata.ts index 688afbfdd4..6170230721 100644 --- a/src/lib/docs/constructDocMetadata.ts +++ b/src/lib/docs/constructDocMetadata.ts @@ -51,7 +51,7 @@ export type DocMetadata = { category: string; title: string; source_url: string; - layout: "docs" | "new-docs"; + layout: string; permalink?: string; latest?: boolean; }; diff --git a/src/lib/docs/nav.ts b/src/lib/docs/nav.ts index 842ef51b5d..8ce188aab7 100644 --- a/src/lib/docs/nav.ts +++ b/src/lib/docs/nav.ts @@ -59,13 +59,16 @@ export const findNavNode = ( }; // Finds the url of the nav section (a node with child pages) named `category`. +// Sections only live one level below the top-level categories. export const getCategoryUrl = ( version: string, category: string, ): string | undefined => { const target = category.toLowerCase(); - return findNavNode( - getNavForVersion(version).categories, - (node) => !!node.url && !!node.pages && node.name.toLowerCase() === target, - )?.url; + return getNavForVersion(version) + .categories.flatMap((c) => c.pages ?? []) + .find( + (node) => + !!node.url && !!node.pages && node.name.toLowerCase() === target, + )?.url; }; From 96220c2c07032de943ec078bc09905333a3588d3 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Mon, 5 Oct 2026 11:16:18 -0400 Subject: [PATCH 10/10] GRO-740 filter out external links from category landing pages --- src/layouts/CategoryLayout.astro | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/layouts/CategoryLayout.astro b/src/layouts/CategoryLayout.astro index 0f1cc24425..5e6471b9d1 100644 --- a/src/layouts/CategoryLayout.astro +++ b/src/layouts/CategoryLayout.astro @@ -32,7 +32,10 @@ const navNode = findNavNode( const pages: PageEntry[] = await Promise.all( (navNode?.pages ?? []) - .filter((node): node is NavNode & { url: string } => Boolean(node.url)) + // Only docs pages get cards; external links are skipped. + .filter((node): node is NavNode & { url: string } => + Boolean(node.url?.startsWith("/docs/")), + ) .map(async (node) => { const doc = await getEntry("docs", docIdFromUrl(node.url)); if (!doc) {