Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ const config = defineConfig({
"Plane is open-source, modern project management software for planning, tracking, and shipping work.",
details:
"This documentation covers workspaces, projects, work items, cycles, modules, pages and wikis, integrations, importers, automations, and Plane AI.",
// Per-page .md versions are already emitted by buildEnd() for the
// `Accept: text/markdown` rewrite in vercel.json, so the plugin only
// owns llms.txt / llms-full.txt.
// Per-page .md versions are already emitted by buildEnd() and served on
// `Accept: text/markdown` by middleware.ts, so the plugin only owns
// llms.txt / llms-full.txt.
generateLLMFriendlyDocsForEachPage: false,
// Don't inject invisible LLM-hint markup into rendered pages.
injectLLMHint: false,
Expand All @@ -82,7 +82,8 @@ const config = defineConfig({
},

buildEnd(siteConfig) {
// Copy source .md files into dist/ for Accept: text/markdown negotiation.
// Copy source .md files into dist/ so middleware.ts can serve them on
// Accept: text/markdown negotiation.
const srcDir = siteConfig.srcDir;
const outDir = siteConfig.outDir;

Expand Down
69 changes: 69 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/** @format */

import { next, rewrite } from "@vercel/functions";

// Markdown content negotiation for agents.
//
// VitePress emits an HTML page for every doc, and buildEnd() in
// .vitepress/config.ts copies each source `.md` twin into dist/ alongside it
// (e.g. /core-concepts/issues/overview -> /core-concepts/issues/overview.md).
//
// A `vercel.json` rewrite can't serve those `.md` files on
// `Accept: text/markdown` because vercel.json rewrites run *after* the
// filesystem, and every clean URL already resolves to an existing `.html` file,
// so the rewrite is never reached. Routing Middleware runs *before* the
// filesystem, so it can transparently serve the `.md` twin at the same URL.
//
// HTML stays the default for browsers. Vercel's CDN already includes the
// `Accept` request header in its cache key, so the HTML and markdown variants
// of the same URL are cached as separate entries — no `Vary` header needed.
export const config = {
// Page routes only: skip built assets, fonts, and any path that already has a
// file extension (`.md`, `.txt`, `.xml`, `.js`, `.css`, images, …).
matcher: ["/", "/((?!assets/|fonts/|.*\\.).*)"],
};

// True only when the client *explicitly* accepts `text/markdown` with a
// non-zero quality value. Per RFC 7231 the type/subtype and the `q` parameter
// are case-insensitive, and `q=0` means "not acceptable". Wildcards (`*/*`,
// `text/*`) are intentionally ignored so browsers — which never list
// `text/markdown` — keep getting HTML.
function acceptsMarkdown(accept: string): boolean {
for (const range of accept.split(",")) {
const params = range.trim().split(";");
if (params[0].trim().toLowerCase() !== "text/markdown") continue;
let q = 1;
for (const param of params.slice(1)) {
const [key, value] = param.split("=");
if (key.trim().toLowerCase() === "q") {
const parsed = Number.parseFloat(value ?? "");
if (!Number.isNaN(parsed)) q = parsed;
}
}
if (q > 0) return true;
}
return false;
}

export default function middleware(request: Request): Response {
const accept = request.headers.get("accept") || "";
if (!acceptsMarkdown(accept)) {
return next();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const url = new URL(request.url);
let pathname = url.pathname;

// Map the clean URL to its emitted `.md` twin.
if (pathname === "/" || pathname === "") {
pathname = "/index";
} else if (pathname.endsWith("/")) {
pathname = pathname.slice(0, -1);
}
if (!pathname.endsWith(".md")) {
pathname = `${pathname}.md`;
}

url.pathname = pathname;
return rewrite(url);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.2.1",
"@vercel/functions": "^3.7.1",
"@voidzero-dev/vitepress-theme": "^4.8.4",
"lucide-vue-next": "^0.577.0",
"medium-zoom": "^1.1.0",
Expand Down
Loading
Loading