Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ tmp/
src/generated/
public/_astro/
.preview-scope.json
# Derived reference pages are regenerated from a ClickHouse snapshot. Keep the
# snapshot renderer and archive tooling in Git, but never commit its 24MB local
# preview tree.
reference-prototype/
# Local-only Mintlify leftovers
package-lock.json
.nimbus/
Expand All @@ -17,3 +21,4 @@ products/clickhouse-private/*
# wrangler dev state and the dist symlink used for local Worker tests
.wrangler/
dist
.reference-snapshots/
51 changes: 44 additions & 7 deletions _site/customizations/webterminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,15 @@
+ '</svg>';

function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
var style = document.createElement('style');
style.id = STYLE_ID;
var style = document.getElementById(STYLE_ID);
// BaseLayout supplies a persistent empty style shell so that the terminal
// does not briefly lose its fixed positioning during Astro navigation.
if (style && style.dataset.webterminalReady === 'true') return;
if (!style) {
style = document.createElement('style');
style.id = STYLE_ID;
document.head.appendChild(style);
}
style.textContent = ''
// Keep the collapsed tray fixed across the viewport. The document and sidebar deliberately
// keep their full height and scroll behind it; opening the panel extends the overlay upward.
Expand Down Expand Up @@ -104,7 +110,7 @@
+ '#' + ACTION_ID + ' svg { width: 16px; height: 16px; }'
+ '#' + PANEL_ID + '.' + OPEN_CLASS + ' #' + ACTION_ID + ' svg { transform: rotate(180deg); }'
+ '@media (max-width: ' + (DESKTOP_MIN_WIDTH - 1) + 'px) { #' + DOCK_ID + ' { display: none; } }';
document.head.appendChild(style);
style.dataset.webterminalReady = 'true';
}

function maxTerminalHeight() {
Expand Down Expand Up @@ -159,8 +165,40 @@
if (panel) return;
injectStyles();

dock = document.createElement('div');
dock.id = DOCK_ID;
// Use BaseLayout's persistent dock when available. The dynamic terminal
// panel then survives a client-side page swap instead of being rebuilt.
dock = document.getElementById(DOCK_ID);
if (!dock) {
dock = document.createElement('div');
dock.id = DOCK_ID;
document.body.appendChild(dock);
}

// BaseLayout renders the collapsed tray in the initial HTML. Enhance that
// stable shell instead of removing and recreating it once this deferred
// script has arrived.
panel = document.getElementById(PANEL_ID);
if (panel) {
viewport = document.getElementById(VIEWPORT_ID);
resizer = document.getElementById(RESIZER_ID);
toggle = document.getElementById(TOGGLE_ID);
action = document.getElementById(ACTION_ID);
if (!viewport || !resizer || !toggle || !action) {
panel = null;
} else {
viewport.addEventListener('wheel', function (e) {
if (!terminalOpen) return;
e.preventDefault();
e.stopPropagation();
}, {passive: false});
resizer.addEventListener('pointerdown', startResize);
resizer.addEventListener('touchstart', function (e) { e.preventDefault(); });
toggle.addEventListener('click', toggleTerminal);
action.addEventListener('click', toggleTerminal);
updateControls();
return;
}
}

panel = document.createElement('section');
panel.id = PANEL_ID;
Expand Down Expand Up @@ -210,7 +248,6 @@

panel.appendChild(tray);
dock.appendChild(panel);
document.body.appendChild(dock);
updateControls();
}

Expand Down
20 changes: 19 additions & 1 deletion astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { tableScroll } from "@cloudflare/nimbus-docs/markdown";
import { satteri } from "@astrojs/markdown-satteri";
import { rebaseUrls } from "./src/plugins/satteri-rebase-urls";
import { mermaidBlocks } from "./src/plugins/satteri-mermaid";
import { katexMathMarkers, katexMathRenderer } from "./src/plugins/satteri-katex";
import { SATTERI_FEATURES } from "./src/plugins/satteri-features";
import { readScope } from "./src/lib/scope";
import type { HastPluginDefinition } from "satteri";
Expand Down Expand Up @@ -120,7 +121,8 @@ export default defineConfig({
// Nimbus). Nimbus's own hast plugins must be re-added here.
processor: satteri({
features: SATTERI_FEATURES,
hastPlugins: [nimbusTableScroll, rebaseUrls({ base: BASE, remoteMounts }), mermaidBlocks()],
mdastPlugins: [katexMathMarkers],
hastPlugins: [nimbusTableScroll, rebaseUrls({ base: BASE, remoteMounts }), mermaidBlocks(), katexMathRenderer],
}),
// The prepared Markdown surfaces are for agents rather than the web
// renderer. Preserve agent-only content and reduce the path selector
Expand All @@ -143,6 +145,22 @@ export default defineConfig({
}),
],
vite: {
// In local development, expose the separately-running archived-artifact
// server through the same origin as the docs app. This avoids cross-origin
// browser restrictions while keeping the component contract identical to
// production, where the website worker serves this prefix from storage.
server: {
proxy: {
// Astro removes `base` before Vite evaluates the proxy matcher, so
// the browser's `/docs/reference-artifacts/...` request is seen here
// as `/reference-artifacts/...`.
"/reference-artifacts": {
target: "http://127.0.0.1:4323",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/reference-artifacts/, ""),
},
},
},
// The Vite dependency optimizer currently resolves React's development
// JSX runtime to its production implementation in this project. The
// production runtime intentionally leaves `jsxDEV` undefined, which made
Expand Down
169 changes: 169 additions & 0 deletions bin/archive-reference-bodies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Local release-archive prototype.
*
* It reads complete, already rendered reference pages and writes only their
* document-article HTML plus navigation data and a small manifest. A release
* job supplies REFERENCE_ARCHIVE_BUILD_DIR (the completed static output); the
* HTTP input is retained solely for the fast local prototype.
*/
import fs from "node:fs";
import path from "node:path";

const version = process.env.REFERENCE_ARCHIVE_VERSION ?? "26.9";
const origin = (process.env.REFERENCE_ARCHIVE_ORIGIN ?? "http://127.0.0.1:4321/docs").replace(/\/$/, "");
const sourceRoot = path.join(process.cwd(), "reference-prototype", version);
const finalOutputRoot = path.resolve(process.env.REFERENCE_ARCHIVE_OUTPUT_DIR ?? "/private/tmp/reference-artifacts", version, "en");
const outputRoot = `${finalOutputRoot}.staging`;
const buildRoot = process.env.REFERENCE_ARCHIVE_BUILD_DIR
? path.resolve(process.env.REFERENCE_ARCHIVE_BUILD_DIR)
: undefined;
const articleOpen = '<article class="docs-content max-w-none">';

function files(directory: string): string[] {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? files(target) : entry.name.endsWith(".mdx") ? [target] : [];
});
}

function routeFor(file: string) {
const relative = path.relative(sourceRoot, file).replace(/\.mdx$/, "").split(path.sep).join("/");
return relative === "index" ? "" : relative.replace(/\/index$/, "");
}

function titleFor(file: string) {
const source = fs.readFileSync(file, "utf8");
const match = source.match(/^title:\s*("(?:[^"\\]|\\.)*")\s*$/m);
return match ? JSON.parse(match[1]) as string : routeFor(file) || "Reference";
}

const settingsExplorerRoutes: Record<string, string> = {
"settings/session-settings": "session-settings",
"settings/server-settings/settings": "server-settings",
"settings/merge-tree-settings": "merge-tree-settings",
};

function normalizeBody(body: string, route: string) {
// The archive is a content contract, never an Astro build artifact. Strip
// component scope IDs and turn callouts into a stable semantic marker that
// the live reference shell can style in future releases.
const explorer = settingsExplorerRoutes[route];
const normalized = body
.replace(/\sdata-astro-cid-[^\s=>]+(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/g, "")
// Client islands are build artifacts. A historical body is inserted into
// the current shell, where these scripts cannot (and must not) hydrate.
// Replace the settings island with a stable semantic mount point instead.
.replace(/<style>astro-island,astro-slot,astro-static-slot\{display:contents\}<\/style>/gi, "")
.replace(/<script\b[\s\S]*?<\/script>/gi, "")
.replace(/<astro-island\b[\s\S]*?<\/astro-island>/gi, explorer
? `<div data-reference-settings-explorer data-reference-settings-index="${explorer}"></div>`
: "")
.replace(/<aside\b([^>]*)>/gi, (_match, attributes: string) => {
const label = attributes.match(/\baria-label=(?:"([^"]*)"|'([^']*)')/i)?.slice(1).find(Boolean)?.toLowerCase() ?? "note";
const type = /^(info|tip|caution|danger|note)\b/.exec(label)?.[1] ?? "note";
return `<aside data-reference-callout="${type}">`;
});
return normalized;
}

function bodyFor(html: string, route: string) {
const start = html.indexOf(articleOpen);
const end = start === -1 ? -1 : html.indexOf("</article>", start + articleOpen.length);
if (start === -1 || end === -1) throw new Error(`Could not extract rendered article from ${route || "reference root"}`);
return normalizeBody(html.slice(start + articleOpen.length, end).trim(), route) + "\n";
}

function renderedFileFor(route: string) {
if (!buildRoot) return undefined;
// Astro normally emits the site's base path *outside* `outDir`, but CI
// artifact assembly sometimes passes the enclosing directory instead. Be
// explicit about the two supported roots rather than guessing routes.
const candidates = [
path.join(buildRoot, "reference", version, route, "index.html"),
path.join(buildRoot, "docs", "reference", version, route, "index.html"),
];
return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
}

async function renderedPageFor(route: string) {
const renderedFile = renderedFileFor(route);
if (renderedFile) {
if (!fs.existsSync(renderedFile)) {
throw new Error(`Static reference output is missing ${path.relative(buildRoot!, renderedFile)}`);
}
return fs.readFileSync(renderedFile, "utf8");
}

const response = await fetch(`${origin}/reference/${version}${route ? `/${route}` : ""}`);
if (!response.ok) throw new Error(`Could not archive ${route || "reference root"}: ${response.status}`);
return response.text();
}

function writeNavigation() {
const navigationFile = process.env.REFERENCE_ARCHIVE_NAVIGATION_FILE;
let navigation: unknown;
if (navigationFile) {
navigation = JSON.parse(fs.readFileSync(path.resolve(navigationFile), "utf8"));
} else {
// This fallback exists only for the local fixture. Release CI supplies a
// snapshot-derived navigation JSON explicitly; the archive never needs
// the historical Astro UI to render it.
const registry = JSON.parse(fs.readFileSync(path.join(process.cwd(), "src/generated/reference-prototype.versions.json"), "utf8")) as {
versions: Record<string, { navigation: unknown }>;
};
navigation = registry.versions[version]?.navigation;
}
if (navigation === undefined) throw new Error(`No navigation data was supplied for ${version}`);
fs.writeFileSync(path.join(outputRoot, "navigation.json"), JSON.stringify({ schemaVersion: 1, version, locale: "en", navigation }, null, 2) + "\n");
}

function writeSettingsIndexes() {
// The explorer is snapshot data, just like navigation. Copy only the
// immutable JSON index; the live shell provides its React/UI implementation.
const source = path.join(process.cwd(), ".remote", "public-build", "reference-settings-index", version);
if (!fs.existsSync(source)) throw new Error(`Settings explorer indexes are missing for ${version}: ${source}`);
fs.cpSync(source, path.join(outputRoot, "settings-index"), { recursive: true });
}

function writeVersionsIndex() {
const archiveRoot = path.dirname(path.dirname(finalOutputRoot));
const versions = fs.readdirSync(archiveRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^\d+(?:\.\d+)+$/.test(entry.name))
.map((entry) => entry.name)
.sort((left, right) => right.localeCompare(left, undefined, { numeric: true }))
.map((key) => ({ version: key, label: key, routeSlug: key.replace(/\./g, "-") }));
fs.writeFileSync(path.join(archiveRoot, "versions.json"), JSON.stringify({ schemaVersion: 1, versions }, null, 2) + "\n");
}

const pages = files(sourceRoot).map((file) => ({ file, route: routeFor(file), title: titleFor(file) }));
fs.rmSync(outputRoot, { recursive: true, force: true });

const concurrency = 8;
let next = 0;
async function worker() {
while (next < pages.length) {
const page = pages[next++];
const output = path.join(outputRoot, page.route, "body.html");
fs.mkdirSync(path.dirname(output), { recursive: true });
fs.writeFileSync(output, bodyFor(await renderedPageFor(page.route), page.route));
}
}

await Promise.all(Array.from({ length: concurrency }, worker));
fs.mkdirSync(outputRoot, { recursive: true });
fs.writeFileSync(path.join(outputRoot, "manifest.json"), JSON.stringify({
schemaVersion: 1,
version,
locale: "en",
pages: pages.map(({ route, title }) => ({ route, title })),
}, null, 2) + "\n");
writeNavigation();
writeSettingsIndexes();
// Publish only after every fragment, manifest, and navigation payload has
// been produced. An interrupted release job therefore leaves the prior
// release archive intact.
fs.rmSync(finalOutputRoot, { recursive: true, force: true });
fs.renameSync(outputRoot, finalOutputRoot);
writeVersionsIndex();

console.log(`archive-reference-bodies: wrote ${pages.length} body fragments and navigation for ${version} to ${finalOutputRoot}`);
Loading
Loading