@@ -55,10 +57,7 @@ const categorySlugPart =
) : (
page.show_category_breadcrumb && (
-
+
{page.category}
@@ -66,30 +65,9 @@ const categorySlugPart =
)}
>
) : (
- <>
- {page.title === "Search" ? (
-
-
- {site.latest_version}
-
-
- ) : (
-
- {version}
-
- )}
-
- {page.category === "Api" && (
-
- {page.category}
-
- )}
- {page.title !== "README" && (
-
- {page.title}
-
- )}
- >
+
+ {page.title}
+
)}
diff --git a/src/components/chrome/PageNav.astro b/src/components/chrome/PageNav.astro
index b558219ed6..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;
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/CategoryLayout.astro b/src/layouts/CategoryLayout.astro
new file mode 100644
index 0000000000..5e6471b9d1
--- /dev/null
+++ b/src/layouts/CategoryLayout.astro
@@ -0,0 +1,57 @@
+---
+import CategoryCards, {
+ type PageEntry,
+} from "@/components/categories/CategoryCards.astro";
+import CategoryHeader from "@/components/categories/CategoryHeader.astro";
+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 "./NewDocsLayout.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));
+}
+
+const navNode = findNavNode(
+ getNavForVersion(page.latest ? "latest" : page.version).categories,
+ (node) => !!node.url && node.url === page.url,
+);
+
+const pages: PageEntry[] = await Promise.all(
+ (navNode?.pages ?? [])
+ // 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) {
+ 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/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 845fbdd978..8ce188aab7 100644
--- a/src/lib/docs/nav.ts
+++ b/src/lib/docs/nav.ts
@@ -45,3 +45,30 @@ export const getNavForVersion = (version: string): Nav => {
const shouldCache = import.meta.env.MODE !== "development";
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`.
+// Sections only live one level below the top-level categories.
+export const getCategoryUrl = (
+ version: string,
+ category: string,
+): string | undefined => {
+ const target = category.toLowerCase();
+ return getNavForVersion(version)
+ .categories.flatMap((c) => c.pages ?? [])
+ .find(
+ (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");
diff --git a/src/pages/docs/[version]/[...slug].astro b/src/pages/docs/[version]/[...slug].astro
index 7b7ba1df61..f87809bf30 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 CategoryLayout from "@/layouts/CategoryLayout.astro";
import NewDocsLayout from "@/layouts/NewDocsLayout.astro";
import OldDocsLayout from "@/layouts/OldDocsLayout.astro";
import { constructDocMetadata } from "@/lib/docs/constructDocMetadata";
@@ -10,6 +11,7 @@ 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";
type Props =
| { kind: "md"; doc: DataEntryMap["docs"][number] }
@@ -86,7 +88,11 @@ if (kind === "html") {
renderedHtml = renderResult.code;
}
-const Layout = doc.data.layout === "docs" ? OldDocsLayout : NewDocsLayout;
+const layouts: Record = {
+ docs: OldDocsLayout,
+ category: CategoryLayout,
+};
+const Layout = layouts[doc.data.layout] ?? NewDocsLayout;
---
{kind === "html" ? (