diff --git a/_config.yml b/_config.yml
index 0eee6dfae2..f5e8eebb00 100644
--- a/_config.yml
+++ b/_config.yml
@@ -81,7 +81,6 @@ available_versions:
- v0.62
- v0.63
collections:
- docs: { output: true }
community-posts: { output: true }
defaults:
- {
diff --git a/astro.config.mjs b/astro.config.mjs
index e762ba5cf6..ee119ba7b9 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -1,5 +1,33 @@
// @ts-check
-import { defineConfig } from 'astro/config';
+import { defineConfig } from "astro/config";
+import { viteStaticCopy } from "vite-plugin-static-copy";
+import { noopMarkdownProcessor } from "./src/lib/markdown/noopMarkdownProcessor";
// https://astro.build/config
-export default defineConfig({});
+export default defineConfig({
+ vite: {
+ plugins: [
+ viteStaticCopy({
+ targets: [
+ {
+ // TODO: What other extensions?
+ src: "_docs/**/*.{jpg,png}",
+ dest: "docs",
+ rename: { stripBase: 1 }, // strips `_docs/`
+ },
+ ],
+ }),
+ ],
+ },
+ markdown: {
+ // Use `getMarkdownRenderer` instead for faster dev builds. There are
+ // thousands of docs md files, and astro processes the markdown for all of
+ // them even if you don't even navigate to a markdown-generated page. Also,
+ // docs md files use liquid syntax which must be processed before the
+ // markdown is converted to html. Processing liquid as part of a satteri
+ // plugin would be unnecessarily and frustratingly slow for dev builds
+ // since it would need to resolve all the includes for thousands of
+ // markdown files.
+ processor: noopMarkdownProcessor,
+ },
+});
diff --git a/bun.lock b/bun.lock
index 254dbdbf03..607261e08f 100644
--- a/bun.lock
+++ b/bun.lock
@@ -44,6 +44,7 @@
"stylelint": "^14.9.0",
"stylelint-config-standard-scss": "^4.0.0",
"stylelint-order": "^5.0.0",
+ "vite-plugin-static-copy": "^4.1.1",
"wait-on": "^9.0.10",
},
},
@@ -514,6 +515,8 @@
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
+ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
+
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
@@ -564,7 +567,7 @@
"chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="],
- "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
@@ -974,6 +977,8 @@
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
+ "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
+
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
"is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="],
@@ -1348,6 +1353,8 @@
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+ "p-map": ["p-map@7.0.5", "", {}, "sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA=="],
+
"p-queue": ["p-queue@9.3.1", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-POWdiIPmsUPGwb4FeQ4OBg46aqmcInSWe45CKDsGHiOBiVQM9chqfQTuqhuTzcg2Vz9faTI65at0KkVyVEiCHw=="],
"p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
@@ -1466,7 +1473,7 @@
"readable-stream": ["readable-stream@1.0.34", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg=="],
- "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
@@ -1800,6 +1807,8 @@
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
+ "vite-plugin-static-copy": ["vite-plugin-static-copy@4.1.1", "", { "dependencies": { "chokidar": "^3.6.0", "p-map": "^7.0.4", "picocolors": "^1.1.1", "tinyglobby": "^0.2.17" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-GrlA8YklrAfSyxJ4M3fdQLOo9oNkp56IM9FYgX/WtEgeIFkPwhu4wzpufBCIuNKCa6Fn77FkRdYxkHqV0FwjAw=="],
+
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
"volar-service-css": ["volar-service-css@0.0.71", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw=="],
@@ -1946,6 +1955,8 @@
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
"cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
@@ -2096,6 +2107,8 @@
"read-pkg-up/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="],
+ "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
"remark-parse/mdast-util-from-markdown": ["mdast-util-from-markdown@1.3.1", "", { "dependencies": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.0", "decode-named-character-reference": "^1.0.0", "mdast-util-to-string": "^3.1.0", "micromark": "^3.0.0", "micromark-util-decode-numeric-character-reference": "^1.0.0", "micromark-util-decode-string": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.0", "unist-util-stringify-position": "^3.0.0", "uvu": "^0.5.0" } }, "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww=="],
"retext-smartypants/unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
diff --git a/docs/sitemap.xml b/docs/sitemap.xml
deleted file mode 100644
index f749f15d30..0000000000
--- a/docs/sitemap.xml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-layout: null
-title: Docs Sitemap
-permalink: /docs/sitemap.xml
----
-
-
-{% assign latest_docs = site.docs | where_exp: "doc", "doc.url contains '/docs/latest/'" | sort: "url" %}
-{% for doc in latest_docs %}
- {% assign canonical = doc.canonical | default: "" %}
- {% unless canonical contains "http" or doc.url contains ".json" %}
- {% assign loc = doc.url | replace: "/index", "/" | replace: "/.html", "/" | replace: ".html", "" %}
-
- https://www.metabase.com{{ loc | xml_escape }}
-
- {% endunless %}
-{% endfor %}
-
diff --git a/package.json b/package.json
index cf2019e27f..1774a62efc 100644
--- a/package.json
+++ b/package.json
@@ -88,6 +88,7 @@
"stylelint": "^14.9.0",
"stylelint-config-standard-scss": "^4.0.0",
"stylelint-order": "^5.0.0",
+ "vite-plugin-static-copy": "^4.1.1",
"wait-on": "^9.0.10"
},
"resolutions": {
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000000..44aa8e6d69
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1 @@
+export const DOCS_SRC_ROOT = "_docs";
diff --git a/src/content.config.ts b/src/content.config.ts
index 3a204e761c..6087964841 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -1,10 +1,12 @@
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
+import { DOCS_SRC_ROOT } from "./constants";
-const examples = defineCollection({
+const docs = defineCollection({
loader: glob({
- pattern: ["src/example-collection/**/*.md"],
+ pattern: ["**/*.md", "!**/embedding/sdk/api/snippets/**"],
+ base: DOCS_SRC_ROOT,
}),
});
-export const collections = { examples };
+export const collections = { docs };
diff --git a/src/lib/liquid/liquidRenderer.ts b/src/lib/liquid/liquidRenderer.ts
index deed1d438b..da601e3677 100644
--- a/src/lib/liquid/liquidRenderer.ts
+++ b/src/lib/liquid/liquidRenderer.ts
@@ -1,9 +1,44 @@
import fs from "node:fs";
import path from "node:path";
-import { Liquid } from "liquidjs";
+import { Liquid, ParseError, TokenizationError } from "liquidjs";
import YAML from "yamljs";
import { registerIncludeFileTag } from "./tags/includeFileTag";
+const MAX_LIQUID_SYNTAX_ERRORS = 25;
+
+// Old markdown docs sometimes contain text that merely looks like Liquid
+// (e.g. "{{#...}}" used to describe template tag syntax) but isn't valid
+// Liquid. Jekyll/Ruby rendered these as blank rather than failing the build,
+// so on a syntax error we cut out just the offending "{{ }}"/"{% %}" span and
+// retry, instead of taking down the whole page.
+const stripInvalidLiquidSpan = (
+ source: string,
+ err: unknown,
+): string | null => {
+ if (!(err instanceof TokenizationError) && !(err instanceof ParseError)) {
+ return null;
+ }
+
+ let { begin, end } = err.token;
+ if (!(source.startsWith("{{", begin) || source.startsWith("{%", begin))) {
+ const outputOpen = source.lastIndexOf("{{", begin);
+ const tagOpen = source.lastIndexOf("{%", begin);
+ let openStart = outputOpen;
+ let closer = "}}";
+ if (tagOpen > outputOpen) {
+ openStart = tagOpen;
+ closer = "%}";
+ }
+ if (openStart === -1) return null;
+ const closeIdx = source.indexOf(closer, end);
+ if (closeIdx === -1) return null;
+ begin = openStart;
+ end = closeIdx + closer.length;
+ }
+
+ return source.slice(0, begin) + source.slice(end);
+};
+
const ROOT = process.cwd();
const INCLUDES_ROOT = path.join(ROOT, "_includes");
@@ -63,14 +98,31 @@ export const getLiquidRenderer = ({
};
return {
- render: (
+ render: async (
html: string,
{ include }: { include?: Record } = {},
) => {
- return liquidEngine.parseAndRender(html, {
- ...ctx,
- include,
- });
+ let source = html;
+ for (let attempt = 0; attempt < MAX_LIQUID_SYNTAX_ERRORS; attempt++) {
+ try {
+ return await liquidEngine.parseAndRender(source, {
+ ...ctx,
+ include,
+ });
+ } catch (err) {
+ const stripped = stripInvalidLiquidSpan(source, err);
+ if (stripped === null) throw err;
+ console.warn(
+ `[liquid] Ignoring invalid Liquid syntax in ${dirname}: ${
+ (err as Error).message
+ }`,
+ );
+ source = stripped;
+ }
+ }
+ throw new Error(
+ `[liquid] Too many invalid Liquid syntax errors in ${dirname}`,
+ );
},
};
};
diff --git a/src/lib/markdown/markdownRenderer.ts b/src/lib/markdown/markdownRenderer.ts
index a6f0c275cb..6dd923817e 100644
--- a/src/lib/markdown/markdownRenderer.ts
+++ b/src/lib/markdown/markdownRenderer.ts
@@ -1,6 +1,7 @@
import { satteri } from "@astrojs/markdown-satteri";
import { ialHastPlugin } from "./plugins/ialHastPlugin";
import { inlineCodeHastPlugin } from "./plugins/inlineCodeHastPlugin";
+import { relativeImagePlugin } from "./plugins/relativeImagePlugin";
import { responsiveTableLabelsHastPlugin } from "./plugins/responsiveTableLabelsHastPlugin";
const docsMarkdownProcessor = satteri({
@@ -8,6 +9,7 @@ const docsMarkdownProcessor = satteri({
ialHastPlugin,
inlineCodeHastPlugin,
responsiveTableLabelsHastPlugin,
+ relativeImagePlugin,
],
});
diff --git a/src/lib/markdown/noopMarkdownProcessor.ts b/src/lib/markdown/noopMarkdownProcessor.ts
new file mode 100644
index 0000000000..b604bb598d
--- /dev/null
+++ b/src/lib/markdown/noopMarkdownProcessor.ts
@@ -0,0 +1,17 @@
+import type { MarkdownProcessor } from "astro/markdown";
+
+export const noopMarkdownProcessor: MarkdownProcessor = {
+ name: "no-op",
+ options: {},
+ createRenderer: async () => ({
+ render: async () => ({
+ code: "",
+ metadata: {
+ headings: [],
+ localImagePaths: [],
+ remoteImagePaths: [],
+ frontmatter: {},
+ },
+ }),
+ }),
+};
diff --git a/src/lib/markdown/plugins/relativeImagePlugin.ts b/src/lib/markdown/plugins/relativeImagePlugin.ts
new file mode 100644
index 0000000000..3a42f3ba94
--- /dev/null
+++ b/src/lib/markdown/plugins/relativeImagePlugin.ts
@@ -0,0 +1,28 @@
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { DOCS_SRC_ROOT } from "@/constants";
+import { defineHastPlugin } from "satteri";
+
+// Resolves relative images from docs md files.
+// The images themselves are copied via viteStaticCopy in astro.config.mjs.
+
+export const relativeImagePlugin = defineHastPlugin({
+ name: "relative-image-resolver",
+ element: {
+ filter: ["img"],
+ visit(node, ctx) {
+ const rawSrc = node.properties?.src;
+ if (typeof rawSrc !== "string" || rawSrc === "") return;
+ // Absolute site paths, absolute URLs, and data: URIs need no rewriting.
+ if (rawSrc.startsWith("/") || URL.canParse(rawSrc)) return;
+ if (!ctx.fileURL) return;
+
+ const absPath = fileURLToPath(new URL(decodeURI(rawSrc), ctx.fileURL));
+ const relPath = path.relative(DOCS_SRC_ROOT, absPath);
+ if (relPath.startsWith("..") || path.isAbsolute(relPath)) return;
+
+ const newSrc = `/docs/${relPath.split(path.sep).map(encodeURIComponent).join("/")}`;
+ ctx.setProperty(node, "src", newSrc);
+ },
+ },
+});
diff --git a/src/pages/docs/[version]/[...slug].astro b/src/pages/docs/[version]/[...slug].astro
new file mode 100644
index 0000000000..28b53088b0
--- /dev/null
+++ b/src/pages/docs/[version]/[...slug].astro
@@ -0,0 +1,53 @@
+---
+import path from "node:path";
+import { pathToFileURL } from "node:url";
+import NewDocsLayout from "@/layouts/NewDocsLayout.astro";
+import { getLiquidRenderer } from "@/lib/liquid/liquidRenderer";
+import { getMarkdownRenderer } from "@/lib/markdown/markdownRenderer";
+import { getCollection, type DataEntryMap } from "astro:content";
+
+type Props = {
+ doc: DataEntryMap["docs"][number];
+};
+
+export const getStaticPaths = async () => {
+ const splitAtFirst = (str: string, delimiter: string) => {
+ const index = str.indexOf(delimiter);
+ return index !== -1 ? [str.slice(0, index), str.slice(index + 1)] : [str];
+ };
+ // TODO: Handle README -> index
+ // TODO: Handle permalink in frontmatter
+ // TODO: Handle redirect_to/redirect_from
+ const docs = await getCollection("docs");
+ return [
+ ...docs.map((doc) => {
+ const [version, slug] = splitAtFirst(doc.id, "/");
+ return {
+ props: { doc },
+ params: { version, slug },
+ };
+ }),
+ ];
+};
+
+const { doc } = Astro.props;
+const dirname = path.dirname(doc.filePath!);
+
+// Process liquid first (e.g. control flow, includes, variables, etc)
+const lq = getLiquidRenderer({ page: doc.data, dirname });
+const lqOutput = await lq.render(doc.body!);
+
+// Remove whitespace between elements so satteri doesn't turn them into code snippets.
+// Mimics previous behavior (which used jekyll + kramdown).
+const lqProcessed = lqOutput.replace(/>\s+<");
+
+// Convert processed markdown to html
+const md = await getMarkdownRenderer();
+const renderResult = await md.render(lqProcessed, {
+ fileURL: pathToFileURL(path.resolve(doc.filePath!)),
+});
+---
+
+
+
+
diff --git a/src/pages/docs/astro-test/[...slug].astro b/src/pages/docs/astro-test/[...slug].astro
deleted file mode 100644
index 88eb3d98eb..0000000000
--- a/src/pages/docs/astro-test/[...slug].astro
+++ /dev/null
@@ -1,41 +0,0 @@
----
-import NewDocsLayout from "@/layouts/NewDocsLayout.astro";
-import { getLiquidRenderer } from "@/lib/liquid/liquidRenderer";
-import { getMarkdownRenderer } from "@/lib/markdown/markdownRenderer";
-import { getCollection, type DataEntryMap } from "astro:content";
-
-type Props = {
- example: DataEntryMap["examples"][number];
-};
-
-export const getStaticPaths = async () => {
- const examples = await getCollection("examples");
- return [
- ...examples.map((example) => ({
- props: { example },
- params: { slug: example.id.replace(/^src\//, "") },
- })),
- ];
-};
-
-const { example } = Astro.props;
-
-// This is for debug purposes only. When rendering _docs, the dirname will be the source md file's path.
-const dirname = "_docs/latest/embedding";
-
-// Process liquid first (e.g. control flow, includes, variables, etc)
-const lq = getLiquidRenderer({ page: example.data, dirname });
-const lqOutput = await lq.render(example.body!);
-
-// Remove whitespace between elements so satteri doesn't turn them into code snippets.
-// Mimics previous behavior (which used jekyll + kramdown).
-const lqProcessed = lqOutput.replace(/>\s+<");
-
-// Convert processed markdown to html
-const md = await getMarkdownRenderer();
-const renderResult = await md.render(lqProcessed);
----
-
-
-
-
diff --git a/src/pages/docs/sitemap.xml.ts b/src/pages/docs/sitemap.xml.ts
new file mode 100644
index 0000000000..7b17ea87b3
--- /dev/null
+++ b/src/pages/docs/sitemap.xml.ts
@@ -0,0 +1,31 @@
+import type { APIRoute } from "astro";
+import { getCollection } from "astro:content";
+
+const SITE_URL = "https://www.metabase.com";
+
+const escapeXml = (value: string) =>
+ value.replace(/&/g, "&").replace(//g, ">");
+
+export const GET: APIRoute = async () => {
+ const docs = await getCollection(
+ "docs",
+ (doc) => doc.id === "latest" || doc.id.startsWith("latest/"),
+ );
+
+ const paths = docs
+ .map((doc) => {
+ const slug = doc.id.slice("latest".length).replace(/^\//, "");
+ return slug ? `/docs/latest/${slug}` : "/docs/latest";
+ })
+ .sort();
+
+ const body = `
+
+${paths.map((path) => ` \n ${escapeXml(`${SITE_URL}${path}`)}\n `).join("\n")}
+
+`;
+
+ return new Response(body, {
+ headers: { "Content-Type": "application/xml" },
+ });
+};