From 21723ab0d7d0e8ad104f0d3cc2fd3d26f68faadd Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 13 Jul 2026 12:29:26 +0200 Subject: [PATCH 1/7] feat(build): generate LLM-friendly markdown output and llms.txt Adds an experimental `llms` config option that emits, at the end of the build, a raw markdown version of each page (includes expanded, rewrites applied), an llms.txt index following the sidebar order, and an llms-full.txt bundle. Root locale only; dynamic routes are skipped. See https://llmstxt.org/ and vuejs/vitepress#4590 --- __tests__/e2e/.vitepress/config.ts | 1 + .../unit/node/build/generateLlmsTxt.test.ts | 241 ++++++++++++++ docs/config.ts | 3 +- docs/en/guide/llms.md | 47 +++ src/node/build/build.ts | 2 + src/node/build/generateLlmsTxt.ts | 294 ++++++++++++++++++ src/node/config.ts | 1 + src/node/siteConfig.ts | 13 + 8 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 __tests__/unit/node/build/generateLlmsTxt.test.ts create mode 100644 docs/en/guide/llms.md create mode 100644 src/node/build/generateLlmsTxt.ts diff --git a/__tests__/e2e/.vitepress/config.ts b/__tests__/e2e/.vitepress/config.ts index 441eda1cd6a3..f847ba12288f 100644 --- a/__tests__/e2e/.vitepress/config.ts +++ b/__tests__/e2e/.vitepress/config.ts @@ -154,6 +154,7 @@ const sidebar: DefaultTheme.Config['sidebar'] = { export default defineConfig({ title: 'Example', description: 'An example app using VitePress.', + llms: true, markdown: { image: { lazyLoading: true diff --git a/__tests__/unit/node/build/generateLlmsTxt.test.ts b/__tests__/unit/node/build/generateLlmsTxt.test.ts new file mode 100644 index 000000000000..441d351084e7 --- /dev/null +++ b/__tests__/unit/node/build/generateLlmsTxt.test.ts @@ -0,0 +1,241 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import type { Logger } from 'vite' +import { generateLlmsTxt } from 'node/build/generateLlmsTxt' +import type { SiteConfig } from 'node/siteConfig' + +const logger = { + info() {}, + warn() {}, + error() {} +} as unknown as Logger + +function writeFixture(dir: string, files: Record) { + for (const [file, content] of Object.entries(files)) { + const abs = path.join(dir, file) + fs.mkdirSync(path.dirname(abs), { recursive: true }) + fs.writeFileSync(abs, content) + } +} + +describe('node/build/generateLlmsTxt', () => { + let srcDir: string + let outDir: string + + beforeEach(() => { + srcDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-llms-src-')) + outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-llms-out-')) + + writeFixture(srcDir, { + 'index.md': [ + '---', + 'layout: home', + 'hero:', + ' name: Test Site', + ' text: A test site for LLMs', + '---' + ].join('\n'), + 'guide/index.md': '# Getting Started\n\nWelcome to the guide.', + 'guide/advanced.md': [ + '---', + 'title: Advanced Guide', + 'description: Advanced usage patterns', + '---', + '', + '# Advanced', + '', + 'Advanced content.' + ].join('\n'), + 'api/reference.md': '# API Reference\n\n\n', + 'api/shared.md': 'Shared API notes.', + 'fr/guide.md': '# Guide en français', + 'unlisted.md': '# Unlisted Page' + }) + }) + + afterEach(() => { + fs.rmSync(srcDir, { recursive: true, force: true }) + fs.rmSync(outDir, { recursive: true, force: true }) + }) + + function makeConfig(overrides: Partial = {}): SiteConfig { + return { + srcDir, + outDir, + pages: [ + 'api/reference.md', + 'data/1.md', + 'fr/guide.md', + 'guide/advanced.md', + 'guide/index.md', + 'index.md', + 'unlisted.md' + ], + dynamicRoutes: [{ path: 'data/1.md' }], + rewrites: { map: {}, inv: {} }, + cleanUrls: false, + markdown: {}, + logger, + site: { + title: 'Fallback Title', + description: 'Fallback description', + base: '/', + themeConfig: { + sidebar: [ + { + text: 'Guide', + items: [ + { text: 'Getting Started', link: '/guide/' }, + { text: 'Advanced', link: '/guide/advanced' } + ] + }, + { + text: 'API', + items: [{ text: 'Reference', link: '/api/reference' }] + } + ] + } + }, + userConfig: { + locales: { + root: { label: 'English', lang: 'en-US' }, + fr: { label: 'Français', lang: 'fr-FR' } + } + }, + llms: { hostname: 'https://example.com' }, + ...overrides + } as unknown as SiteConfig + } + + test('does nothing when llms is not enabled', async () => { + await generateLlmsTxt(makeConfig({ llms: undefined })) + expect(fs.existsSync(path.join(outDir, 'llms.txt'))).toBe(false) + }) + + test('generates llms.txt with hero metadata and sidebar-ordered TOC', async () => { + await generateLlmsTxt(makeConfig()) + + const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') + + expect(llmsTxt).toContain('# Test Site') + expect(llmsTxt).toContain('> A test site for LLMs') + + expect(llmsTxt).toContain('### Guide') + expect(llmsTxt).toContain( + '- [Getting Started](https://example.com/guide.md)' + ) + expect(llmsTxt).toContain( + '- [Advanced Guide](https://example.com/guide/advanced.md): Advanced usage patterns' + ) + expect(llmsTxt).toContain('### API') + expect(llmsTxt).toContain( + '- [API Reference](https://example.com/api/reference.md)' + ) + + // pages not in the sidebar are appended at the end + expect(llmsTxt).toContain( + '- [Unlisted Page](https://example.com/unlisted.md)' + ) + + // sidebar order is preserved + expect(llmsTxt.indexOf('Getting Started')).toBeLessThan( + llmsTxt.indexOf('Advanced Guide') + ) + expect(llmsTxt.indexOf('Advanced Guide')).toBeLessThan( + llmsTxt.indexOf('API Reference') + ) + }) + + test('emits per-page markdown files with url frontmatter', async () => { + await generateLlmsTxt(makeConfig()) + + // dir/index.md collapses to dir.md + const guide = fs.readFileSync(path.join(outDir, 'guide.md'), 'utf-8') + expect(guide).toContain('url: "https://example.com/guide.md"') + expect(guide).toContain('# Getting Started') + + const advanced = fs.readFileSync( + path.join(outDir, 'guide/advanced.md'), + 'utf-8' + ) + expect(advanced).toContain('url: "https://example.com/guide/advanced.md"') + expect(advanced).toContain('description: "Advanced usage patterns"') + // original frontmatter is replaced + expect(advanced).not.toContain('title: Advanced Guide') + + // includes are expanded + const reference = fs.readFileSync( + path.join(outDir, 'api/reference.md'), + 'utf-8' + ) + expect(reference).toContain('Shared API notes.') + expect(reference).not.toContain('@include') + }) + + test('generates llms-full.txt with all pages in TOC order', async () => { + await generateLlmsTxt(makeConfig()) + + const full = fs.readFileSync(path.join(outDir, 'llms-full.txt'), 'utf-8') + + expect(full).toContain('# Getting Started') + expect(full).toContain('Advanced content.') + expect(full).toContain('Shared API notes.') + expect(full).toContain('# Unlisted Page') + + expect(full.indexOf('# Getting Started')).toBeLessThan( + full.indexOf('Advanced content.') + ) + }) + + test('skips non-root locales and dynamic routes', async () => { + await generateLlmsTxt(makeConfig()) + + expect(fs.existsSync(path.join(outDir, 'fr/guide.md'))).toBe(false) + expect(fs.existsSync(path.join(outDir, 'data/1.md'))).toBe(false) + + const full = fs.readFileSync(path.join(outDir, 'llms-full.txt'), 'utf-8') + expect(full).not.toContain('français') + }) + + test('applies rewrites to output paths and links', async () => { + await generateLlmsTxt( + makeConfig({ + rewrites: { + map: { 'guide/advanced.md': 'advanced.md' }, + inv: { 'advanced.md': 'guide/advanced.md' } + } + }) + ) + + expect(fs.existsSync(path.join(outDir, 'advanced.md'))).toBe(true) + expect(fs.existsSync(path.join(outDir, 'guide/advanced.md'))).toBe(false) + + const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') + expect(llmsTxt).toContain('(https://example.com/advanced.md)') + }) + + test('falls back to site title/description and flat TOC without sidebar', async () => { + const config = makeConfig() + delete (config.site.themeConfig as any).sidebar + fs.rmSync(path.join(srcDir, 'index.md')) + config.pages = config.pages.filter((p) => p !== 'index.md') + + await generateLlmsTxt(config) + + const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') + expect(llmsTxt).toContain('# Fallback Title') + expect(llmsTxt).toContain('> Fallback description') + expect(llmsTxt).toContain('- [Advanced Guide](') + }) + + test('respects base in generated links', async () => { + const config = makeConfig() + config.site.base = '/docs/' + + await generateLlmsTxt(config) + + const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') + expect(llmsTxt).toContain('(https://example.com/docs/guide.md)') + }) +}) diff --git a/docs/config.ts b/docs/config.ts index 8f38b2b4cb8d..a981715ff7f6 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -101,7 +101,8 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { collapsed: false, items: [ { text: 'MPA Mode', link: 'mpa-mode' }, - { text: 'Sitemap Generation', link: 'sitemap-generation' } + { text: 'Sitemap Generation', link: 'sitemap-generation' }, + { text: 'LLM-Friendly Output', link: 'llms' } ] }, { text: 'Config & API Reference', base: '/reference/', link: 'site-config' } diff --git a/docs/en/guide/llms.md b/docs/en/guide/llms.md new file mode 100644 index 000000000000..ac3c9b67ba16 --- /dev/null +++ b/docs/en/guide/llms.md @@ -0,0 +1,47 @@ +--- +description: Generate LLM-friendly markdown output and llms.txt files for your VitePress site. +--- + +# LLM-Friendly Output + +VitePress can generate [LLM-friendly](https://llmstxt.org/) versions of your documentation at build time. To enable it, add the following to your `.vitepress/config.js`: + +```ts +export default { + llms: true +} +``` + +This emits the following files into the output directory alongside the regular HTML build: + +- `llms.txt` — an index of all pages with links and descriptions, for LLMs to discover your documentation. +- `llms-full.txt` — the entire documentation in a single markdown file. +- A raw markdown version of each page (e.g. `/guide/getting-started.md` next to `/guide/getting-started.html`), with `` directives expanded and route rewrites applied. + +The table of contents in `llms.txt` follows your sidebar structure and order when a sidebar is configured. + +## Options + +Pass an object to customize the output: + +```ts +export default { + llms: { + // used to build absolute links; falls back to sitemap.hostname. + // links are root-relative when absent + hostname: 'https://example.com', + + // defaults to the index page's hero name, then the site title + title: 'My Project', + + // defaults to the index page's hero text, then the site description + description: 'Documentation for My Project' + } +} +``` + +## Limitations + +- Only the root locale is emitted — translated locales are skipped. +- Pages generated from [dynamic routes](./routing#dynamic-routes) are skipped. +- `<<<` code snippet imports and image references are left as-is in the markdown output. diff --git a/src/node/build/build.ts b/src/node/build/build.ts index 3bce78bd9af4..d165102261d7 100644 --- a/src/node/build/build.ts +++ b/src/node/build/build.ts @@ -13,6 +13,7 @@ import { deserializeFunctions, serializeFunctions } from '../utils/fnSerialize' import { nativeImport } from '../utils/nativeImport' import { task } from '../utils/task' import { bundle } from './bundle' +import { generateLlmsTxt } from './generateLlmsTxt' import { generateSitemap } from './generateSitemap' import { renderPage } from './render' @@ -173,6 +174,7 @@ export async function build( } await generateSitemap(siteConfig) + await generateLlmsTxt(siteConfig) await siteConfig.buildEnd?.(siteConfig) clearCache() diff --git a/src/node/build/generateLlmsTxt.ts b/src/node/build/generateLlmsTxt.ts new file mode 100644 index 000000000000..2a8783db6898 --- /dev/null +++ b/src/node/build/generateLlmsTxt.ts @@ -0,0 +1,294 @@ +import matter from 'gray-matter' +import fs from 'node:fs/promises' +import path from 'node:path' +import pMap from 'p-map' +import type { SiteConfig } from '../config' +import type { DefaultTheme } from '../defaultTheme' +import type { MarkdownRenderer } from '../markdown/markdown' +import { createMarkdownRenderer } from '../markdown/markdown' +import { processIncludes } from '../utils/processIncludes' +import { task } from '../utils/task' + +export interface LlmsOptions { + /** + * Origin used to build absolute links (e.g. `https://example.com`). + * Falls back to `sitemap.hostname`. Links are root-relative when absent. + */ + hostname?: string + + /** + * Title used in `llms.txt`. + * Defaults to the index page's hero name, then the site title. + */ + title?: string + + /** + * Description used in `llms.txt`. + * Defaults to the index page's hero text, then the site description. + */ + description?: string +} + +interface LlmsPage { + /** output path of the emitted markdown file, relative to outDir */ + outPath: string + /** absolute or root-relative link to the emitted markdown file */ + link: string + title: string + description?: string + /** frontmatter-stripped, include-expanded markdown */ + content: string +} + +const includesRE = /` + let md: MarkdownRenderer | undefined + const getMd = async () => + (md ??= await createMarkdownRenderer( + siteConfig.srcDir, + siteConfig.markdown, + siteConfig.site.base, + siteConfig.logger + )) + + let indexFrontmatter: Record = {} + + const pages = ( + await pMap( + siteConfig.pages, + async (page): Promise => { + if (dynamicPaths.has(page)) return + if (skippedLocaleDirs.has(page.split('/')[0])) return + + const srcPath = path.join(siteConfig.srcDir, page) + const { data, content: rawContent } = matter( + await fs.readFile(srcPath, 'utf-8') + ) + + if (page === 'index.md') { + // the landing page provides llms.txt metadata but is not emitted + indexFrontmatter = data + return + } + + let content = rawContent + if (includesRE.test(content)) { + content = processIncludes( + await getMd(), + siteConfig.srcDir, + content, + srcPath, + [], + !!siteConfig.cleanUrls + ) + } + + const outPath = collapseIndexPath( + siteConfig.rewrites.map[page] || page + ) + + return { + outPath, + link: `${origin}${base}${outPath}`, + title: inferTitle(data, content, outPath.replace(/\.md$/, '')), + description: + typeof data.description === 'string' + ? data.description + : undefined, + content: content.trim() + } + }, + { concurrency: siteConfig.buildConcurrency } + ) + ).filter((page) => page !== undefined) + + // per-page markdown files + await pMap( + pages, + async (page) => { + const outFile = path.join(siteConfig.outDir, page.outPath) + await fs.mkdir(path.dirname(outFile), { recursive: true }) + await fs.writeFile( + outFile, + `${pageFrontmatter(page)}\n${page.content}\n` + ) + }, + { concurrency: siteConfig.buildConcurrency } + ) + + // llms.txt — TOC in sidebar order when a sidebar exists + const pagesByKey = new Map( + pages.map((page) => [normalizeLink(page.outPath), page]) + ) + const sidebar = flattenSidebar( + (siteConfig.site.themeConfig as DefaultTheme.Config | undefined)?.sidebar, + skippedLocaleDirs + ) + + const ordered: LlmsPage[] = [] + let toc = renderSidebarItems(sidebar, pagesByKey, ordered, '', 3).trim() + + const unlisted = pages.filter((page) => !ordered.includes(page)) + ordered.push(...unlisted) + if (unlisted.length) { + const entries = unlisted.map(tocEntry).join('') + toc += toc ? `\n\n### Other\n\n${entries}` : entries + } + + const title = + options.title ?? indexFrontmatter.hero?.name ?? siteConfig.site.title + const description = + options.description ?? + indexFrontmatter.hero?.text ?? + siteConfig.site.description + + const llmsTxt = `# ${title}\n\n${ + description ? `> ${description}\n\n` : '' + }## Table of Contents\n\n${toc.trim()}\n` + + await fs.writeFile(path.join(siteConfig.outDir, 'llms.txt'), llmsTxt) + + // llms-full.txt — every page in TOC order + const llmsFullTxt = ordered + .map((page) => `${pageFrontmatter(page)}\n${page.content}\n`) + .join('\n') + + await fs.writeFile( + path.join(siteConfig.outDir, 'llms-full.txt'), + llmsFullTxt + ) + }) +} diff --git a/src/node/config.ts b/src/node/config.ts index f2deaf32c165..3ff221b3ebd7 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -160,6 +160,7 @@ export async function resolveConfig( transformPageData: userConfig.transformPageData, userConfig, sitemap: userConfig.sitemap, + llms: userConfig.llms, buildConcurrency: userConfig.buildConcurrency ?? 64 } diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index e727087c7e3a..9da86e286aed 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -6,6 +6,7 @@ import type { AdditionalConfigDict, AdditionalConfigLoader } from '../../types/shared' +import type { LlmsOptions } from './build/generateLlmsTxt' import type { SitemapItem } from './build/generateSitemap' import type { MarkdownOptions } from './markdown/markdown' import type { ResolvedRouteConfig } from './plugins/dynamicRoutesPlugin' @@ -144,6 +145,17 @@ export interface UserConfig< transformItems?: (items: SitemapItem[]) => Awaitable } + /** + * Generate LLM-friendly output at build time: a markdown version of each + * page, plus `llms.txt` (index) and `llms-full.txt` (full content bundle). + * + * Only the root locale is emitted. Dynamic routes are skipped. + * + * @experimental + * @see https://llmstxt.org/ + */ + llms?: boolean | LlmsOptions + /** * Build end hook: called when SSG finish. * @param siteConfig The resolved configuration. @@ -212,6 +224,7 @@ export interface SiteConfig extends Pick< | 'transformHtml' | 'transformPageData' | 'sitemap' + | 'llms' > { root: string srcDir: string From 5b643daffdf1a9f42bd6702626b9bf9b88f4ea86 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 13 Jul 2026 12:41:20 +0200 Subject: [PATCH 2/7] feat(build): use native llms output for vitepress.dev docs Replaces vitepress-plugin-llms with the built-in `llms: true` option. The generator now detects the landing page through rewrites (en/index.md -> index.md), resolves the sidebar and site metadata through locale/additional config layers via resolveSiteDataByRoute, and renders leaf links before nested sections so trailing top-level links are not misattributed to the previous section heading. --- .../unit/node/build/generateLlmsTxt.test.ts | 65 ++ docs/.vitepress/config.ts | 5 +- docs/package.json | 3 +- pnpm-lock.yaml | 561 ------------------ src/node/build/generateLlmsTxt.ts | 36 +- 5 files changed, 88 insertions(+), 582 deletions(-) diff --git a/__tests__/unit/node/build/generateLlmsTxt.test.ts b/__tests__/unit/node/build/generateLlmsTxt.test.ts index 441d351084e7..8a4797ce3816 100644 --- a/__tests__/unit/node/build/generateLlmsTxt.test.ts +++ b/__tests__/unit/node/build/generateLlmsTxt.test.ts @@ -81,6 +81,10 @@ describe('node/build/generateLlmsTxt', () => { title: 'Fallback Title', description: 'Fallback description', base: '/', + locales: { + root: { label: 'English', lang: 'en-US' }, + fr: { label: 'Français', lang: 'fr-FR' } + }, themeConfig: { sidebar: [ { @@ -215,6 +219,67 @@ describe('node/build/generateLlmsTxt', () => { expect(llmsTxt).toContain('(https://example.com/advanced.md)') }) + test('detects the landing page through rewrites (en/index.md -> index.md)', async () => { + fs.rmSync(path.join(srcDir, 'index.md')) + writeFixture(srcDir, { + 'en/index.md': [ + '---', + 'layout: home', + 'hero:', + ' name: Rewritten Site', + ' text: Rewritten description', + '---' + ].join('\n'), + 'en/guide.md': '# Rewritten Guide' + }) + + const config = makeConfig({ + pages: ['en/guide.md', 'en/index.md', 'unlisted.md'], + rewrites: { + map: { 'en/index.md': 'index.md', 'en/guide.md': 'guide.md' }, + inv: { 'index.md': 'en/index.md', 'guide.md': 'en/guide.md' } + } + }) + + await generateLlmsTxt(config) + + const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') + expect(llmsTxt).toContain('# Rewritten Site') + expect(llmsTxt).toContain('> Rewritten description') + expect(llmsTxt).toContain( + '- [Rewritten Guide](https://example.com/guide.md)' + ) + + // the landing page is not emitted nor listed + expect(fs.existsSync(path.join(outDir, 'index.md'))).toBe(false) + expect(llmsTxt).not.toContain('](https://example.com/index.md)') + }) + + test('resolves the sidebar from additional config layers', async () => { + const config = makeConfig() + delete (config.site.themeConfig as any).sidebar + ;(config.site as any).additionalConfig = { + '/': { + themeConfig: { + sidebar: [ + { + text: 'Layered', + items: [{ text: 'Advanced', link: '/guide/advanced' }] + } + ] + } + } + } + + await generateLlmsTxt(config) + + const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') + expect(llmsTxt).toContain('### Layered') + expect(llmsTxt.indexOf('Advanced Guide')).toBeLessThan( + llmsTxt.indexOf('### Other') + ) + }) + test('falls back to site title/description and flat TOC without sidebar', async () => { const config = makeConfig() delete (config.site.themeConfig as any).sidebar diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index b809371393a5..50f2fe96045e 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -8,7 +8,6 @@ import { groupIconVitePlugin, localIconLoader } from 'vitepress-plugin-group-icons' -import llmstxt from 'vitepress-plugin-llms' const prod = !!process.env.NETLIFY const siteUrl = 'https://vitepress.dev' @@ -36,6 +35,7 @@ export default defineConfig({ lastUpdated: true, cleanUrls: true, metaChunk: true, + llms: true, markdown: { math: true, @@ -143,8 +143,7 @@ export default defineConfig({ ), firebase: 'logos:firebase' } - }), - prod && llmstxt({ workDir: 'en', ignoreFiles: ['index.md'] }) + }) ], experimental: { enableNativePlugin: true diff --git a/docs/package.json b/docs/package.json index c7540b089690..8ed9515d926d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,7 +15,6 @@ "open-cli": "^8.0.0", "postcss-rtlcss": "^6.0.0", "vitepress": "workspace:*", - "vitepress-plugin-group-icons": "^1.7.5", - "vitepress-plugin-llms": "^1.13.2" + "vitepress-plugin-group-icons": "^1.7.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6775cb743fa9..66efd2429c1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -338,9 +338,6 @@ importers: vitepress-plugin-group-icons: specifier: ^1.7.5 version: 1.7.5(vite@8.1.3(@types/node@25.9.4)(esbuild@0.27.7)(jiti@1.21.7)(yaml@2.9.0)) - vitepress-plugin-llms: - specifier: ^1.13.2 - version: 1.13.2 packages: @@ -1032,9 +1029,6 @@ packages: '@types/cross-spawn@6.0.6': resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} - '@types/debug@4.1.13': - resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1080,9 +1074,6 @@ packages: '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.13.2': resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} @@ -1280,18 +1271,10 @@ packages: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1318,13 +1301,6 @@ packages: axios@1.18.1: resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} - bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1334,10 +1310,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} - engines: {node: 18 || 20 || >=22} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1370,9 +1342,6 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - cheerio-select@1.6.0: resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==} @@ -1396,17 +1365,6 @@ packages: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1497,9 +1455,6 @@ packages: supports-color: optional: true - decode-named-character-reference@1.3.0: - resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -1562,9 +1517,6 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -1618,10 +1570,6 @@ packages: resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} engines: {node: '>=10'} - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - esm@3.2.25: resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} engines: {node: '>=6'} @@ -1656,9 +1604,6 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1675,9 +1620,6 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fault@2.0.1: - resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} - fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} @@ -1718,10 +1660,6 @@ packages: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} - format@0.2.2: - resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} - engines: {node: '>=0.4.x'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1730,10 +1668,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -1835,10 +1769,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-fullwidth-code-point@5.1.0: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} @@ -1867,10 +1797,6 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -2024,9 +1950,6 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2078,10 +2001,6 @@ packages: resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} hasBin: true - markdown-title@1.0.2: - resolution: {integrity: sha512-MqIQVVkz+uGEHi3TsHx/czcxxCbRIL7sv5K5DnYw/tI+apY54IbPefV/cmgxp6LoJSEx/TqcHdLs/298afG5QQ==} - engines: {node: '>=6'} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2090,24 +2009,9 @@ packages: resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} deprecated: Version 4 replaces this package with the scoped package @mathjax/src - mdast-util-from-markdown@2.0.3: - resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} - - mdast-util-frontmatter@2.0.1: - resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} - - mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - mdast-util-to-markdown@2.1.2: - resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - - mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -2129,80 +2033,25 @@ packages: mhchemparser@4.2.1: resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} - micromark-core-commonmark@2.0.3: - resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - - micromark-extension-frontmatter@2.0.0: - resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} - - micromark-factory-destination@2.0.1: - resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - - micromark-factory-label@2.0.1: - resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} - - micromark-factory-space@2.0.1: - resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - - micromark-factory-title@2.0.1: - resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - - micromark-factory-whitespace@2.0.1: - resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - micromark-util-chunked@2.0.1: - resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - - micromark-util-classify-character@2.0.1: - resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - - micromark-util-combine-extensions@2.0.1: - resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - - micromark-util-decode-numeric-character-reference@2.0.2: - resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - - micromark-util-decode-string@2.0.1: - resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - micromark-util-html-tag-name@2.0.1: - resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - - micromark-util-normalize-identifier@2.0.1: - resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - - micromark-util-resolve-all@2.0.1: - resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - micromark-util-sanitize-uri@2.0.1: resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - micromark-util-subtokenize@2.1.0: - resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - micromark@4.0.2: - resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - millify@6.1.0: - resolution: {integrity: sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==} - hasBin: true - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2220,10 +2069,6 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -2387,10 +2232,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-bytes@7.1.0: - resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} - engines: {node: '>=20'} - process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -2442,22 +2283,6 @@ packages: resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} engines: {node: '>=8'} - remark-frontmatter@5.0.0: - resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} - - remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - - remark-stringify@11.0.0: - resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - - remark@15.0.1: - resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -2625,10 +2450,6 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} @@ -2647,10 +2468,6 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} @@ -2705,9 +2522,6 @@ packages: resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} engines: {node: '>=14.16'} - tokenx@1.3.0: - resolution: {integrity: sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -2718,9 +2532,6 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - trouter@4.0.0: resolution: {integrity: sha512-bwwr76BThfiVwAFZqks5cJ+VoKNM3/2Yg1ZwJslkdmAUQ6S0UNoCoGYFDxdw+u1skfexggdmD2p35kW5Td4Cug==} engines: {node: '>=6'} @@ -2758,9 +2569,6 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unique-string@3.0.0: resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} engines: {node: '>=12'} @@ -2771,9 +2579,6 @@ packages: unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - unist-util-remove@4.0.0: - resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} - unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -2854,10 +2659,6 @@ packages: vite: optional: true - vitepress-plugin-llms@1.13.2: - resolution: {integrity: sha512-2O4s0I5pjEZzgnoWgBPCZCyhah9FH5uQB6lGADazMoyF1URJshtG04ZnmX+cbmQmniN3T5JzdJO9B4q8JHDKOQ==} - engines: {node: '>=18'} - vitest@4.1.9: resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2951,10 +2752,6 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -2963,23 +2760,11 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.3: - resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} - engines: {node: '>=12'} - yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -3523,10 +3308,6 @@ snapshots: dependencies: '@types/node': 25.9.4 - '@types/debug@4.1.13': - dependencies: - '@types/ms': 2.1.0 - '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} @@ -3574,8 +3355,6 @@ snapshots: '@types/minimist@1.2.5': {} - '@types/ms@2.1.0': {} - '@types/node@24.13.2': dependencies: undici-types: 7.18.2 @@ -3785,14 +3564,8 @@ snapshots: dependencies: environment: 1.1.0 - ansi-regex@5.0.1: {} - ansi-regex@6.2.2: {} - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - ansi-styles@6.2.3: {} arg@5.0.2: {} @@ -3819,20 +3592,12 @@ snapshots: - debug - supports-color - bail@2.0.2: {} - - balanced-match@4.0.4: {} - base64-js@1.5.1: {} birpc@2.9.0: {} boolbase@1.0.0: {} - brace-expansion@5.0.6: - dependencies: - balanced-match: 4.0.4 - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3861,8 +3626,6 @@ snapshots: character-entities-legacy@3.0.0: {} - character-entities@2.0.2: {} - cheerio-select@1.6.0: dependencies: css-select: 4.3.0 @@ -3896,18 +3659,6 @@ snapshots: slice-ansi: 8.0.0 string-width: 8.2.1 - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - colorette@2.0.20: {} combined-stream@1.0.8: @@ -3994,10 +3745,6 @@ snapshots: dependencies: ms: 2.1.3 - decode-named-character-reference@1.3.0: - dependencies: - character-entities: 2.0.2 - deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -4055,8 +3802,6 @@ snapshots: emoji-regex@10.6.0: {} - emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} entities@2.2.0: {} @@ -4119,8 +3864,6 @@ snapshots: escape-goat@3.0.0: {} - escape-string-regexp@5.0.0: {} - esm@3.2.25: {} esprima@4.0.1: {} @@ -4143,8 +3886,6 @@ snapshots: dependencies: is-extendable: 0.1.1 - extend@3.0.2: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4167,10 +3908,6 @@ snapshots: dependencies: reusify: 1.1.0 - fault@2.0.1: - dependencies: - format: 0.2.2 - fd-package-json@2.0.0: dependencies: walk-up-path: 4.0.0 @@ -4205,15 +3942,11 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 - format@0.2.2: {} - fsevents@2.3.3: optional: true function-bind@1.1.2: {} - get-caller-file@2.0.5: {} - get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: @@ -4335,8 +4068,6 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-fullwidth-code-point@5.1.0: dependencies: get-east-asian-width: 1.6.0 @@ -4357,8 +4088,6 @@ snapshots: is-obj@2.0.0: {} - is-plain-obj@4.1.0: {} - is-reference@1.2.1: dependencies: '@types/estree': 1.0.9 @@ -4504,8 +4233,6 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 - longest-streak@3.1.0: {} - lru-cache@10.4.3: {} lru-cache@11.5.1: {} @@ -4557,8 +4284,6 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 - markdown-title@1.0.2: {} - math-intrinsics@1.1.0: {} mathjax-full@3.2.2: @@ -4568,39 +4293,6 @@ snapshots: mj-context-menu: 0.6.1 speech-rule-engine: 4.1.4 - mdast-util-from-markdown@2.0.3: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.2 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-decode-string: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-frontmatter@2.0.1: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - escape-string-regexp: 5.0.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - micromark-extension-frontmatter: 2.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-phrasing@4.1.0: - dependencies: - '@types/mdast': 4.0.4 - unist-util-is: 6.0.1 - mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -4613,22 +4305,6 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 - mdast-util-to-markdown@2.1.2: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - longest-streak: 3.1.0 - mdast-util-phrasing: 4.1.0 - mdast-util-to-string: 4.0.0 - micromark-util-classify-character: 2.0.1 - micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.1.0 - zwitch: 2.0.4 - - mdast-util-to-string@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdurl@2.0.0: {} mensch@0.3.4: {} @@ -4641,155 +4317,28 @@ snapshots: mhchemparser@4.2.1: {} - micromark-core-commonmark@2.0.3: - dependencies: - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-factory-destination: 2.0.1 - micromark-factory-label: 2.0.1 - micromark-factory-space: 2.0.1 - micromark-factory-title: 2.0.1 - micromark-factory-whitespace: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-html-tag-name: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-frontmatter@2.0.0: - dependencies: - fault: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-destination@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-label@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-space@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-types: 2.0.2 - - micromark-factory-title@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-whitespace@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-util-chunked@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-classify-character@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-combine-extensions@2.0.1: - dependencies: - micromark-util-chunked: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-decode-numeric-character-reference@2.0.2: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-decode-string@2.0.1: - dependencies: - decode-named-character-reference: 1.3.0 - micromark-util-character: 2.1.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-symbol: 2.0.1 - micromark-util-encode@2.0.1: {} - micromark-util-html-tag-name@2.0.1: {} - - micromark-util-normalize-identifier@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-resolve-all@2.0.1: - dependencies: - micromark-util-types: 2.0.2 - micromark-util-sanitize-uri@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-encode: 2.0.1 micromark-util-symbol: 2.0.1 - micromark-util-subtokenize@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - micromark-util-symbol@2.0.1: {} micromark-util-types@2.0.2: {} - micromark@4.0.2: - dependencies: - '@types/debug': 4.1.13 - debug: 4.4.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-combine-extensions: 2.0.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-encode: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - transitivePeerDependencies: - - supports-color - micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.2 - millify@6.1.0: - dependencies: - yargs: 17.7.3 - mime-db@1.52.0: {} mime-types@2.1.35: @@ -4800,10 +4349,6 @@ snapshots: mimic-function@5.0.1: {} - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.6 - minimist@1.2.8: {} minisearch@7.2.0: {} @@ -4945,8 +4490,6 @@ snapshots: prettier@3.9.0: {} - pretty-bytes@7.1.0: {} - process@0.11.10: {} prompts@2.4.2: @@ -4990,41 +4533,6 @@ snapshots: regexparam@3.0.0: {} - remark-frontmatter@5.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-frontmatter: 2.0.1 - micromark-extension-frontmatter: 2.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-parse@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - micromark-util-types: 2.0.2 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-stringify@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.2 - unified: 11.0.5 - - remark@15.0.1: - dependencies: - '@types/mdast': 4.0.4 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - require-directory@2.1.1: {} - resolve-pkg-maps@1.0.0: {} resolve@1.22.12: @@ -5238,12 +4746,6 @@ snapshots: string-argv@0.3.2: {} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - string-width@5.1.2: dependencies: eastasianwidth: 0.2.0 @@ -5270,10 +4772,6 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -5320,16 +4818,12 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - tokenx@1.3.0: {} - totalist@3.0.1: {} tr46@0.0.3: {} trim-lines@3.0.1: {} - trough@2.2.0: {} - trouter@4.0.0: dependencies: regexparam: 3.0.0 @@ -5353,16 +4847,6 @@ snapshots: undici-types@7.24.6: {} - unified@11.0.5: - dependencies: - '@types/unist': 3.0.3 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 6.0.3 - unique-string@3.0.0: dependencies: crypto-random-string: 4.0.0 @@ -5375,12 +4859,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-remove@4.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -5442,25 +4920,6 @@ snapshots: optionalDependencies: vite: 8.1.3(@types/node@25.9.4)(esbuild@0.27.7)(jiti@1.21.7)(yaml@2.9.0) - vitepress-plugin-llms@1.13.2: - dependencies: - gray-matter: 4.0.3 - markdown-it: 14.2.0 - markdown-title: 1.0.2 - mdast-util-from-markdown: 2.0.3 - millify: 6.1.0 - minimatch: 10.2.5 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - pretty-bytes: 7.1.0 - remark: 15.0.1 - remark-frontmatter: 5.0.0 - tokenx: 1.3.0 - unist-util-remove: 4.0.0 - unist-util-visit: 5.1.0 - transitivePeerDependencies: - - supports-color - vitest@4.1.9(@types/node@25.9.4)(vite@8.1.3(@types/node@25.9.4)(esbuild@0.27.7)(jiti@1.21.7)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.9 @@ -5550,12 +5009,6 @@ snapshots: wordwrap@1.0.0: {} - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -5566,22 +5019,8 @@ snapshots: dependencies: is-wsl: 3.1.1 - y18n@5.0.8: {} - yaml@2.9.0: {} - yargs-parser@21.1.1: {} - - yargs@17.7.3: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yoctocolors@2.1.2: {} zod@3.25.76: {} diff --git a/src/node/build/generateLlmsTxt.ts b/src/node/build/generateLlmsTxt.ts index 2a8783db6898..9eb08afcd3d4 100644 --- a/src/node/build/generateLlmsTxt.ts +++ b/src/node/build/generateLlmsTxt.ts @@ -6,6 +6,7 @@ import type { SiteConfig } from '../config' import type { DefaultTheme } from '../defaultTheme' import type { MarkdownRenderer } from '../markdown/markdown' import { createMarkdownRenderer } from '../markdown/markdown' +import { resolveSiteDataByRoute } from '../shared' import { processIncludes } from '../utils/processIncludes' import { task } from '../utils/task' @@ -118,7 +119,10 @@ function renderSidebarItems( linkBase: string, depth: number ): string { - let out = '' + // leaf links come before nested sections so they are not + // misattributed to the previous section's heading + let links = '' + let sections = '' for (const item of items) { const base = item.base ?? linkBase @@ -127,7 +131,7 @@ function renderSidebarItems( const page = pagesByKey.get(normalizeLink(base + item.link)) if (page && !ordered.includes(page)) { ordered.push(page) - out += tocEntry(page) + links += tocEntry(page) } } @@ -140,14 +144,14 @@ function renderSidebarItems( depth + 1 ) if (section) { - out += item.text + sections += item.text ? `\n${'#'.repeat(depth)} ${item.text}\n\n${section}` : section } } } - return out + return links + sections } function tocEntry(page: LlmsPage): string { @@ -198,7 +202,11 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { await fs.readFile(srcPath, 'utf-8') ) - if (page === 'index.md') { + const outPath = collapseIndexPath( + siteConfig.rewrites.map[page] || page + ) + + if (outPath === 'index.md') { // the landing page provides llms.txt metadata but is not emitted indexFrontmatter = data return @@ -216,10 +224,6 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { ) } - const outPath = collapseIndexPath( - siteConfig.rewrites.map[page] || page - ) - return { outPath, link: `${origin}${base}${outPath}`, @@ -249,12 +253,15 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { { concurrency: siteConfig.buildConcurrency } ) - // llms.txt — TOC in sidebar order when a sidebar exists + // llms.txt — TOC in sidebar order when a sidebar exists. + // resolve site data for the root index so locale and additional + // config layers (e.g. a root-level config.ts) are taken into account + const rootSite = resolveSiteDataByRoute(siteConfig.site, 'index.md') const pagesByKey = new Map( pages.map((page) => [normalizeLink(page.outPath), page]) ) const sidebar = flattenSidebar( - (siteConfig.site.themeConfig as DefaultTheme.Config | undefined)?.sidebar, + (rootSite.themeConfig as DefaultTheme.Config | undefined)?.sidebar, skippedLocaleDirs ) @@ -268,12 +275,9 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { toc += toc ? `\n\n### Other\n\n${entries}` : entries } - const title = - options.title ?? indexFrontmatter.hero?.name ?? siteConfig.site.title + const title = options.title ?? indexFrontmatter.hero?.name ?? rootSite.title const description = - options.description ?? - indexFrontmatter.hero?.text ?? - siteConfig.site.description + options.description ?? indexFrontmatter.hero?.text ?? rootSite.description const llmsTxt = `# ${title}\n\n${ description ? `> ${description}\n\n` : '' From 485bc0a2b644ac47093fdf3d776c56f99f330dc6 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 13 Jul 2026 13:43:44 +0200 Subject: [PATCH 3/7] feat(build): add ignoreFiles option and llm-only/llm-exclude tags - `llms.ignoreFiles` excludes pages from all LLM output via picomatch globs, matched against both source and rewritten output paths. - `` content appears only in the generated markdown; it is stripped from the rendered HTML. `` is the inverse. Both are processed only when the llms option is enabled. --- .../unit/node/build/generateLlmsTxt.test.ts | 67 +++++++++++++++++++ __tests__/unit/node/markdownToVue.test.ts | 49 ++++++++++++++ docs/en/guide/llms.md | 26 ++++++- src/node/build/generateLlmsTxt.ts | 15 +++++ src/node/markdownToVue.ts | 5 ++ src/node/utils/llmTags.ts | 18 +++++ 6 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/node/utils/llmTags.ts diff --git a/__tests__/unit/node/build/generateLlmsTxt.test.ts b/__tests__/unit/node/build/generateLlmsTxt.test.ts index 8a4797ce3816..a583a269e1c7 100644 --- a/__tests__/unit/node/build/generateLlmsTxt.test.ts +++ b/__tests__/unit/node/build/generateLlmsTxt.test.ts @@ -303,4 +303,71 @@ describe('node/build/generateLlmsTxt', () => { const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') expect(llmsTxt).toContain('(https://example.com/docs/guide.md)') }) + + test('skips pages matching ignoreFiles patterns', async () => { + await generateLlmsTxt( + makeConfig({ + llms: { hostname: 'https://example.com', ignoreFiles: ['api/**'] } + }) + ) + + expect(fs.existsSync(path.join(outDir, 'api/reference.md'))).toBe(false) + expect(fs.existsSync(path.join(outDir, 'guide/advanced.md'))).toBe(true) + + const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') + expect(llmsTxt).not.toContain('API Reference') + + const full = fs.readFileSync(path.join(outDir, 'llms-full.txt'), 'utf-8') + expect(full).not.toContain('Shared API notes.') + }) + + test('matches ignoreFiles against rewritten output paths too', async () => { + await generateLlmsTxt( + makeConfig({ + llms: { + hostname: 'https://example.com', + ignoreFiles: ['advanced.md'] + }, + rewrites: { + map: { 'guide/advanced.md': 'advanced.md' }, + inv: { 'advanced.md': 'guide/advanced.md' } + } + }) + ) + + expect(fs.existsSync(path.join(outDir, 'advanced.md'))).toBe(false) + + const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') + expect(llmsTxt).not.toContain('Advanced Guide') + }) + + test('unwraps llm-only and drops llm-exclude in LLM output', async () => { + writeFixture(srcDir, { + 'tags.md': [ + '# Tags', + '', + '', + '', + 'Secret for LLMs.', + '', + '', + '', + 'Humans only.', + '', + 'Shared content.' + ].join('\n') + }) + + const config = makeConfig() + config.pages = [...config.pages, 'tags.md'] + + await generateLlmsTxt(config) + + const tags = fs.readFileSync(path.join(outDir, 'tags.md'), 'utf-8') + expect(tags).toContain('Secret for LLMs.') + expect(tags).toContain('Shared content.') + expect(tags).not.toContain('Humans only.') + expect(tags).not.toContain('llm-only') + expect(tags).not.toContain('llm-exclude') + }) }) diff --git a/__tests__/unit/node/markdownToVue.test.ts b/__tests__/unit/node/markdownToVue.test.ts index 232496dfe05d..5d67685c3b24 100644 --- a/__tests__/unit/node/markdownToVue.test.ts +++ b/__tests__/unit/node/markdownToVue.test.ts @@ -156,4 +156,53 @@ describe('node/markdownToVue', () => { expect(result.pageData.relativePath).toBe('index.md') }) + + test('drops llm-only and unwraps llm-exclude in HTML when llms is enabled', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-llm-tags-')) + + const file = path.join(root, 'index.md') + const src = + '# Home\n\n\n\nSecret for LLMs.\n\n\n\n\n\nHumans only.\n\n\n\nShared content.\n' + await writeFile(file, src) + + const siteConfig = await resolveConfig(root, 'build', 'production') + siteConfig.llms = true + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + { cache: false }, + '/', + false, + false, + siteConfig + ) + + const result = await render(src, file, 'public') + + expect(result.vueSrc).not.toContain('Secret for LLMs.') + expect(result.vueSrc).toContain('Humans only.') + expect(result.vueSrc).toContain('Shared content.') + expect(result.vueSrc).not.toContain('llm-exclude') + }) + + test('leaves llm tags untouched when llms is not enabled', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-llm-tags-')) + + const file = path.join(root, 'index.md') + const src = '# Home\n\n\n\nSecret for LLMs.\n\n\n' + await writeFile(file, src) + + const siteConfig = await resolveConfig(root, 'build', 'production') + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + { cache: false }, + '/', + false, + false, + siteConfig + ) + + const result = await render(src, file, 'public') + + expect(result.vueSrc).toContain('Secret for LLMs.') + }) }) diff --git a/docs/en/guide/llms.md b/docs/en/guide/llms.md index ac3c9b67ba16..aeb075233f5b 100644 --- a/docs/en/guide/llms.md +++ b/docs/en/guide/llms.md @@ -35,11 +35,35 @@ export default { title: 'My Project', // defaults to the index page's hero text, then the site description - description: 'Documentation for My Project' + description: 'Documentation for My Project', + + // glob patterns for pages to exclude from LLM output, matched + // against source paths (relative to srcDir) and output paths + ignoreFiles: ['about/team.md', 'changelog/**'] } } ``` +## Targeting Humans or LLMs + +Wrap markdown in `` to make it visible only in the generated markdown output, or in `` to keep it out of it: + +```md + + +Extra context that only appears in the markdown served to LLMs. + + + + + +Interactive demo that only makes sense in the browser. + + +``` + +Content in `` is removed from the rendered HTML pages, and content in `` is removed from the generated markdown. The tags themselves never appear in either output. They are only processed when the `llms` option is enabled. + ## Limitations - Only the root locale is emitted — translated locales are skipped. diff --git a/src/node/build/generateLlmsTxt.ts b/src/node/build/generateLlmsTxt.ts index 9eb08afcd3d4..777e02391ff6 100644 --- a/src/node/build/generateLlmsTxt.ts +++ b/src/node/build/generateLlmsTxt.ts @@ -2,11 +2,13 @@ import matter from 'gray-matter' import fs from 'node:fs/promises' import path from 'node:path' import pMap from 'p-map' +import picomatch from 'picomatch' import type { SiteConfig } from '../config' import type { DefaultTheme } from '../defaultTheme' import type { MarkdownRenderer } from '../markdown/markdown' import { createMarkdownRenderer } from '../markdown/markdown' import { resolveSiteDataByRoute } from '../shared' +import { resolveLlmTags } from '../utils/llmTags' import { processIncludes } from '../utils/processIncludes' import { task } from '../utils/task' @@ -28,6 +30,13 @@ export interface LlmsOptions { * Defaults to the index page's hero text, then the site description. */ description?: string + + /** + * Glob patterns for pages to exclude from LLM output. Matched against + * both the source path relative to `srcDir` (e.g. `en/guide/foo.md`) and + * the rewritten output path (e.g. `guide/foo.md`). + */ + ignoreFiles?: string[] } interface LlmsPage { @@ -176,6 +185,9 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { ) ) const dynamicPaths = new Set(siteConfig.dynamicRoutes.map((r) => r.path)) + const isIgnored = options.ignoreFiles?.length + ? picomatch(options.ignoreFiles) + : undefined await task('generating llms.txt', async () => { // lazily created — only needed when a page uses `` @@ -212,6 +224,8 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { return } + if (isIgnored?.(page) || isIgnored?.(outPath)) return + let content = rawContent if (includesRE.test(content)) { content = processIncludes( @@ -223,6 +237,7 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { !!siteConfig.cleanUrls ) } + content = resolveLlmTags(content) return { outPath, diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index 6f744acc6a80..2b3e0b6e12b5 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -20,6 +20,7 @@ import { type PageData } from './shared' import { getGitTimestamp } from './utils/getGitTimestamp' +import { stripLlmTags } from './utils/llmTags' import { processIncludes } from './utils/processIncludes' const debug = createDebug('vitepress:md') @@ -142,6 +143,10 @@ export async function createMarkdownToVueRenderFn( let includes: string[] = [] src = processIncludes(md, srcDir, src, fileOrig, includes, cleanUrls) + // llm-only content is not rendered to HTML — it only appears in the + // markdown output generated when the llms option is enabled + if (siteConfig?.llms) src = stripLlmTags(src) + const localeIndex = getLocaleForPath(siteConfig?.site, relativePath) // reset env before render diff --git a/src/node/utils/llmTags.ts b/src/node/utils/llmTags.ts new file mode 100644 index 000000000000..5173aabf7630 --- /dev/null +++ b/src/node/utils/llmTags.ts @@ -0,0 +1,18 @@ +const llmOnlyRE = /([^]*?)<\/llm-only>/g +const llmExcludeRE = /([^]*?)<\/llm-exclude>/g + +/** + * Prepares markdown for the HTML build: `` blocks are dropped + * with their content, `` tags are removed keeping the content. + */ +export function stripLlmTags(src: string): string { + return src.replace(llmOnlyRE, '').replace(llmExcludeRE, '$1') +} + +/** + * Prepares markdown for LLM output: `` tags are removed keeping + * the content, `` blocks are dropped with their content. + */ +export function resolveLlmTags(src: string): string { + return src.replace(llmOnlyRE, '$1').replace(llmExcludeRE, '') +} From 651668a3571aff5175db3d9afbfeec14270b7f1a Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 13 Jul 2026 13:55:41 +0200 Subject: [PATCH 4/7] feat(build): add llms.enabled option Allows toggling LLM output generation while keeping the rest of the options, e.g. `llms: { enabled: !!process.env.CI, ... }`. Also gates the llm-only/llm-exclude tag processing in the markdown pipeline. --- .../unit/node/build/generateLlmsTxt.test.ts | 9 ++++++ __tests__/unit/node/markdownToVue.test.ts | 23 ++++++++++++++ docs/.vitepress/config.ts | 30 +++++++++---------- docs/en/guide/llms.md | 4 +++ src/node/build/generateLlmsTxt.ts | 12 ++++++-- src/node/markdownToVue.ts | 4 +-- src/node/utils/llmTags.ts | 9 ++++++ 7 files changed, 72 insertions(+), 19 deletions(-) diff --git a/__tests__/unit/node/build/generateLlmsTxt.test.ts b/__tests__/unit/node/build/generateLlmsTxt.test.ts index a583a269e1c7..1a9b99344889 100644 --- a/__tests__/unit/node/build/generateLlmsTxt.test.ts +++ b/__tests__/unit/node/build/generateLlmsTxt.test.ts @@ -117,6 +117,15 @@ describe('node/build/generateLlmsTxt', () => { expect(fs.existsSync(path.join(outDir, 'llms.txt'))).toBe(false) }) + test('does nothing when llms.enabled is false', async () => { + await generateLlmsTxt( + makeConfig({ + llms: { enabled: false, hostname: 'https://example.com' } + }) + ) + expect(fs.existsSync(path.join(outDir, 'llms.txt'))).toBe(false) + }) + test('generates llms.txt with hero metadata and sidebar-ordered TOC', async () => { await generateLlmsTxt(makeConfig()) diff --git a/__tests__/unit/node/markdownToVue.test.ts b/__tests__/unit/node/markdownToVue.test.ts index 5d67685c3b24..e4ac59d3f99e 100644 --- a/__tests__/unit/node/markdownToVue.test.ts +++ b/__tests__/unit/node/markdownToVue.test.ts @@ -205,4 +205,27 @@ describe('node/markdownToVue', () => { expect(result.vueSrc).toContain('Secret for LLMs.') }) + + test('leaves llm tags untouched when llms.enabled is false', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-llm-tags-')) + + const file = path.join(root, 'index.md') + const src = '# Home\n\n\n\nSecret for LLMs.\n\n\n' + await writeFile(file, src) + + const siteConfig = await resolveConfig(root, 'build', 'production') + siteConfig.llms = { enabled: false } + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + { cache: false }, + '/', + false, + false, + siteConfig + ) + + const result = await render(src, file, 'public') + + expect(result.vueSrc).toContain('Secret for LLMs.') + }) }) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 50f2fe96045e..da3e260c7798 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -158,20 +158,20 @@ export default defineConfig({ const description = pageData.description || site.description const locale = localeToOgLocaleMap[site.localeIndex || 'root'] - ;((pageData.frontmatter.head ??= []) as HeadConfig[]).push( - ['meta', { property: 'og:url', content: url }], - ['meta', { property: 'og:title', content: title }], - ['meta', { property: 'og:description', content: description }], - ['meta', { property: 'og:type', content: 'website' }], - ['meta', { property: 'og:locale', content: locale }], - ['meta', { property: 'og:site_name', content: 'VitePress' }], - ['meta', { property: 'og:image', content: ogImage }], - ['meta', { property: 'og:image:secure_url', content: ogImage }], - ['meta', { property: 'og:image:type', content: 'image/jpeg' }], - ['meta', { property: 'og:image:width', content: '1280' }], - ['meta', { property: 'og:image:height', content: '640' }], - ['meta', { property: 'og:image:alt', content: 'VitePress' }], - ['link', { rel: 'canonical', href: url }] - ) + ; ((pageData.frontmatter.head ??= []) as HeadConfig[]).push( + ['meta', { property: 'og:url', content: url }], + ['meta', { property: 'og:title', content: title }], + ['meta', { property: 'og:description', content: description }], + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:locale', content: locale }], + ['meta', { property: 'og:site_name', content: 'VitePress' }], + ['meta', { property: 'og:image', content: ogImage }], + ['meta', { property: 'og:image:secure_url', content: ogImage }], + ['meta', { property: 'og:image:type', content: 'image/jpeg' }], + ['meta', { property: 'og:image:width', content: '1280' }], + ['meta', { property: 'og:image:height', content: '640' }], + ['meta', { property: 'og:image:alt', content: 'VitePress' }], + ['link', { rel: 'canonical', href: url }] + ) } : undefined }) diff --git a/docs/en/guide/llms.md b/docs/en/guide/llms.md index aeb075233f5b..940b7456f371 100644 --- a/docs/en/guide/llms.md +++ b/docs/en/guide/llms.md @@ -27,6 +27,10 @@ Pass an object to customize the output: ```ts export default { llms: { + // toggle generation while keeping the rest of the options, + // e.g. only generate on CI (defaults to true) + enabled: !!process.env.CI, + // used to build absolute links; falls back to sitemap.hostname. // links are root-relative when absent hostname: 'https://example.com', diff --git a/src/node/build/generateLlmsTxt.ts b/src/node/build/generateLlmsTxt.ts index 777e02391ff6..2493b58230ab 100644 --- a/src/node/build/generateLlmsTxt.ts +++ b/src/node/build/generateLlmsTxt.ts @@ -8,11 +8,19 @@ import type { DefaultTheme } from '../defaultTheme' import type { MarkdownRenderer } from '../markdown/markdown' import { createMarkdownRenderer } from '../markdown/markdown' import { resolveSiteDataByRoute } from '../shared' -import { resolveLlmTags } from '../utils/llmTags' +import { isLlmsEnabled, resolveLlmTags } from '../utils/llmTags' import { processIncludes } from '../utils/processIncludes' import { task } from '../utils/task' export interface LlmsOptions { + /** + * Whether to generate LLM output. Useful to keep the rest of the options + * while toggling generation, e.g. only on CI. + * + * @default true + */ + enabled?: boolean + /** * Origin used to build absolute links (e.g. `https://example.com`). * Falls back to `sitemap.hostname`. Links are root-relative when absent. @@ -170,7 +178,7 @@ function tocEntry(page: LlmsPage): string { } export async function generateLlmsTxt(siteConfig: SiteConfig) { - if (!siteConfig.llms) return + if (!isLlmsEnabled(siteConfig.llms)) return const options: LlmsOptions = siteConfig.llms === true ? {} : siteConfig.llms diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index 2b3e0b6e12b5..d1654dce6599 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -20,7 +20,7 @@ import { type PageData } from './shared' import { getGitTimestamp } from './utils/getGitTimestamp' -import { stripLlmTags } from './utils/llmTags' +import { isLlmsEnabled, stripLlmTags } from './utils/llmTags' import { processIncludes } from './utils/processIncludes' const debug = createDebug('vitepress:md') @@ -145,7 +145,7 @@ export async function createMarkdownToVueRenderFn( // llm-only content is not rendered to HTML — it only appears in the // markdown output generated when the llms option is enabled - if (siteConfig?.llms) src = stripLlmTags(src) + if (isLlmsEnabled(siteConfig?.llms)) src = stripLlmTags(src) const localeIndex = getLocaleForPath(siteConfig?.site, relativePath) diff --git a/src/node/utils/llmTags.ts b/src/node/utils/llmTags.ts index 5173aabf7630..a2b78e12ff9a 100644 --- a/src/node/utils/llmTags.ts +++ b/src/node/utils/llmTags.ts @@ -1,6 +1,15 @@ +import type { LlmsOptions } from '../build/generateLlmsTxt' +import type { UserConfig } from '../siteConfig' + const llmOnlyRE = /([^]*?)<\/llm-only>/g const llmExcludeRE = /([^]*?)<\/llm-exclude>/g +export function isLlmsEnabled( + llms: UserConfig['llms'] +): llms is true | LlmsOptions { + return !!llms && (llms === true || llms.enabled !== false) +} + /** * Prepares markdown for the HTML build: `` blocks are dropped * with their content, `` tags are removed keeping the content. From e5239bce58f1333683e2f3cc500747c9916020f3 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 13 Jul 2026 14:21:56 +0200 Subject: [PATCH 5/7] refactor: fix generation of docs for llm-only --- .../unit/node/build/generateLlmsTxt.test.ts | 4 +- __tests__/unit/node/utils/llmTags.test.ts | 152 ++++++++++++++++++ docs/en/guide/llms.md | 2 + src/node/utils/llmTags.ts | 61 ++++++- 4 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 __tests__/unit/node/utils/llmTags.test.ts diff --git a/__tests__/unit/node/build/generateLlmsTxt.test.ts b/__tests__/unit/node/build/generateLlmsTxt.test.ts index 1a9b99344889..cd53a6ee63d2 100644 --- a/__tests__/unit/node/build/generateLlmsTxt.test.ts +++ b/__tests__/unit/node/build/generateLlmsTxt.test.ts @@ -361,7 +361,9 @@ describe('node/build/generateLlmsTxt', () => { '', '', '', - 'Humans only.', + '', + 'Humans only.', + '', '', 'Shared content.' ].join('\n') diff --git a/__tests__/unit/node/utils/llmTags.test.ts b/__tests__/unit/node/utils/llmTags.test.ts new file mode 100644 index 000000000000..06468c1f97ae --- /dev/null +++ b/__tests__/unit/node/utils/llmTags.test.ts @@ -0,0 +1,152 @@ +import { resolveLlmTags, stripLlmTags } from 'node/utils/llmTags' + +describe('node/utils/llmTags', () => { + const basic = [ + '# Title', + '', + '', + '', + 'For LLMs.', + '', + '', + '', + '', + '', + 'For humans.', + '', + '', + '', + 'Shared.', + '' + ].join('\n') + + test('stripLlmTags drops llm-only content and unwraps llm-exclude', () => { + const out = stripLlmTags(basic) + expect(out).not.toContain('For LLMs.') + expect(out).toContain('For humans.') + expect(out).toContain('Shared.') + expect(out).not.toContain('llm-only') + expect(out).not.toContain('llm-exclude') + }) + + test('resolveLlmTags unwraps llm-only and drops llm-exclude content', () => { + const out = resolveLlmTags(basic) + expect(out).toContain('For LLMs.') + expect(out).not.toContain('For humans.') + expect(out).toContain('Shared.') + expect(out).not.toContain('llm-only') + expect(out).not.toContain('llm-exclude') + }) + + test('ignores tags inside fenced code blocks', () => { + const src = [ + 'Before.', + '', + '```md', + '', + '', + 'Example.', + '', + '', + '```', + '', + 'After.', + '' + ].join('\n') + + expect(stripLlmTags(src)).toBe(src) + expect(resolveLlmTags(src)).toBe(src) + }) + + test('ignores tags that are not alone on their line', () => { + const src = + 'Wrap content in `` and close it with ``.\n' + expect(stripLlmTags(src)).toBe(src) + expect(resolveLlmTags(src)).toBe(src) + }) + + test('inline mention before a fenced example does not swallow the page', () => { + // regression: docs/en/guide/llms.md mentions the tags inline and shows + // them inside a ```md fence — the old regex matched from the inline + // mention to the closing tag inside the fence + const src = [ + 'Wrap markdown in `` or ``:', + '', + '```md', + '', + '', + 'LLM extra context.', + '', + '', + '', + '', + '', + 'Browser-only demo.', + '', + '', + '```', + '', + 'Content in `` is removed from HTML.', + '' + ].join('\n') + + expect(stripLlmTags(src)).toBe(src) + expect(resolveLlmTags(src)).toBe(src) + }) + + test('handles fenced code blocks inside llm blocks', () => { + const src = [ + '', + '', + '```js', + 'const a = 1', + '```', + '', + '', + '', + 'Shared.', + '' + ].join('\n') + + const stripped = stripLlmTags(src) + expect(stripped).not.toContain('const a = 1') + expect(stripped).toContain('Shared.') + + const resolved = resolveLlmTags(src) + expect(resolved).toContain('const a = 1') + expect(resolved).not.toContain('llm-only') + }) + + test('supports ~~~ fences and longer fences', () => { + const src = [ + '~~~md', + '', + 'in tilde fence', + '', + '~~~', + '', + '````md', + '', + 'in long fence', + '', + '````', + '' + ].join('\n') + + expect(stripLlmTags(src)).toBe(src) + expect(resolveLlmTags(src)).toBe(src) + }) + + test('leaves unclosed tags untouched', () => { + const src = '# Title\n\n\n\nDangling.\n' + expect(stripLlmTags(src)).toBe(src) + expect(resolveLlmTags(src)).toBe(src) + }) + + test('allows indented tags up to 3 spaces', () => { + const src = ' \ncontent\n \nShared.\n' + expect(stripLlmTags(src)).not.toContain('content') + expect(resolveLlmTags(src)).toContain('content') + expect(resolveLlmTags(src)).not.toContain('llm-only') + }) +}) diff --git a/docs/en/guide/llms.md b/docs/en/guide/llms.md index 940b7456f371..e69f9c9a90e8 100644 --- a/docs/en/guide/llms.md +++ b/docs/en/guide/llms.md @@ -68,6 +68,8 @@ Interactive demo that only makes sense in the browser. Content in `` is removed from the rendered HTML pages, and content in `` is removed from the generated markdown. The tags themselves never appear in either output. They are only processed when the `llms` option is enabled. +Each tag must be alone on its own line, like in the example above. Tags inside fenced code blocks or anywhere else on a line (e.g. in inline code) are left untouched, so you can document them. + ## Limitations - Only the root locale is emitted — translated locales are skipped. diff --git a/src/node/utils/llmTags.ts b/src/node/utils/llmTags.ts index a2b78e12ff9a..abd80a3f6520 100644 --- a/src/node/utils/llmTags.ts +++ b/src/node/utils/llmTags.ts @@ -1,9 +1,6 @@ import type { LlmsOptions } from '../build/generateLlmsTxt' import type { UserConfig } from '../siteConfig' -const llmOnlyRE = /([^]*?)<\/llm-only>/g -const llmExcludeRE = /([^]*?)<\/llm-exclude>/g - export function isLlmsEnabled( llms: UserConfig['llms'] ): llms is true | LlmsOptions { @@ -15,7 +12,7 @@ export function isLlmsEnabled( * with their content, `` tags are removed keeping the content. */ export function stripLlmTags(src: string): string { - return src.replace(llmOnlyRE, '').replace(llmExcludeRE, '$1') + return processLlmTags(src, 'exclude') } /** @@ -23,5 +20,59 @@ export function stripLlmTags(src: string): string { * the content, `` blocks are dropped with their content. */ export function resolveLlmTags(src: string): string { - return src.replace(llmOnlyRE, '$1').replace(llmExcludeRE, '') + return processLlmTags(src, 'only') +} + +// up to 3 leading spaces like CommonMark fences; anything more is an +// indented code block +const tagRE = /^ {0,3}<(\/?)llm-(only|exclude)>\s*$/ +const fenceRE = /^ {0,3}(`{3,}|~{3,})/ + +/** + * Removes `` / `` blocks, keeping the content of + * `keep` blocks and dropping the other's. Tags must be alone on their line + * and outside fenced code blocks; anything else is left untouched so the + * tags themselves can be documented. + */ +function processLlmTags(src: string, keep: 'only' | 'exclude'): string { + const lines = src.split('\n') + const out: string[] = [] + // tag line + content buffered until the closing tag; flushed verbatim if + // the block is never closed + let block: { tag: string; lines: string[] } | undefined + let fence: string | undefined + + for (const line of lines) { + const fenceMatch = line.match(fenceRE) + if (fenceMatch) { + if (!fence) fence = fenceMatch[1] + else if ( + fenceMatch[1][0] === fence[0] && + fenceMatch[1].length >= fence.length + ) + fence = undefined + } + + const tagMatch = fenceMatch || fence ? null : line.match(tagRE) + if (tagMatch) { + const [, closing, tag] = tagMatch + if (!block && !closing) { + block = { tag, lines: [line] } + continue + } + if (block && closing && tag === block.tag) { + if (tag === keep) out.push(...block.lines.slice(1)) + block = undefined + continue + } + } + + if (block) block.lines.push(line) + else out.push(line) + } + + // unclosed block: restore it verbatim + if (block) out.push(...block.lines) + + return out.join('\n') } From 017aee500d2cbb94daf11282839593e095fe67f0 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 13 Jul 2026 16:44:43 +0200 Subject: [PATCH 6/7] perf(build): collect LLM sources during the markdown transform generateLlmsTxt no longer re-reads pages from disk, re-parses frontmatter, and re-runs include processing with a second markdown renderer: the markdown to vue transform now collects the include-expanded source (before llm tags are stripped for the HTML build) and generateLlmsTxt assembles the output from it. --- .../unit/node/build/generateLlmsTxt.test.ts | 193 ++++++++++-------- __tests__/unit/node/markdownToVue.test.ts | 31 +++ src/node/build/generateLlmsTxt.ts | 62 +++--- src/node/markdownToVue.ts | 16 +- 4 files changed, 187 insertions(+), 115 deletions(-) diff --git a/__tests__/unit/node/build/generateLlmsTxt.test.ts b/__tests__/unit/node/build/generateLlmsTxt.test.ts index cd53a6ee63d2..d067b56431de 100644 --- a/__tests__/unit/node/build/generateLlmsTxt.test.ts +++ b/__tests__/unit/node/build/generateLlmsTxt.test.ts @@ -2,7 +2,10 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import type { Logger } from 'vite' -import { generateLlmsTxt } from 'node/build/generateLlmsTxt' +import { + collectLlmsSource, + generateLlmsTxt +} from 'node/build/generateLlmsTxt' import type { SiteConfig } from 'node/siteConfig' const logger = { @@ -11,11 +14,36 @@ const logger = { error() {} } as unknown as Logger -function writeFixture(dir: string, files: Record) { +// sources as collected from the markdown → Vue pipeline: includes are +// already expanded there +const fixtures: Record = { + 'index.md': [ + '---', + 'layout: home', + 'hero:', + ' name: Test Site', + ' text: A test site for LLMs', + '---' + ].join('\n'), + 'guide/index.md': '# Getting Started\n\nWelcome to the guide.', + 'guide/advanced.md': [ + '---', + 'title: Advanced Guide', + 'description: Advanced usage patterns', + '---', + '', + '# Advanced', + '', + 'Advanced content.' + ].join('\n'), + 'api/reference.md': '# API Reference\n\nShared API notes.\n', + 'fr/guide.md': '# Guide en français', + 'unlisted.md': '# Unlisted Page' +} + +function seed(config: SiteConfig, files: Record = fixtures) { for (const [file, content] of Object.entries(files)) { - const abs = path.join(dir, file) - fs.mkdirSync(path.dirname(abs), { recursive: true }) - fs.writeFileSync(abs, content) + collectLlmsSource(config, file, content) } } @@ -26,32 +54,6 @@ describe('node/build/generateLlmsTxt', () => { beforeEach(() => { srcDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-llms-src-')) outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-llms-out-')) - - writeFixture(srcDir, { - 'index.md': [ - '---', - 'layout: home', - 'hero:', - ' name: Test Site', - ' text: A test site for LLMs', - '---' - ].join('\n'), - 'guide/index.md': '# Getting Started\n\nWelcome to the guide.', - 'guide/advanced.md': [ - '---', - 'title: Advanced Guide', - 'description: Advanced usage patterns', - '---', - '', - '# Advanced', - '', - 'Advanced content.' - ].join('\n'), - 'api/reference.md': '# API Reference\n\n\n', - 'api/shared.md': 'Shared API notes.', - 'fr/guide.md': '# Guide en français', - 'unlisted.md': '# Unlisted Page' - }) }) afterEach(() => { @@ -113,21 +115,25 @@ describe('node/build/generateLlmsTxt', () => { } test('does nothing when llms is not enabled', async () => { - await generateLlmsTxt(makeConfig({ llms: undefined })) + const config = makeConfig({ llms: undefined }) + seed(config) + await generateLlmsTxt(config) expect(fs.existsSync(path.join(outDir, 'llms.txt'))).toBe(false) }) test('does nothing when llms.enabled is false', async () => { - await generateLlmsTxt( - makeConfig({ - llms: { enabled: false, hostname: 'https://example.com' } - }) - ) + const config = makeConfig({ + llms: { enabled: false, hostname: 'https://example.com' } + }) + seed(config) + await generateLlmsTxt(config) expect(fs.existsSync(path.join(outDir, 'llms.txt'))).toBe(false) }) test('generates llms.txt with hero metadata and sidebar-ordered TOC', async () => { - await generateLlmsTxt(makeConfig()) + const config = makeConfig() + seed(config) + await generateLlmsTxt(config) const llmsTxt = fs.readFileSync(path.join(outDir, 'llms.txt'), 'utf-8') @@ -161,7 +167,9 @@ describe('node/build/generateLlmsTxt', () => { }) test('emits per-page markdown files with url frontmatter', async () => { - await generateLlmsTxt(makeConfig()) + const config = makeConfig() + seed(config) + await generateLlmsTxt(config) // dir/index.md collapses to dir.md const guide = fs.readFileSync(path.join(outDir, 'guide.md'), 'utf-8') @@ -177,17 +185,30 @@ describe('node/build/generateLlmsTxt', () => { // original frontmatter is replaced expect(advanced).not.toContain('title: Advanced Guide') - // includes are expanded + // collected sources have includes already expanded const reference = fs.readFileSync( path.join(outDir, 'api/reference.md'), 'utf-8' ) expect(reference).toContain('Shared API notes.') - expect(reference).not.toContain('@include') + }) + + test('falls back to reading the source file when a page was not collected', async () => { + const config = makeConfig() + const { 'unlisted.md': unlisted, ...collected } = fixtures + seed(config, collected) + fs.writeFileSync(path.join(srcDir, 'unlisted.md'), unlisted) + + await generateLlmsTxt(config) + + const out = fs.readFileSync(path.join(outDir, 'unlisted.md'), 'utf-8') + expect(out).toContain('# Unlisted Page') }) test('generates llms-full.txt with all pages in TOC order', async () => { - await generateLlmsTxt(makeConfig()) + const config = makeConfig() + seed(config) + await generateLlmsTxt(config) const full = fs.readFileSync(path.join(outDir, 'llms-full.txt'), 'utf-8') @@ -202,7 +223,9 @@ describe('node/build/generateLlmsTxt', () => { }) test('skips non-root locales and dynamic routes', async () => { - await generateLlmsTxt(makeConfig()) + const config = makeConfig() + seed(config) + await generateLlmsTxt(config) expect(fs.existsSync(path.join(outDir, 'fr/guide.md'))).toBe(false) expect(fs.existsSync(path.join(outDir, 'data/1.md'))).toBe(false) @@ -212,14 +235,14 @@ describe('node/build/generateLlmsTxt', () => { }) test('applies rewrites to output paths and links', async () => { - await generateLlmsTxt( - makeConfig({ - rewrites: { - map: { 'guide/advanced.md': 'advanced.md' }, - inv: { 'advanced.md': 'guide/advanced.md' } - } - }) - ) + const config = makeConfig({ + rewrites: { + map: { 'guide/advanced.md': 'advanced.md' }, + inv: { 'advanced.md': 'guide/advanced.md' } + } + }) + seed(config) + await generateLlmsTxt(config) expect(fs.existsSync(path.join(outDir, 'advanced.md'))).toBe(true) expect(fs.existsSync(path.join(outDir, 'guide/advanced.md'))).toBe(false) @@ -229,8 +252,14 @@ describe('node/build/generateLlmsTxt', () => { }) test('detects the landing page through rewrites (en/index.md -> index.md)', async () => { - fs.rmSync(path.join(srcDir, 'index.md')) - writeFixture(srcDir, { + const config = makeConfig({ + pages: ['en/guide.md', 'en/index.md', 'unlisted.md'], + rewrites: { + map: { 'en/index.md': 'index.md', 'en/guide.md': 'guide.md' }, + inv: { 'index.md': 'en/index.md', 'guide.md': 'en/guide.md' } + } + }) + seed(config, { 'en/index.md': [ '---', 'layout: home', @@ -239,15 +268,8 @@ describe('node/build/generateLlmsTxt', () => { ' text: Rewritten description', '---' ].join('\n'), - 'en/guide.md': '# Rewritten Guide' - }) - - const config = makeConfig({ - pages: ['en/guide.md', 'en/index.md', 'unlisted.md'], - rewrites: { - map: { 'en/index.md': 'index.md', 'en/guide.md': 'guide.md' }, - inv: { 'index.md': 'en/index.md', 'guide.md': 'en/guide.md' } - } + 'en/guide.md': '# Rewritten Guide', + 'unlisted.md': fixtures['unlisted.md'] }) await generateLlmsTxt(config) @@ -279,6 +301,7 @@ describe('node/build/generateLlmsTxt', () => { } } } + seed(config) await generateLlmsTxt(config) @@ -292,8 +315,9 @@ describe('node/build/generateLlmsTxt', () => { test('falls back to site title/description and flat TOC without sidebar', async () => { const config = makeConfig() delete (config.site.themeConfig as any).sidebar - fs.rmSync(path.join(srcDir, 'index.md')) config.pages = config.pages.filter((p) => p !== 'index.md') + const { 'index.md': _, ...collected } = fixtures + seed(config, collected) await generateLlmsTxt(config) @@ -306,6 +330,7 @@ describe('node/build/generateLlmsTxt', () => { test('respects base in generated links', async () => { const config = makeConfig() config.site.base = '/docs/' + seed(config) await generateLlmsTxt(config) @@ -314,11 +339,11 @@ describe('node/build/generateLlmsTxt', () => { }) test('skips pages matching ignoreFiles patterns', async () => { - await generateLlmsTxt( - makeConfig({ - llms: { hostname: 'https://example.com', ignoreFiles: ['api/**'] } - }) - ) + const config = makeConfig({ + llms: { hostname: 'https://example.com', ignoreFiles: ['api/**'] } + }) + seed(config) + await generateLlmsTxt(config) expect(fs.existsSync(path.join(outDir, 'api/reference.md'))).toBe(false) expect(fs.existsSync(path.join(outDir, 'guide/advanced.md'))).toBe(true) @@ -331,18 +356,18 @@ describe('node/build/generateLlmsTxt', () => { }) test('matches ignoreFiles against rewritten output paths too', async () => { - await generateLlmsTxt( - makeConfig({ - llms: { - hostname: 'https://example.com', - ignoreFiles: ['advanced.md'] - }, - rewrites: { - map: { 'guide/advanced.md': 'advanced.md' }, - inv: { 'advanced.md': 'guide/advanced.md' } - } - }) - ) + const config = makeConfig({ + llms: { + hostname: 'https://example.com', + ignoreFiles: ['advanced.md'] + }, + rewrites: { + map: { 'guide/advanced.md': 'advanced.md' }, + inv: { 'advanced.md': 'guide/advanced.md' } + } + }) + seed(config) + await generateLlmsTxt(config) expect(fs.existsSync(path.join(outDir, 'advanced.md'))).toBe(false) @@ -351,7 +376,10 @@ describe('node/build/generateLlmsTxt', () => { }) test('unwraps llm-only and drops llm-exclude in LLM output', async () => { - writeFixture(srcDir, { + const config = makeConfig() + config.pages = [...config.pages, 'tags.md'] + seed(config, { + ...fixtures, 'tags.md': [ '# Tags', '', @@ -369,9 +397,6 @@ describe('node/build/generateLlmsTxt', () => { ].join('\n') }) - const config = makeConfig() - config.pages = [...config.pages, 'tags.md'] - await generateLlmsTxt(config) const tags = fs.readFileSync(path.join(outDir, 'tags.md'), 'utf-8') diff --git a/__tests__/unit/node/markdownToVue.test.ts b/__tests__/unit/node/markdownToVue.test.ts index e4ac59d3f99e..d60278e2087d 100644 --- a/__tests__/unit/node/markdownToVue.test.ts +++ b/__tests__/unit/node/markdownToVue.test.ts @@ -1,3 +1,4 @@ +import { getLlmsSources } from 'node/build/generateLlmsTxt' import { resolveConfig } from 'node/config' import { createMarkdownToVueRenderFn } from 'node/markdownToVue' import { mkdtemp, rm, writeFile } from 'node:fs/promises' @@ -184,6 +185,36 @@ describe('node/markdownToVue', () => { expect(result.vueSrc).not.toContain('llm-exclude') }) + test('collects the include-expanded source with llm tags intact', async () => { + root = await mkdtemp(path.join(tmpdir(), 'vitepress-llm-collect-')) + + const file = path.join(root, 'index.md') + const src = + '# Home\n\n\n\n\n\nSecret for LLMs.\n\n\n' + await writeFile(file, src) + await writeFile(path.join(root, 'shared.md'), 'Shared notes.\n') + + const siteConfig = await resolveConfig(root, 'build', 'production') + siteConfig.llms = true + const render = await createMarkdownToVueRenderFn( + siteConfig.srcDir, + { cache: false }, + '/', + false, + false, + siteConfig + ) + + await render(src, file, 'public') + + const collected = getLlmsSources(siteConfig)?.get('index.md') + expect(collected).toContain('Shared notes.') + expect(collected).not.toContain('@include') + // llm-only content is stripped from HTML but kept for the LLM output + expect(collected).toContain('Secret for LLMs.') + expect(collected).toContain('') + }) + test('leaves llm tags untouched when llms is not enabled', async () => { root = await mkdtemp(path.join(tmpdir(), 'vitepress-llm-tags-')) diff --git a/src/node/build/generateLlmsTxt.ts b/src/node/build/generateLlmsTxt.ts index 2493b58230ab..79c53bfcc463 100644 --- a/src/node/build/generateLlmsTxt.ts +++ b/src/node/build/generateLlmsTxt.ts @@ -5,11 +5,8 @@ import pMap from 'p-map' import picomatch from 'picomatch' import type { SiteConfig } from '../config' import type { DefaultTheme } from '../defaultTheme' -import type { MarkdownRenderer } from '../markdown/markdown' -import { createMarkdownRenderer } from '../markdown/markdown' import { resolveSiteDataByRoute } from '../shared' import { isLlmsEnabled, resolveLlmTags } from '../utils/llmTags' -import { processIncludes } from '../utils/processIncludes' import { task } from '../utils/task' export interface LlmsOptions { @@ -58,7 +55,30 @@ interface LlmsPage { content: string } -const includesRE = /` - let md: MarkdownRenderer | undefined - const getMd = async () => - (md ??= await createMarkdownRenderer( - siteConfig.srcDir, - siteConfig.markdown, - siteConfig.site.base, - siteConfig.logger - )) + const sources = getLlmsSources(siteConfig) let indexFrontmatter: Record = {} @@ -217,10 +229,13 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { if (dynamicPaths.has(page)) return if (skippedLocaleDirs.has(page.split('/')[0])) return - const srcPath = path.join(siteConfig.srcDir, page) - const { data, content: rawContent } = matter( - await fs.readFile(srcPath, 'utf-8') - ) + // collected during the markdown → Vue transform with includes + // already expanded; the fs fallback only covers pages that never + // went through the bundle (should not happen in practice) + const src = + sources?.get(page) ?? + (await fs.readFile(path.join(siteConfig.srcDir, page), 'utf-8')) + const { data, content: rawContent } = matter(src) const outPath = collapseIndexPath( siteConfig.rewrites.map[page] || page @@ -234,18 +249,7 @@ export async function generateLlmsTxt(siteConfig: SiteConfig) { if (isIgnored?.(page) || isIgnored?.(outPath)) return - let content = rawContent - if (includesRE.test(content)) { - content = processIncludes( - await getMd(), - siteConfig.srcDir, - content, - srcPath, - [], - !!siteConfig.cleanUrls - ) - } - content = resolveLlmTags(content) + const content = resolveLlmTags(rawContent) return { outPath, diff --git a/src/node/markdownToVue.ts b/src/node/markdownToVue.ts index d1654dce6599..2530bb890379 100644 --- a/src/node/markdownToVue.ts +++ b/src/node/markdownToVue.ts @@ -3,6 +3,7 @@ import { LRUCache } from 'lru-cache' import fs from 'node:fs' import path from 'node:path' import { createDebug } from 'obug' +import { collectLlmsSource } from './build/generateLlmsTxt' import type { SiteConfig } from './config' import { createMarkdownRenderer, @@ -144,8 +145,19 @@ export async function createMarkdownToVueRenderFn( src = processIncludes(md, srcDir, src, fileOrig, includes, cleanUrls) // llm-only content is not rendered to HTML — it only appears in the - // markdown output generated when the llms option is enabled - if (isLlmsEnabled(siteConfig?.llms)) src = stripLlmTags(src) + // markdown output generated when the llms option is enabled, from the + // include-expanded source collected here (dynamic routes share one + // source file and are excluded from LLM output) + if (isLlmsEnabled(siteConfig?.llms)) { + if (!dynamicRoute) { + collectLlmsSource( + siteConfig, + slash(path.relative(srcDir, fileOrig)), + src + ) + } + src = stripLlmTags(src) + } const localeIndex = getLocaleForPath(siteConfig?.site, relativePath) From b111e5fc35b0dcfbae868c06b334340531ff0381 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 13 Jul 2026 16:52:20 +0200 Subject: [PATCH 7/7] style: fix lint --- __tests__/unit/node/build/generateLlmsTxt.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/__tests__/unit/node/build/generateLlmsTxt.test.ts b/__tests__/unit/node/build/generateLlmsTxt.test.ts index d067b56431de..6d6498d07fec 100644 --- a/__tests__/unit/node/build/generateLlmsTxt.test.ts +++ b/__tests__/unit/node/build/generateLlmsTxt.test.ts @@ -2,10 +2,7 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import type { Logger } from 'vite' -import { - collectLlmsSource, - generateLlmsTxt -} from 'node/build/generateLlmsTxt' +import { collectLlmsSource, generateLlmsTxt } from 'node/build/generateLlmsTxt' import type { SiteConfig } from 'node/siteConfig' const logger = {