diff --git a/_components/Footer.tsx b/_components/Footer.tsx
index 5365b3691..85a3b41e1 100644
--- a/_components/Footer.tsx
+++ b/_components/Footer.tsx
@@ -59,7 +59,7 @@ const data = [
},
{
label: "Deno API Reference",
- href: "/api/deno/~/Deno",
+ href: "/api/deno/",
},
],
},
diff --git a/_components/SidebarNav/comp.tsx b/_components/SidebarNav/comp.tsx
index f08f52d06..4e44d7c50 100644
--- a/_components/SidebarNav/comp.tsx
+++ b/_components/SidebarNav/comp.tsx
@@ -4,7 +4,7 @@ import ReferenceSidebarNav from "../../reference/_components/ReferenceSidebarNav
export default function (data: Lume.Data) {
const sectionData = data.sectionData;
const currentUrl = data.currentUrl.replace(/\/$/, "");
- const isReference = currentUrl.startsWith("/api/");
+ const isReference = currentUrl === "/api" || currentUrl.startsWith("/api/");
if (isReference) {
return ;
diff --git a/_includes/layout.tsx b/_includes/layout.tsx
index f79bd04d7..b022c362b 100644
--- a/_includes/layout.tsx
+++ b/_includes/layout.tsx
@@ -148,7 +148,7 @@ export default function Layout(data: Lume.Data) {
/>
+
+
+ {data.intro}
+
+
+ {data.groups.map(({ title, url, slug, group }) => (
+
+
+
+ {group.sections.flatMap((section) =>
+ section.nodes.map((node) => (
+
+
+
+ {node.name}
+
+ {node.docs && (
+
+ )}
+
+ ))
+ )}
+
+
+ ))}
+
+ );
+}
diff --git a/_includes/reference/landing.tsx b/_includes/reference/landing.tsx
new file mode 100644
index 000000000..84dcca9d5
--- /dev/null
+++ b/_includes/reference/landing.tsx
@@ -0,0 +1,86 @@
+export const layout = "doc.tsx";
+
+export interface LandingTile {
+ title: string;
+ href: string;
+ description: string | null;
+}
+
+export interface LandingSection {
+ key: string;
+ title: string;
+ description: string;
+ href: string;
+ allSymbolsHref: string;
+ tiles: LandingTile[];
+ /** Render tiles in a denser grid (used for the long Node module list). */
+ dense?: boolean;
+}
+
+export interface LandingData {
+ intro: string;
+ sections: LandingSection[];
+}
+
+/** The /api/ hub: every API group presented as a tile grid, generated from
+ * the same grouping data as the reference pages themselves. */
+export default function ApiLanding(
+ { data }: { data: LandingData } & Lume.Data,
+ _helpers: Lume.Helpers,
+) {
+ return (
+
+ API reference
+ {data.intro}
+
+ {data.sections.map((section) => (
+
+
+
+ {section.description}
+
+
+
+ ))}
+
+ );
+}
diff --git a/_includes/reference/multiSymbol.tsx b/_includes/reference/multiSymbol.tsx
new file mode 100644
index 000000000..088efd8ab
--- /dev/null
+++ b/_includes/reference/multiSymbol.tsx
@@ -0,0 +1,52 @@
+import type { Group } from "../../reference/_lib/group.ts";
+
+export const layout = "doc.tsx";
+
+/** A grouped reference page: every symbol of a category (Deno, Web) or
+ * module (Node) documented on a single page โ a symbol index up top,
+ * full documentation below. */
+export default function MultiSymbol(
+ { data, comp }: { data: Group } & Lume.Data,
+ _helpers: Lume.Helpers,
+) {
+ return (
+
+
+
+ {data.title}
+ {data.docs &&
}
+
+
+
+
+ {data.sections.map((section) => (
+
+
{section.title}
+
+
+ ))}
+
+
+
+
+
+ {data.symbols.map((symbol) => (
+
+ ))}
+
+
+
+ );
+}
+
+function SymbolDivider(
+ // deno-lint-ignore no-explicit-any
+ { comp, symbol }: { comp: any; symbol: Group["symbols"][number] },
+) {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/_includes/reference/preview.tsx b/_includes/reference/preview.tsx
new file mode 100644
index 000000000..642292499
--- /dev/null
+++ b/_includes/reference/preview.tsx
@@ -0,0 +1,124 @@
+import type {
+ PreviewMember,
+ PreviewSymbol,
+} from "../../reference/preview.page.ts";
+
+export const layout = "doc.tsx";
+
+interface PreviewData {
+ module: string;
+ usage: string;
+ symbols: PreviewSymbol[];
+ kindOrder: string[];
+ kindTitles: Record
;
+}
+
+/** PROTOTYPE layout: Node.js-docs-style prose-first rendering, fed from raw
+ * doc nodes instead of @deno/doc's pre-rendered HTML. */
+export default function Preview(
+ { data }: { data: PreviewData } & Lume.Data,
+ helpers: Lume.Helpers,
+) {
+ const md = (text: string | null) =>
+ text
+ ? (
+
+ )
+ : null;
+
+ const sinceBadge = (since: string | null) =>
+ since && (
+
+ Added in: {since}
+
+ );
+
+ const deprecatedBadge = (deprecated: string | null) =>
+ deprecated !== null && (
+
+
Deprecated
+ {md(deprecated || "This API is deprecated.")}
+
+ );
+
+ const member = (m: PreviewMember) => (
+
+
+ {sinceBadge(m.since)}
+ {m.returnType && (
+
+ Returns: {m.returnType}
+
+ )}
+ {deprecatedBadge(m.deprecated)}
+ {md(m.doc)}
+
+ );
+
+ return (
+
+
+
+ node:{data.module}
+
+
+
Renderer preview
+
+ This page is a prototype of rendering reference docs directly from
+ raw doc nodes, side by side with{" "}
+ the current renderer .
+
+
+ {data.usage}
+
+ {data.kindOrder.map((kind) => {
+ const group = data.symbols.filter((s) => s.kind === kind);
+ if (group.length === 0) return null;
+ return (
+
+
+ {data.kindTitles[kind]}
+
+ {group.map((symbol) => (
+
+
+ {sinceBadge(symbol.since)}
+ {symbol.signature && (
+ {symbol.signature}
+ )}
+ {symbol.returnType && symbol.kind !== "class" && (
+
+ {symbol.kind === "function" ? "Returns" : "Type"}:{" "}
+ {symbol.returnType}
+
+ )}
+ {deprecatedBadge(symbol.deprecated)}
+ {md(symbol.doc)}
+ {symbol.members.map(member)}
+
+ ))}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/api/deno/index.md b/api/deno/index.md
index 35572b869..e3557127a 100644
--- a/api/deno/index.md
+++ b/api/deno/index.md
@@ -2,6 +2,7 @@
title: "Deno Namespace APIs"
description: "A guide to Deno's built-in runtime APIs. Learn about file system operations, network functionality, permissions management, and other core capabilities available through the global Deno namespace."
layout: doc.tsx
+url: /api/deno/about/
oldUrl:
- /runtime/manual/runtime/
- /runtime/manual/runtime/builtin_apis/
diff --git a/api/node/index.md b/api/node/index.md
index ac5afd6df..b7e497de6 100644
--- a/api/node/index.md
+++ b/api/node/index.md
@@ -2,6 +2,7 @@
title: "Node.js Built-in APIs"
description: "Complete reference for Node.js built-in modules and globals supported in Deno. Explore Node.js APIs including fs, http, crypto, process, buffer, and more with compatibility information."
layout: doc.tsx
+url: /api/node/about/
oldUrl:
- /runtime/manual/node/compatibility/
- /runtime/manual/npm_nodejs/compatibility_mode/
diff --git a/api/web/index.md b/api/web/index.md
index a819cec84..1e9776794 100644
--- a/api/web/index.md
+++ b/api/web/index.md
@@ -2,6 +2,7 @@
title: "Web Platform APIs"
description: "A guide to the Web Platform APIs available in Deno. Learn about fetch, events, workers, storage, and other web standard APIs, including implementation details and deviations from browser specifications."
layout: doc.tsx
+url: /api/web/about/
oldUrl:
- /runtime/manual/runtime/navigator_api/
- /runtime/manual/runtime/web_platform_apis/
diff --git a/middleware/redirects.ts b/middleware/redirects.ts
index 6a7f8af92..ab6d57655 100644
--- a/middleware/redirects.ts
+++ b/middleware/redirects.ts
@@ -140,6 +140,21 @@ function loadFromJson() {
);
}
+ // Redirects from old per-symbol API reference URLs to their anchor on the
+ // grouped reference pages, generated by reference/reference.page.ts.
+ if (existsSync("./_site/api/_redirects.json")) {
+ const apiRedirects = JSON.parse(
+ new TextDecoder().decode(
+ Deno.readFileSync("./_site/api/_redirects.json"),
+ ),
+ ) as Record;
+ redirects = { ...apiRedirects, ...redirects };
+ } else {
+ log.warn(
+ `๐ redirectsMiddleware : No './_site/api/_redirects.json' found.`,
+ );
+ }
+
log.info(
`๐ redirectsMiddleware : Total number of redirects loaded: ${
Object.keys(redirects).length
@@ -154,8 +169,6 @@ function addGoLinksAndRedirectLinks(redirects: Record) {
`๐ addGoLinksAndRedirectLinks: Adding additional redirects...`,
);
- redirects["/api/"] = "/api/deno/";
-
log.debug(
`๐ redirectsMiddleware : Reading redirects from 'go.json'...`,
);
diff --git a/orama/generate_orama_index_full.ts b/orama/generate_orama_index_full.ts
index b9b70f1ab..4bd9039dc 100644
--- a/orama/generate_orama_index_full.ts
+++ b/orama/generate_orama_index_full.ts
@@ -16,6 +16,7 @@
import { walk } from "@std/fs";
import { fromFileUrl, join, relative } from "@std/path";
+import { buildReference } from "../reference/_lib/group.ts";
import { MarkdownIndexer } from "./indexing/MarkdownIndexer.ts";
import { FileSelector } from "./identification/FileSelector.ts";
import { IndexCollection } from "./indexing/IndexCollection.ts";
@@ -316,6 +317,8 @@ function processApiSymbol(
async function processApiReference(): Promise {
console.log("\n๐ง Processing API reference documentation...");
const apiDocs: OramaFullDocument[] = [];
+ // deno-lint-ignore no-explicit-any
+ const referenceJsons: Record = {};
for (const refFile of REFERENCE_FILES) {
const fullPath = join(ROOT_DIR, refFile.path);
@@ -326,6 +329,7 @@ async function processApiReference(): Promise {
const content = await Deno.readTextFile(fullPath);
const data = JSON.parse(content);
+ referenceJsons[refFile.apiType] = data;
let processedCount = 0;
let skippedCount = 0;
@@ -362,6 +366,26 @@ async function processApiReference(): Promise {
}
}
+ // Symbols now live as anchors on grouped reference pages; remap the old
+ // per-symbol URLs so search results land directly on the right anchor.
+ if (referenceJsons.deno && referenceJsons.web && referenceJsons.node) {
+ try {
+ const { redirects } = buildReference(referenceJsons);
+ let remapped = 0;
+ for (const doc of apiDocs) {
+ const target = redirects[doc.path];
+ if (target) {
+ doc.path = target;
+ doc.url = `${BASE_URL}${target}`;
+ remapped++;
+ }
+ }
+ console.log(` Remapped ${remapped} API doc URLs to grouped pages.`);
+ } catch (error) {
+ console.warn(`Could not remap API URLs to grouped pages: ${error}`);
+ }
+ }
+
return apiDocs;
}
diff --git a/reference/_components/ReferenceSidebarNav.tsx b/reference/_components/ReferenceSidebarNav.tsx
index 635403572..c5f84629a 100644
--- a/reference/_components/ReferenceSidebarNav.tsx
+++ b/reference/_components/ReferenceSidebarNav.tsx
@@ -8,9 +8,8 @@ export default function ReferenceSidebarNav(data: Lume.Data) {
title: "Deno APIs",
key: "deno",
basePath: "/api/deno",
- categoryLabel: "Deno APIs by category",
items: [
- { href: "/api/deno", title: "Deno specific APIs" },
+ { href: "/api/deno/about/", title: "About", isBottom: true },
{
href: "/api/deno/all_symbols",
title: "All Deno symbols",
@@ -22,9 +21,8 @@ export default function ReferenceSidebarNav(data: Lume.Data) {
title: "Web APIs",
key: "web",
basePath: "/api/web",
- categoryLabel: "Web APIs by category",
items: [
- { href: "/api/web", title: "Web Platform Support" },
+ { href: "/api/web/about/", title: "About", isBottom: true },
{
href: "/api/web/all_symbols",
title: "All web symbols",
@@ -36,9 +34,8 @@ export default function ReferenceSidebarNav(data: Lume.Data) {
title: "Node APIs",
key: "node",
basePath: "/api/node",
- categoryLabel: "Node APIs by namespace",
items: [
- { href: "/api/node", title: "Node support in deno" },
+ { href: "/api/node/about/", title: "About", isBottom: true },
{
href: "/api/node/all_symbols",
title: "All node symbols",
@@ -87,7 +84,11 @@ function ApiSection({ section, currentUrl, apiCategories }: {
return (
-
+
{/* Main section items */}
{topItems.map((item: any) => (
@@ -100,26 +101,14 @@ function ApiSection({ section, currentUrl, apiCategories }: {
))}
- {/* Category accordion */}
-
-
- {section.categoryLabel}
-
-
-
-
-
+ {/* Categories, flat */}
+
- {/* Bottom items (All symbols) */}
+ {/* Bottom items (About, All symbols) */}
{bottomItems.map((item: any) => {
const isActive = item.href.replace(/\/$/, "") === currentUrl;
return (
diff --git a/reference/_components/SymbolArticle.tsx b/reference/_components/SymbolArticle.tsx
new file mode 100644
index 000000000..30b92faca
--- /dev/null
+++ b/reference/_components/SymbolArticle.tsx
@@ -0,0 +1,62 @@
+import type { GroupSymbol } from "../_lib/group.ts";
+
+/** Full documentation for one root symbol on a grouped reference page.
+ * Adapted from SymbolGroup.tsx, but heading at h2 level with a stable anchor
+ * so many symbols can share a page. */
+export default function (
+ // deno-lint-ignore no-explicit-any
+ { comp, symbol }: { comp: any; symbol: GroupSymbol },
+) {
+ const group = symbol.symbolGroup;
+ return (
+
+ {group.symbols.map((item, i) => (
+
+
+
+ {item.kind.title_lowercase}
+ {" "}
+
+ {group.name}
+
+
+ {item.subtitle && (
+
+ {item.subtitle.kind === "class"
+ ?
+ : (
+
+ )}
+
+ )}
+ {(item.tags && item.tags.length > 0) && (
+
+ {item.tags.map((tag) => (
+
+ ))}
+
+ )}
+
+ {i === 0 &&
}
+
+ {item.deprecated && (
+
+ )}
+
+
+ {item.content.map((contentItem) => (
+ contentItem.kind === "function"
+ ?
+ :
+ ))}
+
+
+ ))}
+
+ );
+}
diff --git a/reference/_lib/group.ts b/reference/_lib/group.ts
new file mode 100644
index 000000000..c8b635bb3
Binary files /dev/null and b/reference/_lib/group.ts differ
diff --git a/reference/preview.page.ts b/reference/preview.page.ts
new file mode 100644
index 000000000..002c8a571
--- /dev/null
+++ b/reference/preview.page.ts
@@ -0,0 +1,330 @@
+/**
+ * PROTOTYPE: renders node:http directly from raw `deno doc --json` output
+ * (doc nodes) instead of @deno/doc's generateHtmlAsJSON, in the prose-first
+ * style of the Node.js documentation.
+ *
+ * Served at /api/preview/node-http/ alongside the regular pages so the two
+ * renderers can be compared. Generate the input with:
+ *
+ * deno doc --json reference_gen/types/node/node__http.d.ts \
+ * > reference_gen/gen/raw-node-http.json
+ */
+
+export const layout = "raw.tsx";
+
+const SOURCE = "reference_gen/gen/raw-node-http.json";
+const MODULE = "http";
+
+// ---------------------------------------------------------------------------
+// Raw doc-node shapes (the subset we consume).
+// deno-lint-ignore-file no-explicit-any
+
+interface JsDoc {
+ doc?: string;
+ tags?: { kind: string; doc?: string; name?: string }[];
+}
+
+interface Declaration {
+ kind: string;
+ jsDoc?: JsDoc;
+ def?: any;
+}
+
+interface RawSymbol {
+ name: string;
+ declarations: Declaration[];
+}
+
+// ---------------------------------------------------------------------------
+// Render-friendly shapes consumed by _includes/reference/preview.tsx.
+
+export interface PreviewMember {
+ /** e.g. "server.setTimeout([msecs][, callback])" or "request.aborted" */
+ signature: string;
+ anchor: string;
+ since: string | null;
+ deprecated: string | null;
+ doc: string | null;
+ returnType: string | null;
+}
+
+export interface PreviewSymbol {
+ kind: string;
+ /** e.g. "Class: http.Server" */
+ heading: string;
+ /** e.g. "http.request(options[, callback])" */
+ signature: string | null;
+ anchor: string;
+ since: string | null;
+ deprecated: string | null;
+ doc: string | null;
+ returnType: string | null;
+ members: PreviewMember[];
+}
+
+function tag(jsDoc: JsDoc | undefined, kind: string): string | null {
+ const t = jsDoc?.tags?.find((t) => t.kind === kind);
+ return t ? (t.doc ?? "") : null;
+}
+
+/** Plain-text type from a doc-node type AST, good enough for inline display. */
+function typeText(t: any): string {
+ if (!t) return "";
+ if (t.repr) return t.repr;
+ switch (t.kind) {
+ case "keyword":
+ case "literal":
+ return String(t.value?.string ?? t.value ?? t.repr ?? "");
+ case "typeRef": {
+ const name = t.value?.typeName ?? "";
+ const params = t.value?.typeParams?.map(typeText).join(", ");
+ return params ? `${name}<${params}>` : name;
+ }
+ case "union":
+ return t.value.map(typeText).join(" | ");
+ case "intersection":
+ return t.value.map(typeText).join(" & ");
+ case "array":
+ return `${typeText(t.value)}[]`;
+ case "parenthesized":
+ return `(${typeText(t.value)})`;
+ case "fnOrConstructor":
+ return "Function";
+ case "typeLiteral":
+ return "Object";
+ default:
+ return t.kind ?? "";
+ }
+}
+
+function paramName(p: any): string {
+ if (p.kind === "rest") return `...${paramName(p.arg)}`;
+ if (p.kind === "assign") return paramName(p.left);
+ if (p.kind === "object") return "options";
+ if (p.kind === "array") return "values";
+ return p.name ?? "arg";
+}
+
+/** Node-style call signature: request(options[, callback]) */
+function callSignature(prefix: string, name: string, params: any[]): string {
+ let out = "";
+ let openedBrackets = 0;
+ for (let i = 0; i < params.length; i++) {
+ const p = params[i];
+ const optional = p.optional || p.kind === "assign";
+ const pname = paramName(p);
+ if (i === 0) {
+ out += optional ? `[${pname}` : pname;
+ if (optional) openedBrackets++;
+ } else if (optional) {
+ out += `[, ${pname}`;
+ openedBrackets++;
+ } else {
+ out += `, ${pname}`;
+ }
+ }
+ out += "]".repeat(openedBrackets);
+ return `${prefix}${name}(${out})`;
+}
+
+function functionMember(
+ prefix: string,
+ name: string,
+ decl: { jsDoc?: JsDoc; functionDef?: any; optional?: boolean },
+ anchorPrefix: string,
+): PreviewMember {
+ return {
+ signature: callSignature(prefix, name, decl.functionDef?.params ?? []),
+ anchor: `${anchorPrefix}.${name}`,
+ since: tag(decl.jsDoc, "since"),
+ deprecated: tag(decl.jsDoc, "deprecated"),
+ doc: decl.jsDoc?.doc ?? null,
+ returnType: typeText(decl.functionDef?.returnType) || null,
+ };
+}
+
+function propertyMember(
+ prefix: string,
+ name: string,
+ decl: { jsDoc?: JsDoc; tsType?: any },
+ anchorPrefix: string,
+): PreviewMember {
+ const type = typeText(decl.tsType);
+ return {
+ signature: `${prefix}${name}${type ? `: ${type}` : ""}`,
+ anchor: `${anchorPrefix}.${name}`,
+ since: tag(decl.jsDoc, "since"),
+ deprecated: tag(decl.jsDoc, "deprecated"),
+ doc: decl.jsDoc?.doc ?? null,
+ returnType: null,
+ };
+}
+
+/** Members sorted documented-first, then alphabetically. */
+function sortMembers(members: PreviewMember[]): PreviewMember[] {
+ return members.sort((a, b) =>
+ Number(Boolean(b.doc)) - Number(Boolean(a.doc)) ||
+ a.signature.localeCompare(b.signature)
+ );
+}
+
+function transformSymbol(symbol: RawSymbol): PreviewSymbol | null {
+ const decl = symbol.declarations[0];
+ if (!decl) return null;
+ const name = symbol.name;
+ const jsDoc = decl.jsDoc;
+ const base: Omit = {
+ anchor: name,
+ since: tag(jsDoc, "since"),
+ deprecated: tag(jsDoc, "deprecated"),
+ doc: jsDoc?.doc ?? null,
+ returnType: null,
+ members: [],
+ };
+
+ switch (decl.kind) {
+ case "function":
+ return {
+ ...base,
+ kind: "function",
+ heading: callSignature(`${MODULE}.`, name, decl.def?.params ?? []),
+ signature: null,
+ returnType: typeText(decl.def?.returnType) || null,
+ };
+ case "class": {
+ const def = decl.def ?? {};
+ const instance = name.charAt(0).toLowerCase() + name.slice(1);
+ const members: PreviewMember[] = [];
+ for (const ctor of def.constructors ?? []) {
+ members.push({
+ signature: callSignature(
+ "new ",
+ `${MODULE}.${name}`,
+ ctor.params ?? [],
+ ),
+ anchor: `${name}.constructor`,
+ since: tag(ctor.jsDoc, "since"),
+ deprecated: null,
+ doc: ctor.jsDoc?.doc ?? null,
+ returnType: null,
+ });
+ }
+ const props = (def.properties ?? []).filter((p: any) => !p.isStatic);
+ const methods = (def.methods ?? []).filter((m: any) => !m.isStatic);
+ const seen = new Set();
+ const propAndMethodMembers: PreviewMember[] = [];
+ for (const p of props) {
+ propAndMethodMembers.push(
+ propertyMember(`${instance}.`, p.name, p, name),
+ );
+ }
+ for (const m of methods) {
+ if (seen.has(m.name)) continue; // collapse overloads to the first
+ seen.add(m.name);
+ propAndMethodMembers.push(
+ functionMember(`${instance}.`, m.name, m, name),
+ );
+ }
+ members.push(...sortMembers(propAndMethodMembers));
+ return {
+ ...base,
+ kind: "class",
+ heading: `Class: ${MODULE}.${name}`,
+ signature: null,
+ members,
+ };
+ }
+ case "interface": {
+ const def = decl.def ?? {};
+ const members: PreviewMember[] = [];
+ for (const p of def.properties ?? []) {
+ members.push(propertyMember("", p.name, p, name));
+ }
+ for (const m of def.methods ?? []) {
+ members.push(functionMember("", m.name, m, name));
+ }
+ return {
+ ...base,
+ kind: "interface",
+ heading: `Interface: ${name}`,
+ signature: null,
+ members: sortMembers(members),
+ };
+ }
+ case "typeAlias":
+ return {
+ ...base,
+ kind: "typeAlias",
+ heading: `Type: ${name}`,
+ signature: `type ${name} = ${typeText(decl.def?.tsType)}`,
+ };
+ case "variable":
+ return {
+ ...base,
+ kind: "variable",
+ heading: `${MODULE}.${name}`,
+ signature: null,
+ returnType: typeText(decl.def?.tsType) || null,
+ };
+ default:
+ return null;
+ }
+}
+
+const KIND_ORDER = ["class", "function", "interface", "typeAlias", "variable"];
+const KIND_TITLES: Record = {
+ class: "Classes",
+ function: "Functions",
+ interface: "Interfaces",
+ typeAlias: "Type aliases",
+ variable: "Variables",
+};
+
+export default function* () {
+ let raw: { nodes: Record };
+ try {
+ raw = JSON.parse(Deno.readTextFileSync(SOURCE));
+ } catch {
+ // Prototype input not generated; skip silently.
+ return;
+ }
+
+ const symbols = Object.values(raw.nodes)[0].symbols
+ .map(transformSymbol)
+ .filter((s): s is PreviewSymbol => s !== null)
+ .sort((a, b) =>
+ KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) ||
+ a.anchor.localeCompare(b.anchor)
+ );
+
+ const toc = [];
+ for (const kind of KIND_ORDER) {
+ const group = symbols.filter((s) => s.kind === kind);
+ if (group.length === 0) continue;
+ toc.push({ level: 1, content: KIND_TITLES[kind], anchor: `kind-${kind}` });
+ for (const s of group) {
+ toc.push({ level: 2, content: s.heading, anchor: s.anchor });
+ }
+ }
+
+ yield {
+ url: "/api/preview/node-http/",
+ title: "node:http (renderer preview)",
+ description:
+ "Prototype of rendering node:http from raw doc nodes in a Node.js-style prose-first format.",
+ layout: "reference/preview.tsx",
+ data: {
+ module: MODULE,
+ usage: `import * as http from "node:${MODULE}";`,
+ symbols,
+ kindOrder: KIND_ORDER,
+ kindTitles: KIND_TITLES,
+ toc_ctx: {
+ usages: null,
+ top_symbols: null,
+ document_navigation_str: null,
+ document_navigation: toc,
+ },
+ },
+ };
+}
diff --git a/reference/reference.page.ts b/reference/reference.page.ts
index ea34ab154..8dcc16deb 100644
--- a/reference/reference.page.ts
+++ b/reference/reference.page.ts
@@ -1,4 +1,11 @@
import type { Page } from "@deno/doc";
+import { type ApiKind, buildReference, type Group } from "./_lib/group.ts";
+import denoCategories from "../reference_gen/deno-categories.json" with {
+ type: "json",
+};
+import webCategories from "../reference_gen/web-categories.json" with {
+ type: "json",
+};
export const layout = "raw.tsx";
@@ -21,123 +28,229 @@ export const sidebar = [
},
];
-const kinds = [
- { path: "reference_gen/gen/deno.json", name: "Deno" },
- { path: "reference_gen/gen/web.json", name: "Web" },
- { path: "reference_gen/gen/node.json", name: "Node" },
-] as const;
+const kinds: { path: string; kind: ApiKind; title: string }[] = [
+ { path: "reference_gen/gen/deno.json", kind: "deno", title: "Deno" },
+ { path: "reference_gen/gen/web.json", kind: "web", title: "Web" },
+ { path: "reference_gen/gen/node.json", kind: "node", title: "Node" },
+];
export default function* () {
+ if (Deno.env.get("SKIP_REFERENCE") === "1") {
+ console.warn("โญ๏ธ Reference docs generation skipped (SKIP_REFERENCE set)");
+ return;
+ }
+
+ const jsons = {} as Record>;
try {
- if (Deno.env.get("SKIP_REFERENCE") === "1") {
- console.warn("โญ๏ธ Reference docs generation skipped (SKIP_REFERENCE set)");
- return;
+ for (const { path, kind } of kinds) {
+ console.log(`๐ Loading ${kind} reference docs from ${path}...`);
+ jsons[kind] = JSON.parse(Deno.readTextFileSync(path));
+ console.log(
+ `โ
Loaded ${kind} reference docs (${
+ Object.keys(jsons[kind]).length
+ } entries)`,
+ );
+ }
+ } catch (ex) {
+ console.warn(
+ "โ ๏ธ Reference docs were not generated. Run 'deno task generate:reference' first.",
+ ex,
+ );
+ return;
+ }
+
+ const { groups, redirects, warnings, rewritePage } = buildReference(jsons, {
+ deno: denoCategories,
+ web: webCategories,
+ });
+
+ if (warnings.length > 0) {
+ const logPath = "reference_gen/gen/reference-warnings.log";
+ Deno.writeTextFileSync(logPath, warnings.join("\n") + "\n");
+ console.warn(
+ `โ ๏ธ ${warnings.length} reference link warnings (dead links in source docs). Full list: ${logPath}`,
+ );
+ }
+
+ for (const { kind, title } of kinds) {
+ for (const group of groups[kind]) {
+ yield {
+ url: group.url,
+ title: `${group.title} - ${title} documentation`,
+ description: descriptionFor(group),
+ layout: "reference/multiSymbol.tsx",
+ data: {
+ ...group,
+ toc_ctx: {
+ usages: null,
+ top_symbols: null,
+ document_navigation_str: null,
+ document_navigation: group.toc,
+ },
+ },
+ };
}
- for (const { path, name } of kinds) {
- console.log(`๐ Loading ${name} reference docs from ${path}...`);
-
- let json: Record;
- try {
- const fileContent = Deno.readTextFileSync(path);
- json = JSON.parse(fileContent);
- console.log(
- `โ
Successfully loaded ${name} reference docs (${
- Object.keys(json).length
- } entries)`,
- );
- } catch (readError) {
- console.error(`โ Failed to read ${path}:`, readError);
- console.error(` Current working directory: ${Deno.cwd()}`);
-
- // Check if the file exists
- try {
- const stat = Deno.statSync(path);
- console.error(
- ` File exists but read failed. Size: ${stat.size} bytes`,
- );
- } catch {
- console.error(` File does not exist at: ${path}`);
-
- // Check if the gen directory exists
- try {
- Deno.statSync("reference_gen/gen");
- console.error(` Gen directory exists, contents:`);
- for (const entry of Deno.readDirSync("reference_gen/gen")) {
- console.error(
- ` - ${entry.name} (${entry.isFile ? "file" : "dir"})`,
- );
- }
- } catch {
- console.error(
- ` Gen directory does not exist at reference_gen/gen`,
- );
- console.error(
- ` Run 'deno task generate:reference' to generate the reference docs`,
- );
- }
- }
- throw readError;
- }
- for (
- const [filepath, content] of Object.entries(json)
- ) {
- if (content.kind === "search") {
- continue;
- }
-
- // Skip generating index pages since we have static versions
- if (
- (name === "Deno" || name === "Web" || name === "Node") &&
- filepath === "./index.json"
- ) {
- continue;
- }
-
- const trailingLength = filepath.endsWith("index.json")
- ? -"index.json".length
- : -".json".length;
-
- // Remove leading "./" if present
- let normalizedPath = filepath.slice(0, trailingLength);
- if (normalizedPath.startsWith("./")) {
- normalizedPath = normalizedPath.slice(2);
- }
-
- const url = `/api/${name.toLowerCase()}/${normalizedPath}`;
-
- if ("path" in content) {
- // TODO: handle redirects in a more integrated manner
-
- yield {
- url,
- content:
- ` `,
- };
-
- continue;
- }
-
- let layout;
- if (content.kind === "IndexCtx") {
- layout = "index";
- } else if (content.kind === "AllSymbolsCtx") {
- layout = "allSymbols";
- } else if (content.kind === "SymbolPageCtx") {
- layout = "symbol";
- } else {
- throw `unknown page kind: ${(content as { kind: string }).kind}`;
- }
-
- yield {
- url,
- title: content.html_head_ctx.title,
- layout: `reference/${layout}.tsx`,
- data: content,
- };
- }
+ // The "view all symbols" page, with links rewritten to the new URLs.
+ for (const [filepath, content] of Object.entries(jsons[kind])) {
+ if (content.kind !== "AllSymbolsCtx") continue;
+ const normalized = filepath.replace(/^\.\//, "").replace(/\.json$/, "");
+ yield {
+ url: `/api/${kind}/${normalized}`,
+ title: `All Symbols - ${title} documentation`,
+ layout: "reference/allSymbols.tsx",
+ data: rewritePage(kind, filepath, content),
+ };
}
- } catch (ex) {
- console.warn("โ ๏ธ Reference docs were not generated." + ex);
}
+
+ // Landing page per API kind: a dense Ctrl+F-able index of every symbol.
+ // The hand-written guides that previously lived at these URLs moved to
+ // /api//about/ (see api/*/index.md).
+ const kindMeta = {
+ deno: {
+ title: "Deno APIs",
+ intro:
+ "Every API in the global Deno namespace, by category. Use your browser's find-in-page to locate a symbol, then click through for full documentation.",
+ aboutLabel: "About the Deno namespace",
+ },
+ web: {
+ title: "Web APIs",
+ intro:
+ "Every web platform API implemented by Deno, by category. Use your browser's find-in-page to locate a symbol, then click through for full documentation.",
+ aboutLabel: "About web platform support",
+ },
+ node: {
+ title: "Node APIs",
+ intro:
+ "Every Node.js built-in module supported by Deno and its exports. Use your browser's find-in-page to locate a symbol, then click through for full documentation.",
+ aboutLabel: "About Node.js compatibility",
+ },
+ } as const;
+
+ for (const { kind } of kinds) {
+ const meta = kindMeta[kind];
+ yield {
+ url: `/api/${kind}/`,
+ title: `${meta.title} reference`,
+ description: meta.intro,
+ fullWidth: true,
+ layout: "reference/apiIndex.tsx",
+ data: {
+ title: meta.title,
+ intro: meta.intro,
+ aboutUrl: `/api/${kind}/about/`,
+ aboutLabel: meta.aboutLabel,
+ allSymbolsUrl: `/api/${kind}/all_symbols`,
+ groups: groups[kind].map((group) => ({
+ title: group.title === "Uncategorized" ? "Other APIs" : (
+ kind === "node" ? `node:${group.slug}` : group.title
+ ),
+ url: group.url,
+ slug: group.slug.replace(/\//g, "-"),
+ group,
+ })),
+ },
+ };
+ }
+
+ // The /api/ hub: tile grids for every API group, derived from the same
+ // grouping data as the pages themselves.
+ yield {
+ url: "/api/",
+ title: "API reference",
+ description:
+ "Complete API reference for Deno: built-in Deno APIs, supported Web APIs, and Node.js compatibility modules.",
+ fullWidth: true,
+ layout: "reference/landing.tsx",
+ data: {
+ intro:
+ "The complete API reference for Deno: runtime APIs available in the Deno namespace, supported web platform APIs, and the Node.js built-in modules Deno provides for compatibility.",
+ sections: [
+ {
+ key: "deno",
+ title: "Deno APIs",
+ description:
+ "Non-standard APIs in the global Deno namespace: file system, network, subprocesses, testing, FFI, and more.",
+ href: "/api/deno/",
+ allSymbolsHref: "/api/deno/all_symbols",
+ tiles: categoryTiles(groups.deno),
+ },
+ {
+ key: "web",
+ title: "Web APIs",
+ description:
+ "Web platform standards implemented by Deno: fetch, streams, workers, crypto, and other browser-compatible APIs.",
+ href: "/api/web/",
+ allSymbolsHref: "/api/web/all_symbols",
+ tiles: categoryTiles(groups.web),
+ },
+ {
+ key: "node",
+ title: "Node APIs",
+ description:
+ "Node.js built-in modules supported in Deno for backwards compatibility, importable via the node: scheme.",
+ href: "/api/node/",
+ allSymbolsHref: "/api/node/all_symbols",
+ tiles: groups.node
+ .map((group) => ({
+ title: `node:${group.slug}`,
+ href: group.url,
+ description: tileDescription(group.docs),
+ }))
+ .sort((a, b) => a.title.localeCompare(b.title)),
+ dense: true,
+ },
+ ],
+ },
+ };
+
+ console.log(
+ `๐ Reference pages: ${
+ Object.values(groups).reduce((a, g) => a + g.length, 0)
+ } grouped pages, ${Object.keys(redirects).length} redirects`,
+ );
+
+ // Consumed by middleware/redirects.ts to 301 old per-symbol URLs to their
+ // anchor on the grouped pages.
+ yield {
+ url: "/api/_redirects.json",
+ content: JSON.stringify(redirects),
+ };
+}
+
+/** First sentence of a rendered HTML docs block, as plain text. */
+function tileDescription(docs: string | null): string | null {
+ if (!docs) return null;
+ const text = docs.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
+ if (text.length === 0) return null;
+ const sentence = text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text;
+ return sentence.length > 140 ? sentence.slice(0, 137) + "..." : sentence;
+}
+
+function categoryTiles(groups: Group[]) {
+ return groups
+ .map((group) => ({
+ // The generated catch-all category reads poorly on a landing page.
+ title: group.title === "Uncategorized" ? "Other APIs" : group.title,
+ href: group.url,
+ description: tileDescription(group.docs),
+ }))
+ .sort((a, b) =>
+ (a.title === "Other APIs" ? 1 : 0) - (b.title === "Other APIs" ? 1 : 0) ||
+ a.title.localeCompare(b.title)
+ );
+}
+
+function descriptionFor(group: Group): string {
+ const fallbacks: Record = {
+ deno: `API reference for Deno's built-in ${group.title} APIs.`,
+ web: `API reference for the ${group.title} Web APIs available in Deno.`,
+ node: `API reference for the node:${group.slug} module in Deno.`,
+ };
+ const fallback = fallbacks[group.apiKind];
+ if (!group.docs) return fallback;
+ const text = group.docs.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
+ if (text.length === 0) return fallback;
+ return text.length > 160 ? text.slice(0, 157) + "..." : text;
}
diff --git a/server.ts b/server.ts
index 749821a8a..b0afa2415 100644
--- a/server.ts
+++ b/server.ts
@@ -10,7 +10,9 @@ import expires from "lume/middlewares/expires.ts";
import createLlmsFilesMiddleware from "./middleware/llmsFiles.ts";
import createMarkdownSourceMiddleware from "./middleware/markdownSource.ts";
-export const server = new Server({ root: "_site" });
+const port = Number(Deno.env.get("PORT") ?? 8000);
+
+export const server = new Server({ root: "_site", port });
server.use(redirectsMiddleware);
server.use(createMarkdownSourceMiddleware({ root: "_site" }));
@@ -28,4 +30,4 @@ server.use(expires({
server.start();
-console.log("Listening on http://localhost:8000");
+console.log(`Listening on http://localhost:${port}`);