From 44303ae331c7ae9bf575b7d02a184760f6f82ff4 Mon Sep 17 00:00:00 2001
From: Andrei Ivascu <7030530+aivascu@users.noreply.github.com>
Date: Mon, 31 Aug 2026 23:10:41 +0300
Subject: [PATCH 1/6] Add llms.txt and robots.txt for agent discovery.
---
site/public/llms.txt | 69 ++++++++++++++++++++++++++++++++++++++++++
site/public/robots.txt | 4 +++
2 files changed, 73 insertions(+)
create mode 100644 site/public/llms.txt
create mode 100644 site/public/robots.txt
diff --git a/site/public/llms.txt b/site/public/llms.txt
new file mode 100644
index 0000000..ee2ab78
--- /dev/null
+++ b/site/public/llms.txt
@@ -0,0 +1,69 @@
+# AutoFixture documentation
+> AutoFixture creates anonymous test data so you write less arrange code in .NET unit tests.
+
+This file helps LLM agents discover readable documentation on https://autofixture.com.
+
+Prefer markdown URLs when you need exact C# signatures (generics stay intact). HTML pages are fine for how-to reading.
+
+## Start here
+
+- https://autofixture.com/docs/get-started/introduction
+- https://autofixture.com/docs/get-started/installation
+- https://autofixture.com/docs/get-started/first-test
+- https://autofixture.com/docs/reference/cheat-sheet
+- https://autofixture.com/docs/reference/faq
+
+## Guide HTML
+
+Guides live under `/docs/{section}/{page}`.
+
+- https://autofixture.com/docs/get-started/introduction
+- https://autofixture.com/docs/fundamentals/fixture-and-create
+- https://autofixture.com/docs/fundamentals/build-dsl
+- https://autofixture.com/docs/fundamentals/customizations
+- https://autofixture.com/docs/how-to/collections
+- https://autofixture.com/docs/integrations/overview
+- https://autofixture.com/docs/integrations/xunit3
+- https://autofixture.com/docs/integrations/automoq
+- https://autofixture.com/docs/advanced/specimen-pipeline
+- https://autofixture.com/docs/reference/packages
+- https://autofixture.com/docs/reference/v4-to-v5-migration
+
+## Guide markdown (preferred for agents)
+
+Same content as HTML, as raw markdown:
+
+ https://autofixture.com/docs-markdown/{section}/{page}.md
+
+Examples:
+
+- https://autofixture.com/docs-markdown/get-started/introduction.md
+- https://autofixture.com/docs-markdown/fundamentals/customizations.md
+- https://autofixture.com/docs-markdown/reference/cheat-sheet.md
+
+## API reference
+
+Browse HTML: https://autofixture.com/api (redirects to latest AutoFixture v5)
+
+Raw API markdown (best for signatures):
+
+ https://autofixture.com/api-markdown/{packageId}/{versionSegment}/{slug}.md
+
+Example (Fixture class, v5):
+
+- https://autofixture.com/api-markdown/autofixture/5-0-0-rc-1/autofixture.fixture.md
+- HTML twin: https://autofixture.com/api/autofixture/5-0-0-rc-1/autofixture.fixture/
+
+Slugs and version segments are lowercase. Package ids match the catalog (autofixture, xunit3, automoq, idioms, …).
+
+## Machine-readable indexes
+
+- https://autofixture.com/api-catalog.json — packages and versions
+- https://autofixture.com/api-meta/routes.json — all API HTML routes
+- https://autofixture.com/api-meta/{packageId}/{versionSegment}/toc.json — package TOC
+- https://autofixture.com/api-meta/{packageId}/{versionSegment}/search.json — searchable API snippets
+- https://autofixture.com/sitemap.xml — HTML and markdown URLs
+
+## Optional
+
+- https://autofixture.com/robots.txt
diff --git a/site/public/robots.txt b/site/public/robots.txt
new file mode 100644
index 0000000..1a253a4
--- /dev/null
+++ b/site/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://autofixture.com/sitemap.xml
From a9ac71319717d6f6904a91835448efda850ffc97 Mon Sep 17 00:00:00 2001
From: Andrei Ivascu <7030530+aivascu@users.noreply.github.com>
Date: Mon, 31 Aug 2026 23:11:20 +0300
Subject: [PATCH 2/6] Add prepare-agent-assets script for docs-markdown and
sitemap.
---
site/scripts/prepare-agent-assets.mjs | 178 ++++++++++++++++++++++++++
1 file changed, 178 insertions(+)
create mode 100644 site/scripts/prepare-agent-assets.mjs
diff --git a/site/scripts/prepare-agent-assets.mjs b/site/scripts/prepare-agent-assets.mjs
new file mode 100644
index 0000000..dc60865
--- /dev/null
+++ b/site/scripts/prepare-agent-assets.mjs
@@ -0,0 +1,178 @@
+/**
+ * Prepare agent-facing static assets before `nuxt generate`:
+ * - Mirror guide markdown to public/docs-markdown/ (URL-aligned paths)
+ * - Write public/sitemap.xml (guides, docs-markdown, API routes when present)
+ *
+ * Run from site/: node scripts/prepare-agent-assets.mjs
+ * Or via npm pregenerate / just site-generate.
+ */
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const siteRoot = path.resolve(__dirname, '..')
+const contentDocs = path.join(siteRoot, 'content', 'docs')
+const publicDir = path.join(siteRoot, 'public')
+const docsMarkdownDir = path.join(publicDir, 'docs-markdown')
+const sitemapPath = path.join(publicDir, 'sitemap.xml')
+const routesJsonPath = path.join(publicDir, 'api-meta', 'routes.json')
+
+const SITE_ORIGIN = 'https://autofixture.com'
+
+/** Strip Nuxt Content-style numeric prefixes: "1.get-started" → "get-started" */
+function stripNumericPrefix(segment) {
+ return segment.replace(/^\d+\./, '')
+}
+
+/**
+ * Map content-relative path to public docs path without leading slash.
+ * e.g. "1.get-started/1.introduction.md" → "get-started/introduction.md"
+ */
+function toPublicDocsRel(relFromDocs) {
+ const parts = relFromDocs.split(/[/\\]/).filter(Boolean)
+ const mapped = parts.map((part, index) => {
+ if (index === parts.length - 1 && part.toLowerCase().endsWith('.md')) {
+ const base = part.slice(0, -3)
+ return `${stripNumericPrefix(base)}.md`
+ }
+ return stripNumericPrefix(part)
+ })
+ return mapped.join('/')
+}
+
+function walkMarkdownFiles(dir, base = dir) {
+ /** @type {string[]} */
+ const files = []
+ if (!fs.existsSync(dir)) return files
+
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name)
+ if (entry.isDirectory()) {
+ files.push(...walkMarkdownFiles(full, base))
+ continue
+ }
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
+ files.push(path.relative(base, full))
+ }
+ }
+ return files
+}
+
+function removeIfExists(target) {
+ if (fs.existsSync(target)) {
+ fs.rmSync(target, { recursive: true, force: true })
+ }
+}
+
+function ensureDir(dir) {
+ fs.mkdirSync(dir, { recursive: true })
+}
+
+/**
+ * @returns {{ htmlPaths: string[], markdownPaths: string[] }}
+ */
+function mirrorDocsMarkdown() {
+ removeIfExists(docsMarkdownDir)
+ ensureDir(docsMarkdownDir)
+
+ /** @type {string[]} */
+ const htmlPaths = []
+ /** @type {string[]} */
+ const markdownPaths = []
+
+ const files = walkMarkdownFiles(contentDocs)
+ for (const rel of files) {
+ const publicRel = toPublicDocsRel(rel)
+ const dest = path.join(docsMarkdownDir, publicRel)
+ ensureDir(path.dirname(dest))
+ fs.copyFileSync(path.join(contentDocs, rel), dest)
+
+ const withoutExt = publicRel.replace(/\.md$/i, '')
+ htmlPaths.push(`/docs/${withoutExt}`)
+ markdownPaths.push(`/docs-markdown/${publicRel.replace(/\\/g, '/')}`)
+ }
+
+ htmlPaths.sort()
+ markdownPaths.sort()
+ return { htmlPaths, markdownPaths }
+}
+
+function readApiRoutes() {
+ if (!fs.existsSync(routesJsonPath)) return []
+ try {
+ const data = JSON.parse(fs.readFileSync(routesJsonPath, 'utf8'))
+ if (!Array.isArray(data)) return []
+ return data.filter((r) => typeof r === 'string' && r.startsWith('/'))
+ } catch {
+ return []
+ }
+}
+
+function escapeXml(value) {
+ return value
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+}
+
+/**
+ * @param {string[]} locs
+ */
+function writeSitemap(locs) {
+ const unique = [...new Set(locs)]
+ unique.sort()
+
+ const body = unique
+ .map(
+ (loc) => `
+ ${escapeXml(`${SITE_ORIGIN}${loc}`)}
+ `,
+ )
+ .join('\n')
+
+ const xml = `
+
+${body}
+
+`
+
+ fs.writeFileSync(sitemapPath, xml, 'utf8')
+ return unique.length
+}
+
+function main() {
+ if (!fs.existsSync(contentDocs)) {
+ console.error(`Missing content docs at ${contentDocs}`)
+ process.exit(1)
+ }
+
+ const { htmlPaths, markdownPaths } = mirrorDocsMarkdown()
+ const apiRoutes = readApiRoutes()
+
+ const locs = [
+ '/',
+ '/llms.txt',
+ '/robots.txt',
+ '/api-catalog.json',
+ ...htmlPaths,
+ ...markdownPaths,
+ ...apiRoutes,
+ ]
+
+ // Prefer catalog when prepare-api has run; omit if missing (local docs-only).
+ if (!fs.existsSync(path.join(publicDir, 'api-catalog.json'))) {
+ const i = locs.indexOf('/api-catalog.json')
+ if (i >= 0) locs.splice(i, 1)
+ }
+
+ const count = writeSitemap(locs)
+
+ console.log(
+ `prepare-agent-assets: mirrored ${htmlPaths.length} guides → docs-markdown/; sitemap ${count} URLs` +
+ (apiRoutes.length ? ` (incl. ${apiRoutes.length} API routes)` : ' (no api-meta/routes.json yet)'),
+ )
+}
+
+main()
From bf85b4268a9e75b62fc8136678908550f2adeac0 Mon Sep 17 00:00:00 2001
From: Andrei Ivascu <7030530+aivascu@users.noreply.github.com>
Date: Mon, 31 Aug 2026 23:11:48 +0300
Subject: [PATCH 3/6] Wire agent assets into generate, CI, and gitignore.
---
.github/actions/build-site/action.yml | 5 +++++
.gitignore | 2 ++
justfile | 8 ++++++--
site/package.json | 2 ++
4 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/.github/actions/build-site/action.yml b/.github/actions/build-site/action.yml
index fbd3aeb..9c33891 100644
--- a/.github/actions/build-site/action.yml
+++ b/.github/actions/build-site/action.yml
@@ -49,6 +49,11 @@ runs:
run: npm ci
working-directory: site
+ - name: Prepare agent assets
+ shell: bash
+ run: npm run prepare-agent-assets
+ working-directory: site
+
- name: Generate static site
shell: bash
run: npm run generate
diff --git a/.gitignore b/.gitignore
index 25b9ae1..7102b4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,8 @@ site/.data/
site/public/api-markdown/
site/public/api-meta/
site/public/api-catalog.json
+site/public/docs-markdown/
+site/public/sitemap.xml
###############
# OS / IDE
diff --git a/justfile b/justfile
index 1a850d2..1f110f7 100644
--- a/justfile
+++ b/justfile
@@ -29,8 +29,12 @@ site-install:
site-dev:
npm run dev --prefix site
-# Statically generate the site (same as CI; run prepare-api first)
-site-generate:
+# Mirror guide markdown + write sitemap (also runs via npm pregenerate)
+prepare-agent-assets:
+ npm run prepare-agent-assets --prefix site
+
+# Statically generate the site (same as CI; run prepare-api first so API routes enter the sitemap)
+site-generate: prepare-agent-assets
npm run generate --prefix site
# Prepare API docs and generate the static site (CI parity)
diff --git a/site/package.json b/site/package.json
index 6cedbcd..c7b4e0a 100644
--- a/site/package.json
+++ b/site/package.json
@@ -5,6 +5,8 @@
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev --port 3000",
+ "prepare-agent-assets": "node scripts/prepare-agent-assets.mjs",
+ "pregenerate": "node scripts/prepare-agent-assets.mjs",
"generate": "nuxt generate",
"preview": "nuxt preview"
},
From 0f81dbde075e61d0585f1f10cd900178b9a67b6d Mon Sep 17 00:00:00 2001
From: Andrei Ivascu <7030530+aivascu@users.noreply.github.com>
Date: Mon, 31 Aug 2026 23:15:39 +0300
Subject: [PATCH 4/6] Preserve C# generics in API HTML for agent readers.
---
site/server/utils/renderApiMarkdown.ts | 39 +++++++++++++++++++++++++-
1 file changed, 38 insertions(+), 1 deletion(-)
diff --git a/site/server/utils/renderApiMarkdown.ts b/site/server/utils/renderApiMarkdown.ts
index e550131..95f4eb8 100644
--- a/site/server/utils/renderApiMarkdown.ts
+++ b/site/server/utils/renderApiMarkdown.ts
@@ -1,6 +1,13 @@
import MarkdownIt from 'markdown-it'
import { getCodeHighlighter, normalizeLang, SHIKI_THEMES } from './highlightCode'
+/** Private-use placeholders so markdown-it html_inline does not eat C# generics. */
+const LT = '\uE000'
+const GT = '\uE001'
+
+const HTML_TAG_RE =
+ /^<\/?(?:a|abbr|b|br|code|div|em|h[1-6]|hr|i|img|li|ol|p|pre|span|strong|sub|sup|table|tbody|td|th|thead|tr|ul)(?:\s[\s\S]*)?>$/i
+
function stripHtml(value: string) {
return value
.replace(/<[^>]+>/g, '')
@@ -55,6 +62,34 @@ function stripFrontmatter(markdown: string) {
return markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '')
}
+/**
+ * Keep DocFX / intentional HTML tags; rewrite C# generics like Create<T>()
+ * so markdown-it with html:true does not treat them as tags.
+ * Fenced and inline code are left untouched for Shiki / default code rendering.
+ */
+export function protectNonHtmlAngleBrackets(markdown: string) {
+ const slots: string[] = []
+ const park = (match: string) => {
+ const index = slots.length
+ slots.push(match)
+ return `\0SLOT${index}\0`
+ }
+
+ let text = markdown.replace(/```[\s\S]*?```/g, park)
+ text = text.replace(/`[^`\n]+`/g, park)
+
+ text = text.replace(/<[^>\n]+>/g, (match) => {
+ if (HTML_TAG_RE.test(match)) return match
+ return match.replaceAll('<', LT).replaceAll('>', GT)
+ })
+
+ return text.replace(/\0SLOT(\d+)\0/g, (_, index: string) => slots[Number(index)]!)
+}
+
+export function restoreProtectedAngleBrackets(html: string) {
+ return html.replaceAll(LT, '<').replaceAll(GT, '>')
+}
+
export async function renderApiMarkdown(markdown: string) {
const highlighter = await getCodeHighlighter()
const md = new MarkdownIt({
@@ -81,5 +116,7 @@ export async function renderApiMarkdown(markdown: string) {
md.renderer.rules.table_open = () => '
'
md.renderer.rules.table_close = () => '
'
- return normalizeHeadingAnchors(md.render(stripFrontmatter(markdown)))
+ const prepared = protectNonHtmlAngleBrackets(stripFrontmatter(markdown))
+ const html = md.render(prepared)
+ return restoreProtectedAngleBrackets(normalizeHeadingAnchors(html))
}
From f690709d4504cc7e1ed2d8f7e7f4d0b56eba3caa Mon Sep 17 00:00:00 2001
From: Andrei Ivascu <7030530+aivascu@users.noreply.github.com>
Date: Mon, 31 Aug 2026 23:16:03 +0300
Subject: [PATCH 5/6] Document agent assets and prepare-api build order.
---
readme.md | 2 ++
site/readme.md | 26 +++++++++++++++++++++++++-
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/readme.md b/readme.md
index a11ddad..3c61e20 100644
--- a/readme.md
+++ b/readme.md
@@ -19,6 +19,8 @@ Or via just: `just prepare-api`
API content is generated locally and not committed. Run `prepare` before starting the dev server.
+For a full static build, run `prepare-api` before `site-generate` so the agent sitemap includes API routes. See [site/readme.md](./site/readme.md) for `/llms.txt`, docs-markdown mirroring, and sitemap details.
+
Needs [DocFX](https://dotnet.github.io/docfx/), [Node.js](https://nodejs.org/), and optionally [just](https://github.com/casey/just#installation).
## CI / deploy
diff --git a/site/readme.md b/site/readme.md
index 232e846..adc42d0 100644
--- a/site/readme.md
+++ b/site/readme.md
@@ -10,12 +10,36 @@ just prepare-api # generate + copy API markdown into public/api-markdown
just site-dev
```
+For a production-like static build, run `prepare-api` before `generate` so API routes are included in the sitemap:
+
+```bash
+just prepare-api
+just site-generate # runs prepare-agent-assets, then nuxt generate
+```
+
## Routes
| Path | Content |
|------|---------|
| `/` | Home |
-| `/docs/**` | Guides (Get started section; docs layout) |
+| `/docs/**` | Guides (docs layout) |
+| `/docs-markdown/**` | Same guides as raw markdown (for LLM agents; generated at build) |
| `/api/{package}/{version}/**` | Generated API reference |
+| `/api-markdown/**` | Raw API markdown (generated; not committed) |
+| `/llms.txt` | Agent discovery index |
+| `/robots.txt` | Crawler rules + sitemap pointer |
+| `/sitemap.xml` | Generated URL list (guides, docs-markdown, API) |
API markdown is generated into `public/api-markdown` (not Nuxt Content). On API pages, the header shows the package and version from the API catalog.
+
+## Agent assets
+
+`scripts/prepare-agent-assets.mjs` (also `npm run prepare-agent-assets` / `pregenerate`):
+
+1. Mirrors `content/docs/**/*.md` → `public/docs-markdown/` with Nuxt-style paths (numeric prefixes stripped).
+2. Writes `public/sitemap.xml` from those guides plus `public/api-meta/routes.json` when present.
+
+Committed: `public/llms.txt`, `public/robots.txt`.
+Generated (gitignored): `public/docs-markdown/`, `public/sitemap.xml`.
+
+Keep `llms.txt` in sync when you add major guide sections.
From 503e1f5c524f6e74d7bfa801d1bbe1813935d947 Mon Sep 17 00:00:00 2001
From: Andrei Ivascu <7030530+aivascu@users.noreply.github.com>
Date: Tue, 1 Sep 2026 00:01:01 +0300
Subject: [PATCH 6/6] Unescape DocFX \> in generic declarations for clean
markdown and HTML.
---
api-gen/lib/postprocess.mjs | 16 +++++++++++++---
site/server/utils/renderApiMarkdown.ts | 12 +++++++++++-
2 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/api-gen/lib/postprocess.mjs b/api-gen/lib/postprocess.mjs
index 937999d..558ef50 100644
--- a/api-gen/lib/postprocess.mjs
+++ b/api-gen/lib/postprocess.mjs
@@ -116,6 +116,14 @@ function repairBrokenMarkdownLinks(content) {
)
}
+/** DocFX escapes >, (, ) in signatures; undo so generics read as Create(). */
+export function unescapeDocFxMarkdown(content) {
+ return content
+ .replace(/\\>/g, '>')
+ .replace(/\\\(/g, '(')
+ .replace(/\\\)/g, ')')
+}
+
function rewriteMarkdownLinks(content, packageId, versionSegment, packageDir, pageIndex) {
function linkFor(file, hash = '') {
const slug = String(file).replace(/\.md$/i, '').toLowerCase()
@@ -179,9 +187,11 @@ function processPackageDir(outRoot, dir, pageIndex) {
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) continue
const filePath = path.join(dir, entry.name)
const original = fs.readFileSync(filePath, 'utf8')
- const updated = annotateMemberHeadingIds(
- rewriteMarkdownLinks(original, packageId, versionSegment, dir, pageIndex),
- ).replace(/See the \[table of contents\]\(\.\/toc\.yml\)\.\r?\n?/i, '')
+ const updated = unescapeDocFxMarkdown(
+ annotateMemberHeadingIds(
+ rewriteMarkdownLinks(original, packageId, versionSegment, dir, pageIndex),
+ ).replace(/See the \[table of contents\]\(\.\/toc\.yml\)\.\r?\n?/i, ''),
+ )
if (updated !== original) {
fs.writeFileSync(filePath, updated)
}
diff --git a/site/server/utils/renderApiMarkdown.ts b/site/server/utils/renderApiMarkdown.ts
index 95f4eb8..1bf42fa 100644
--- a/site/server/utils/renderApiMarkdown.ts
+++ b/site/server/utils/renderApiMarkdown.ts
@@ -62,6 +62,14 @@ function stripFrontmatter(markdown: string) {
return markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '')
}
+/** DocFX escapes >, (, ) in signatures; undo so generics read as Create(). */
+export function unescapeDocFxMarkdown(markdown: string) {
+ return markdown
+ .replace(/\\>/g, '>')
+ .replace(/\\\(/g, '(')
+ .replace(/\\\)/g, ')')
+}
+
/**
* Keep DocFX / intentional HTML tags; rewrite C# generics like Create<T>()
* so markdown-it with html:true does not treat them as tags.
@@ -116,7 +124,9 @@ export async function renderApiMarkdown(markdown: string) {
md.renderer.rules.table_open = () => ''
md.renderer.rules.table_close = () => '
'
- const prepared = protectNonHtmlAngleBrackets(stripFrontmatter(markdown))
+ const prepared = protectNonHtmlAngleBrackets(
+ unescapeDocFxMarkdown(stripFrontmatter(markdown)),
+ )
const html = md.render(prepared)
return restoreProtectedAngleBrackets(normalizeHeadingAnchors(html))
}