From 5675aae14ff934aa791f8d4621542329b79d6335 Mon Sep 17 00:00:00 2001 From: Maciej Ptak <0ptaq0@gmail.com> Date: Sun, 25 Oct 2026 13:38:19 +0100 Subject: [PATCH 1/3] docs: serve page markdown on Accept header --- middleware.js | 51 +++++++++++++++++++ package-lock.json | 122 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 174 insertions(+) create mode 100644 middleware.js diff --git a/middleware.js b/middleware.js new file mode 100644 index 0000000..753efb8 --- /dev/null +++ b/middleware.js @@ -0,0 +1,51 @@ +import { next, rewrite } from '@vercel/functions'; + +// Vercel Routing Middleware: serves the markdown twin of a docs page (built next to it as /page.md) +// to AI agents that send `Accept: text/markdown`. The home page has no twin, so it answers with llms.txt. +export const config = { + // Pages (with or without .html) and .md twins only; skips assets and any other file + matcher: '/((?!assets/)(?!.*\\.(?!(?:md|html)$)[^/.]+$).*)' +}; + +// Suffix of page URLs on this site: '' with cleanUrls, '.html' without +const PAGE_SUFFIX = ''; + +const quality = (accept, type) => { + for (const part of accept.split(',')) { + const [media, ...params] = part.split(';').map(value => value.trim().toLowerCase()); + if (media !== type) continue; + const q = params.find(param => param.startsWith('q=')); + return q ? Number(q.slice(2)) || 0 : 1; + } + return 0; +}; + +const prefersMarkdown = accept => { + const markdown = quality(accept, 'text/markdown'); + return markdown > 0 && markdown >= quality(accept, 'text/html'); +}; + +export default async function middleware(request) { + const url = new URL(request.url); + + if (url.pathname.endsWith('.md')) { + return next({ headers: { Link: `<${url.origin}${url.pathname.slice(0, -3)}${PAGE_SUFFIX}>; rel="canonical"`, Vary: 'Accept' } }); + } + if (!prefersMarkdown(request.headers.get('accept') || '')) { + return next({ headers: { Vary: 'Accept' } }); + } + + const page = url.pathname.replace(/(\/index)?(\.html)?\/?$/, ''); + const markdown = new URL(page ? `${page}.md` : '/llms.txt', url.origin); + // Not every page has a twin (archived versions, for one). Credentials are forwarded and redirects + // not followed, so a protected preview's login redirect does not pass for an existing file. + const headers = {}; + for (const name of ['cookie', 'x-vercel-protection-bypass']) { + if (request.headers.has(name)) headers[name] = request.headers.get(name); + } + const twin = await fetch(markdown, { method: 'HEAD', headers, redirect: 'manual' }).catch(() => null); + if (!twin?.ok) { + return next({ headers: { Vary: 'Accept' } }); + } + return rewrite(markdown, { headers: { Vary: 'Accept' } }); +} diff --git a/package-lock.json b/package-lock.json index 8d67862..ccaf1f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@salesforce/eslint-plugin-aura": "^3.0.0", "@salesforce/eslint-plugin-lightning": "^2.0.0", "@salesforce/sfdx-lwc-jest": "^7.1.2", + "@vercel/functions": "^3.9.11", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jest": "^29.5.0", @@ -3904,6 +3905,70 @@ "dev": true, "license": "ISC" }, + "node_modules/@vercel/cli-config": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@vercel/cli-config/-/cli-config-0.3.3.tgz", + "integrity": "sha512-5T0ZtIFgcFHuPFuuoJKno5ZeKvLQGiS/JAFMI1zG1HBw2dklScoHdA0LDb/SoeRYD53N33iXFAqyRA0tHLhdnQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xdg-app-paths": "5", + "zod": "4.1.11" + } + }, + "node_modules/@vercel/cli-exec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vercel/cli-exec/-/cli-exec-1.0.1.tgz", + "integrity": "sha512-g9XerViJ/paZujufXYcu5XYI2vU2rtB4sgdpjUHde5RnOkdmpu0ngH46LCFGHoPXO/C+qDPSczIHIRN+8Q2YKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "execa": "5.1.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@vercel/functions": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@vercel/functions/-/functions-3.10.3.tgz", + "integrity": "sha512-EjMUf+myWckV9vj5FbzRG1wQP1IFRgmlOu2ho1FiFTjLs1mFnY4dxeI8Swk9pszj4jv/jGdvlmqRKHipuEt5bw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@vercel/oidc": "4.0.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-web-identity": "*", + "ws": ">=8" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-web-identity": { + "optional": true + }, + "ws": { + "optional": true + } + } + }, + "node_modules/@vercel/oidc": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-4.0.3.tgz", + "integrity": "sha512-0sljpA/leE1vLkY7DVOs/z/OH6x1hgKhrF+IY2YdbRYm/6UnQjeu0SMnzOKLtkJWp13srUFkWdir4iU3feFYfg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@vercel/cli-config": "0.3.3", + "@vercel/cli-exec": "1.0.1", + "jose": "^5.9.6" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", @@ -8552,6 +8617,16 @@ "node": ">= 20" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10057,6 +10132,16 @@ "node": ">= 0.8.0" } }, + "node_modules/os-paths": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/os-paths/-/os-paths-4.4.0.tgz", + "integrity": "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -12861,6 +12946,33 @@ } } }, + "node_modules/xdg-app-paths": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/xdg-app-paths/-/xdg-app-paths-5.5.1.tgz", + "integrity": "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-paths": "^4.0.1", + "xdg-portable": "^7.2.0" + }, + "engines": { + "node": ">= 6.0" + } + }, + "node_modules/xdg-portable": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/xdg-portable/-/xdg-portable-7.3.0.tgz", + "integrity": "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-paths": "^4.0.1" + }, + "engines": { + "node": ">= 6.0" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -12978,6 +13090,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 3b9995b..12bed36 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@salesforce/eslint-plugin-aura": "^3.0.0", "@salesforce/eslint-plugin-lightning": "^2.0.0", "@salesforce/sfdx-lwc-jest": "^7.1.2", + "@vercel/functions": "^3.9.11", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jest": "^29.5.0", From cae2eb6df97b4e31f4fb95fffe3c453674583020 Mon Sep 17 00:00:00 2001 From: Maciej Ptak <0ptaq0@gmail.com> Date: Sun, 25 Oct 2026 13:38:19 +0100 Subject: [PATCH 2/3] docs: link markdown twin from each page --- website/.vitepress/config.mts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index e889244..a1105b3 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -111,6 +111,20 @@ export default defineConfig({ ['link', { rel: 'canonical', href: canonicalUrl || siteUrl }], ['meta', { property: 'og:url', content: canonicalUrl || siteUrl }] ); + // Markdown twin built by vitepress-plugin-llms (llms.txt for the home page), + // served by middleware.js on Accept: text/markdown + const markdownPath = + pageData.relativePath === 'index.md' + ? 'llms.txt' + : pageData.relativePath.replace(/\/index\.md$/, '.md'); + pageData.frontmatter.head.push([ + 'link', + { + rel: 'alternate', + type: 'text/markdown', + href: `${siteUrl}/${markdownPath}` + } + ]); const pageTitle = pageData.frontmatter.title || pageData.title; pageData.frontmatter.head.push( [ From 40b4ddff2a34a197a257373fba188f2db1a3200b Mon Sep 17 00:00:00 2001 From: Maciej Ptak <0ptaq0@gmail.com> Date: Sun, 25 Oct 2026 13:38:19 +0100 Subject: [PATCH 3/3] docs: welcome AI agents in robots.txt --- website/public/robots.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/public/robots.txt b/website/public/robots.txt index 3f4fef1..b96b63a 100644 --- a/website/public/robots.txt +++ b/website/public/robots.txt @@ -1,4 +1,14 @@ User-agent: * +User-agent: GPTBot +User-agent: OAI-SearchBot +User-agent: ChatGPT-User +User-agent: ClaudeBot +User-agent: Claude-SearchBot +User-agent: Claude-User +User-agent: PerplexityBot +User-agent: Perplexity-User Allow: / +Content-Signal: search=yes, ai-input=yes, ai-train=yes +# Markdown for AI agents: /llms.txt, /llms-full.txt and .md for every page Sitemap: https://trigger.beyondthecloud.dev/sitemap.xml