[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71FS50MM46P5JZS1Y65VDE5":3,"skill-same:01M71FS50MM46P5JZS1Y65VDE5":77},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":49,"linkOnly":50,"markdownContent":51,"markdownBody":52,"frontMatter":53,"refName":54,"commitSha":55,"skillDirectory":56,"forksCount":57,"repositoryHtmlUrl":58,"repositoryHomepageUrl":59,"ownerHtmlUrl":60,"ownerBlogUrl":6,"ownerDescription":61,"updatedAt":62,"install":63,"resources":76},"01M71FS50MM46P5JZS1Y65VDE5","migrate-to-nuxt-agent-discovery",null,"Migrate a Nuxt site to the nuxt-agent-discovery module, replacing a hand-rolled markdown negotiation and agent discovery layer (md-rewrite modules, raw markdown routes, api-catalog, sitemap.md, agent robots.txt, llms.txt bridges, the markdown pipeline under MCP tools) with configuration and hooks. Use when adopting the module in a site, or when asked to remove a site's own agent-discovery code.","benjamincanac\u002Fnuxt-agent-discovery","benjamincanac","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F739984?v=4","01M71FS3PKCDPD9RP5A91H3XNS","Benjamin Canac","01M71FS3QQ8MP8Y0XRTW0CKQHY",18,"MIT","https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery\u002Fblob\u002Fmain\u002Fskills\u002Fmigrate-to-nuxt-agent-discovery\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48],"discovery","agent discovery","migrate","migrate to nuxt agent discovery","nuxt","llms","with","hand","api","code","rewrite","when","use","MCP","module","hooks","catalog","remove","pipeline","modules","2026-11-10 10:12:30",false,"---\nname: migrate-to-nuxt-agent-discovery\ndescription: Migrate a Nuxt site to the nuxt-agent-discovery module, replacing a hand-rolled markdown negotiation and agent discovery layer (md-rewrite modules, raw markdown routes, api-catalog, sitemap.md, agent robots.txt, llms.txt bridges, the markdown pipeline under MCP tools) with configuration and hooks. Use when adopting the module in a site, or when asked to remove a site's own agent-discovery code.\n---\n\n# Migrating a site to nuxt-agent-discovery\n\nReplace a site's hand-rolled agent-discovery layer with the module. The reference migration is [nuxt\u002Fui#6883](https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fui\u002Fpull\u002F6883): a negotiation core, a CDN rewrite module, a request middleware, an error handler and six route handlers deleted, replaced by one config block and one Nitro plugin holding three hooks. Almost every file it touches is a deletion.\n\nRead the module's README first. It is the only document that ships with the package and the only one guaranteed to match the installed version.\n\nThe shape of the work: **configuration replaces routing code, hooks replace site-specific code, everything else gets deleted.** A migration that keeps a file the module owns is not finished, and one that pushes site knowledge into the module is wrong in the other direction.\n\n## What moves and what stays\n\n| Stays in the site | Moves to the module |\n|---|---|\n| MDC transformers and any content rewriting | `Accept` and User-Agent negotiation |\n| The `llms.txt` content: sections, prose, ordering | The CDN route table and the Nitro middleware |\n| MCP tools: names, descriptions, schemas (Step 6 ports the pipeline under them) | `Vary` and `Link` headers |\n| Hand-authored OpenAPI paths | The raw markdown route |\n| The content backend itself | Markdown error bodies |\n| Page structure and route rules | `.well-known\u002Fapi-catalog`, the MCP server card, `sitemap.md`, the agent `robots.txt` |\n\n`\u002Fllms.txt` and `\u002Fllms-full.txt` stay owned by `nuxt-llms` throughout. The module feeds them, never registers them.\n\n## Step 1: survey what the site has\n\nDo this before touching anything, and write the inventory down. It is also the deletion list.\n\n```sh\nrg -l 'text\u002Fmarkdown|ClaudeBot|GPTBot|api-catalog|llms|Vary' --glob '!node_modules' --glob '!*.lock'\nrg -n 'contentRawMarkdown|llms:generate|content:llms' --glob '!node_modules'\nrg -n 'isr|swr|cache' nuxt.config.ts\nls public\u002Frobots.txt vercel.json 2>\u002Fdev\u002Fnull\n```\n\nRecord:\n\n- **Content backend**: `@nuxt\u002Fcontent`, comark, or something else. Decides the `source` option.\n- **Deploy target**: Vercel gets edge negotiation, everything else gets the middleware only, where a prerendered page keeps serving HTML to agents.\n- **Route rules carrying `isr`, `swr` or `cache`**, and which page patterns they cover. The module detects these and switches those patterns to a 307, but you need to know which pages change behaviour.\n- **i18n locales**, which become a wildcard segment in a `routes` pattern rather than one pattern per locale. With `@nuxtjs\u002Fi18n` installed the module detects the locale roots (`\u002Fen`, `\u002Ffr`) itself and wraps their twins as homepages, so they need no entry either.\n- **Companion modules**: `@nuxtjs\u002Frobots` and `@nuxtjs\u002Fsitemap` take over `robots.txt` and `sitemap.xml`, and the module hands off to them automatically.\n- **Every existing agent-facing file**: negotiation utils, an `md-rewrite` module, a raw route, `.well-known` routes, `sitemap.md`, a static `public\u002Frobots.txt`, `vercel.json` rewrites, an error handler chained in `nitro:config`, a `useCanonical` composable, skills served from `publicAssets`, and any `index.md` handler.\n- **Standalone `.md` documents the site serves itself** (`\u002Fdesign.md` and friends). These need `excludePrefixes.extend` or the rewrites will send them to a raw twin that does not exist.\n\n## Step 2: install\n\n```sh\npnpm add -D nuxt-agent-discovery\n```\n\nInstall the latest release and check the version that resolved: refuse to migrate a site onto 0.1.2 or older. Those versions hang `\u002Fllms.txt` and `\u002F` in dev after the first llms request (an oversized `x-nitro-prerender` header the dev proxy drops silently, so the server logs a 200 while the client times out), lose the markdown error handler to any later-registered module that also prepends a Nitro `errorHandler` (evlog does), and serve excluded prefixes from the raw route that every listing denies. Current releases send the hint header only during prerender, re-sort the handler at `nitro:init`, and answer 404 on the raw route.\n\nAdd `'nuxt-agent-discovery'` to `modules`. Order does not matter for anything listed in `modules`, which the module reads directly. `@nuxtjs\u002Frobots`, `@nuxtjs\u002Fsitemap` and `@nuxtjs\u002Fmcp-toolkit` are detected later still, at `modules:done`, so a site getting them through `@nuxtjs\u002Fseo` is covered too. `@nuxt\u002Fcontent` and `nuxt-llms` are read during setup, so a content module pulled in as another module's dependency rather than listed is not seen: set `agentDiscovery.source` explicitly if the build warns that no content source resolved.\n\n## Step 3: configure\n\nStart from the site's page structure, not from its page list.\n\n```ts\nagentDiscovery: {\n  siteUrl: SITE_URL,\n  siteName: 'Nuxt UI',\n  routes: [\n    { path: '\u002F', raw: '\u002Fraw\u002Findex.md' },\n    '\u002Fdocs\u002F**'\n  ]\n}\n```\n\n- **`routes`** is the decision that matters. One pattern per negotiated tree, `*` for a single segment (a locale), `**` for a subtree. The generated route table is O(patterns), so a site that enumerates pages here has misunderstood the option. Pages outside the patterns keep serving HTML to agents, which is the right answer for marketing pages, pricing, and anything without a markdown representation.\n- **`raw`** is only honoured on exact patterns. Use it for `\u002F` when the landing page is a Vue page rather than a document.\n- **`excludePrefixes.extend`** gets every standalone `.md` route and any API surface the defaults do not already cover. `replace` is only for dropping a default. An excluded path is not a page anywhere: it never negotiates, no listing includes it, and the raw route answers 404 for it. Server code opts back in per call with `includeExcluded` on `getAgentDocument()` and `listAgentPages()`, nothing else does. State it that way in the PR too, since \"only negotiation is affected\" is a wrong claim.\n- **`discovery.links`** carries what only the site knows: its OpenAPI document, its docs entry point, recovery links for error bodies (`header: false` keeps them out of the `Link` header). Rels are validated against the IANA registry and an invented one fails the build. Sites migrating off `rel=\"llms\"`, `rel=\"llms-full\"`, `rel=\"mcp\"` or `rel=\"design\"` do not need replacements: `llms.txt` and `llms-full.txt` are pushed into the registry by the module, the MCP endpoint belongs in `discovery.mcpServerCard`, and a design document is `describedby` or `related`.\n- **`sitemap.markdown`** takes `expand` and `labels` to control grouping. `expand: ['\u002Fdocs']` turns one Docs section into one section per area.\n- **`skills.dir`** replaces serving a skills directory through `publicAssets`, and generates the index from the files on disk.\n- **`robots.disallow`** carries the `Disallow` lines a static `public\u002Frobots.txt` usually exists for, so the static file can always go. Wildcard group only: the per-agent `Allow` groups deliberately exempt the named agents, so what search engines skip stays reachable for them. A site adopting `@nuxtjs\u002Frobots` instead puts its disallows in that module's config, and this one contributes the agent groups and the `Content-Signal` line through `robots:config`, deduplicating the disallows. When `@nuxtjs\u002Frobots` arrives, grep the site's utils first: an auto-imported util named `isBot` silently shadows its composable.\n- Leave `siteUrl` empty only if the site resolves its canonical URL per request. Prerendered documents bake it in.\n\n## Step 4: pick the content source\n\n- **`@nuxt\u002Fcontent`**: nothing to do, `source: 'auto'` detects it.\n- **comark**: write the accessor, since comark sites construct their own content instance. `source: 'comark'` throws on purpose.\n\n```ts\n\u002F\u002F server\u002Futils\u002Fagent-source.ts\nimport { createComarkSource } from '#agent-discovery\u002Fcomark'\n\nexport default createComarkSource(() => getProdContent())\n```\n\n- **Anything else**: a file default-exporting `defineAgentContentSource({ routes, get })`. Implement `list()` too when the backend can return metadata in one call, since `sitemap.md` and the `llms.txt` bridge otherwise fall back to a `get()` per page, and `firstLeaf()` so a section URL redirects to its first document instead of 404ing. A custom adapter rendering straight to markdown must call `absolutizeMarkdownLinks()` itself.\n\n## Step 5: port the site-specific code to hooks\n\nEverything the site knows and the module cannot. This is where a hand-rolled raw route's extras go.\n\n```ts\n\u002F\u002F server\u002Fplugins\u002Fagent-discovery.ts\nexport default defineNitroPlugin((nitroApp) => {\n  \u002F\u002F The MDC transform the old raw route called itself, now applied to the\n  \u002F\u002F minimark tree before it is stringified.\n  nitroApp.hooks.hook('agent-discovery:document', async (event, page) => {\n    await transformMDC(event, page as any)\n  })\n\n  \u002F\u002F The module already lists what `@nuxtjs\u002Fmcp-toolkit` reports. This is for\n  \u002F\u002F what the toolkit cannot know, and it runs last, so append rather than assign.\n  nitroApp.hooks.hook('agent-discovery:mcp-server-card', (event, card) => {\n    card.tools = [...(card.tools ?? []), { name: 'external', description: 'Served elsewhere.' }]\n  })\n\n  \u002F\u002F The generated `\u002Fraw\u002Findex.md`, when the landing page is a Vue page and the\n  \u002F\u002F adapter has no `\u002F` entry to read a title and description off.\n  nitroApp.hooks.hook('agent-discovery:index', (event, index) => {\n    index.title = 'Example'\n    index.description = 'What the site is'\n    index.body.push('...')\n  })\n})\n```\n\nThree helpers replace hand-written equivalents:\n\n- `renderAgentResources(event)` renders the discovery registry as a markdown block, for a page the site renders by hand. The module appends it to the `\u002F` document and to each locale landing document itself, and leaves a body that already carries the heading alone, so those documents have no reason to render it.\n- `agentDiscoveryOpenApi(event)` returns the discovery layer as OpenAPI fragments. Spread the site's own paths last so they win.\n- `rawUrl(event, href)` resolves a page URL to its markdown twin from the same route config, for links the site builds itself inside `llms:generate` hooks.\n\nA site that rewrote its own `llms.txt` links to raw twins can delete that code: the module rewrites every same-origin link. What stays is ordering, prose, and sections.\n\n## Step 6: port the MCP tools\n\nA site with an MCP server always has a hand-rolled duplicate of the adapter pipeline underneath it: a `fetchPageMarkdown`, a section slicer, a hardcoded site URL, usually in a `server\u002Futils\u002Fmcp.ts`. Replace them with the exports from `#agent-discovery` (the README's Agent tooling section) and delete the site util:\n\n- `getAgentDocument(event, path, { sections })` replaces the fetch-and-parse, and returns the exact bytes the raw route serves.\n- `listAgentPages(event, { search, prefix })` replaces the hand-written page listing.\n- `extractSections(markdown, titles)` replaces the section slicer.\n- `getAgentSiteUrl(event)` replaces every hardcoded site URL.\n\nThe tools themselves stay in the site. Names, descriptions and schemas are prompt engineering, and the module ships none.\n\nTwo wrinkles. A tool that deliberately serves excluded content, say a nightly docs version the site's `excludePrefixes` hides, needs `includeExcluded: true` on both `getAgentDocument()` and `listAgentPages()`, since both skip excluded prefixes by default. And a listing over a dimension the adapter does not model, versions for instance, that must include excluded pages can keep its `queryCollection` for the listing while still reading every document through `getAgentDocument()`.\n\n## Step 7: delete the old layer\n\n| Delete | Replaced by |\n|---|---|\n| `modules\u002Fmd-rewrite.ts`, `vercel.json` negotiation routes | the vercel preset in the module |\n| `server\u002Futils\u002FmarkdownNegotiation.ts` and friends | the negotiation core |\n| `server\u002Fmiddleware\u002Fmarkdown.ts` | the module's middleware |\n| `server\u002Ferror.ts` plus the `nitro:config` errorHandler chaining in `nuxt.config` | `errors: true` |\n| `server\u002Froutes\u002Fraw\u002F[...slug].md.get.ts` | the module's raw route |\n| `server\u002Froutes\u002Fraw\u002Findex.md.get.ts` | a `\u002F` document in the content source, or `agent-discovery:index` |\n| `server\u002Froutes\u002F.well-known\u002Fapi-catalog.get.ts`, `.well-known\u002Fmcp\u002Fserver-card.json.get.ts` | `discovery.apiCatalog`, `discovery.mcpServerCard` |\n| `server\u002Froutes\u002Fsitemap.md.get.ts` | `sitemap.markdown` |\n| `public\u002Frobots.txt` with its `Disallow` lines, hand-maintained agent lists | `robots.aiPolicy` and `robots.disallow` |\n| `skills\u002Findex.json`, `publicAssets` entries for skills | `skills.dir` |\n| `app\u002Fcomposables\u002FuseCanonical.ts` | the module's composable |\n| `routeRules` entries setting `Vary` or the discovery `Link` | emitted by the module |\n| llms link-rewriting helpers | the llms bridge |\n| the markdown pipeline under MCP tools (`server\u002Futils\u002Fmcp.ts` and friends) | `getAgentDocument()`, `listAgentPages()`, `extractSections()`, `getAgentSiteUrl()` |\n\nTwo that are easy to miss: the `Vary`\u002F`Link` `routeRules` block, which now conflicts with what the module emits, and the `nitro:config` hook chaining the error handler, which the module does itself.\n\nKeep a hand-written `\u002Fraw\u002Findex.md` handler only when the site wants full control of that document. Otherwise prefer a `\u002F` document in the content source, which the module wraps with the resources block and the sitemap footer, and keep the `agent-discovery:index` hook for metadata when no `\u002F` document exists. On an i18n site the landing documents live at the locale roots, and the module wraps `content\u002Fen\u002Findex.md` and its siblings the same way; the hook only ever fills the generated `\u002F`.\n\n## Adopting `@nuxtjs\u002Fsitemap` in the same PR\n\nA migration often replaces a hand-rolled `sitemap.xml` route at the same time. The raw twins are dropped from the sitemap by this module's exclude either way, but two `@nuxtjs\u002Fsitemap` traps bite hard on exactly the sites this skill targets:\n\n- **Never set `routeRules['\u002Fsitemap.xml'] = { prerender: true }`.** During prerender `@nuxtjs\u002Fsitemap` resolves its own route against the canonical site URL and ingests the live production sitemap, so every deploy ships a copy of the previous one. The proof on a bitten site: the build artifact is a strict subset of production's entries, old changefreq values included.\n- **The `nuxt:prerender` app source lists every prerendered page.** On a versioned docs site that means legacy and nightly versions and unversioned redirect stubs all land in the sitemap. Set `excludeAppSources: true`, point `sitemap.sources` at a small server route querying the content collections, and list the handful of Vue-only pages in `urls`. Content stays the single source of truth.\n\n## Step 8: verify\n\nLocally first:\n\n```sh\npnpm dev\ncurl -sS -D - -H 'Accept: text\u002Fmarkdown' http:\u002F\u002Flocalhost:3000\u002Fdocs\u002F... | head -20\ncurl -sS -D - -A ClaudeBot http:\u002F\u002Flocalhost:3000\u002F | head -20\ncurl -sS http:\u002F\u002Flocalhost:3000\u002Fsitemap.md | head\ncurl -sS http:\u002F\u002Flocalhost:3000\u002F.well-known\u002Fapi-catalog | jq .\ncurl -sS -A ClaudeBot http:\u002F\u002Flocalhost:3000\u002Fnope | head\n```\n\nThen build, and read the log rather than skimming it. The module logs at info when a cached route rule covers a negotiated pattern, so grep for `response cache` rather than for warnings. It warns when no content source resolves, when a static `robots.txt` shadows the generated one, and when `extend` and `replace` are both set, and it throws on an invented link rel or a `siteUrl` carrying a path. Compare the prerendered route count against the old build.\n\nStale state fakes results at every stage here, three ways. Nuxt's build cache can keep serving a server route whose file was deleted: clear `node_modules\u002F.cache\u002Fnuxt` after deleting route files, and remember a CDN's build cache restores it on preview deploys. A built server persists cached functions under `.data` between boots: wipe it, knowing a `@nuxthub` site needs `.data\u002Fdb` back before it boots. And before trusting any curl against a locally booted `.output\u002Fserver`, check the PID actually listening on the port (`lsof -iTCP:\u003Cport>`): a zombie server from a previous build answers every probe and never sees an on-disk edit.\n\nThen deploy a preview and run the `verify-nuxt-agent-discovery` skill against it. Local checks cannot exercise `Vary`, the `Link` header or the CDN rewrites, which is where the interesting failures live.\n\n## Gotchas\n\n- **Never register `\u002Fllms.txt` or `\u002Fllms-full.txt`.** The module refuses to, and a site that adds its own collides with `nuxt-llms`.\n- `@nuxt\u002Fcontent` installs its llms feature, and therefore `\u002Fraw\u002F**:slug.md`, only when `nuxt-llms` is present. The module sets `llms.contentRawMarkdown: false` and takes over both the route and the link rewriting, so the raw route survives a later backend swap.\n- On a cached page the module redirects instead of rewriting, and disables request-time negotiation in production. Dev keeps negotiating.\n- Off Vercel, Nitro serves prerendered files ahead of user handlers, so a prerendered page stays HTML for agents. Either render those routes on demand or accept that agents reach markdown through the `.md` URL.\n- Paths whose last segment is dotted never negotiate, which is what keeps `_payload.json` and images out.\n- A markdown document is read detached from its site, so relative links have to be absolutised. The `@nuxt\u002Fcontent` adapter does it; a custom one has to call `absolutizeMarkdownLinks()`.\n\n## Deliverable\n\nOne PR per site. The description should say what was deleted, what became configuration, what stayed behind a hook, and which behaviour changed for agents (usually: more user agents honoured, `Vary` now correct, markdown errors, and negotiation working in dev). Link the preview check results.\n","# Migrating a site to nuxt-agent-discovery\n\nReplace a site's hand-rolled agent-discovery layer with the module. The reference migration is [nuxt\u002Fui#6883](https:\u002F\u002Fgithub.com\u002Fnuxt\u002Fui\u002Fpull\u002F6883): a negotiation core, a CDN rewrite module, a request middleware, an error handler and six route handlers deleted, replaced by one config block and one Nitro plugin holding three hooks. Almost every file it touches is a deletion.\n\nRead the module's README first. It is the only document that ships with the package and the only one guaranteed to match the installed version.\n\nThe shape of the work: **configuration replaces routing code, hooks replace site-specific code, everything else gets deleted.** A migration that keeps a file the module owns is not finished, and one that pushes site knowledge into the module is wrong in the other direction.\n\n## What moves and what stays\n\n| Stays in the site | Moves to the module |\n|---|---|\n| MDC transformers and any content rewriting | `Accept` and User-Agent negotiation |\n| The `llms.txt` content: sections, prose, ordering | The CDN route table and the Nitro middleware |\n| MCP tools: names, descriptions, schemas (Step 6 ports the pipeline under them) | `Vary` and `Link` headers |\n| Hand-authored OpenAPI paths | The raw markdown route |\n| The content backend itself | Markdown error bodies |\n| Page structure and route rules | `.well-known\u002Fapi-catalog`, the MCP server card, `sitemap.md`, the agent `robots.txt` |\n\n`\u002Fllms.txt` and `\u002Fllms-full.txt` stay owned by `nuxt-llms` throughout. The module feeds them, never registers them.\n\n## Step 1: survey what the site has\n\nDo this before touching anything, and write the inventory down. It is also the deletion list.\n\n```sh\nrg -l 'text\u002Fmarkdown|ClaudeBot|GPTBot|api-catalog|llms|Vary' --glob '!node_modules' --glob '!*.lock'\nrg -n 'contentRawMarkdown|llms:generate|content:llms' --glob '!node_modules'\nrg -n 'isr|swr|cache' nuxt.config.ts\nls public\u002Frobots.txt vercel.json 2>\u002Fdev\u002Fnull\n```\n\nRecord:\n\n- **Content backend**: `@nuxt\u002Fcontent`, comark, or something else. Decides the `source` option.\n- **Deploy target**: Vercel gets edge negotiation, everything else gets the middleware only, where a prerendered page keeps serving HTML to agents.\n- **Route rules carrying `isr`, `swr` or `cache`**, and which page patterns they cover. The module detects these and switches those patterns to a 307, but you need to know which pages change behaviour.\n- **i18n locales**, which become a wildcard segment in a `routes` pattern rather than one pattern per locale. With `@nuxtjs\u002Fi18n` installed the module detects the locale roots (`\u002Fen`, `\u002Ffr`) itself and wraps their twins as homepages, so they need no entry either.\n- **Companion modules**: `@nuxtjs\u002Frobots` and `@nuxtjs\u002Fsitemap` take over `robots.txt` and `sitemap.xml`, and the module hands off to them automatically.\n- **Every existing agent-facing file**: negotiation utils, an `md-rewrite` module, a raw route, `.well-known` routes, `sitemap.md`, a static `public\u002Frobots.txt`, `vercel.json` rewrites, an error handler chained in `nitro:config`, a `useCanonical` composable, skills served from `publicAssets`, and any `index.md` handler.\n- **Standalone `.md` documents the site serves itself** (`\u002Fdesign.md` and friends). These need `excludePrefixes.extend` or the rewrites will send them to a raw twin that does not exist.\n\n## Step 2: install\n\n```sh\npnpm add -D nuxt-agent-discovery\n```\n\nInstall the latest release and check the version that resolved: refuse to migrate a site onto 0.1.2 or older. Those versions hang `\u002Fllms.txt` and `\u002F` in dev after the first llms request (an oversized `x-nitro-prerender` header the dev proxy drops silently, so the server logs a 200 while the client times out), lose the markdown error handler to any later-registered module that also prepends a Nitro `errorHandler` (evlog does), and serve excluded prefixes from the raw route that every listing denies. Current releases send the hint header only during prerender, re-sort the handler at `nitro:init`, and answer 404 on the raw route.\n\nAdd `'nuxt-agent-discovery'` to `modules`. Order does not matter for anything listed in `modules`, which the module reads directly. `@nuxtjs\u002Frobots`, `@nuxtjs\u002Fsitemap` and `@nuxtjs\u002Fmcp-toolkit` are detected later still, at `modules:done`, so a site getting them through `@nuxtjs\u002Fseo` is covered too. `@nuxt\u002Fcontent` and `nuxt-llms` are read during setup, so a content module pulled in as another module's dependency rather than listed is not seen: set `agentDiscovery.source` explicitly if the build warns that no content source resolved.\n\n## Step 3: configure\n\nStart from the site's page structure, not from its page list.\n\n```ts\nagentDiscovery: {\n  siteUrl: SITE_URL,\n  siteName: 'Nuxt UI',\n  routes: [\n    { path: '\u002F', raw: '\u002Fraw\u002Findex.md' },\n    '\u002Fdocs\u002F**'\n  ]\n}\n```\n\n- **`routes`** is the decision that matters. One pattern per negotiated tree, `*` for a single segment (a locale), `**` for a subtree. The generated route table is O(patterns), so a site that enumerates pages here has misunderstood the option. Pages outside the patterns keep serving HTML to agents, which is the right answer for marketing pages, pricing, and anything without a markdown representation.\n- **`raw`** is only honoured on exact patterns. Use it for `\u002F` when the landing page is a Vue page rather than a document.\n- **`excludePrefixes.extend`** gets every standalone `.md` route and any API surface the defaults do not already cover. `replace` is only for dropping a default. An excluded path is not a page anywhere: it never negotiates, no listing includes it, and the raw route answers 404 for it. Server code opts back in per call with `includeExcluded` on `getAgentDocument()` and `listAgentPages()`, nothing else does. State it that way in the PR too, since \"only negotiation is affected\" is a wrong claim.\n- **`discovery.links`** carries what only the site knows: its OpenAPI document, its docs entry point, recovery links for error bodies (`header: false` keeps them out of the `Link` header). Rels are validated against the IANA registry and an invented one fails the build. Sites migrating off `rel=\"llms\"`, `rel=\"llms-full\"`, `rel=\"mcp\"` or `rel=\"design\"` do not need replacements: `llms.txt` and `llms-full.txt` are pushed into the registry by the module, the MCP endpoint belongs in `discovery.mcpServerCard`, and a design document is `describedby` or `related`.\n- **`sitemap.markdown`** takes `expand` and `labels` to control grouping. `expand: ['\u002Fdocs']` turns one Docs section into one section per area.\n- **`skills.dir`** replaces serving a skills directory through `publicAssets`, and generates the index from the files on disk.\n- **`robots.disallow`** carries the `Disallow` lines a static `public\u002Frobots.txt` usually exists for, so the static file can always go. Wildcard group only: the per-agent `Allow` groups deliberately exempt the named agents, so what search engines skip stays reachable for them. A site adopting `@nuxtjs\u002Frobots` instead puts its disallows in that module's config, and this one contributes the agent groups and the `Content-Signal` line through `robots:config`, deduplicating the disallows. When `@nuxtjs\u002Frobots` arrives, grep the site's utils first: an auto-imported util named `isBot` silently shadows its composable.\n- Leave `siteUrl` empty only if the site resolves its canonical URL per request. Prerendered documents bake it in.\n\n## Step 4: pick the content source\n\n- **`@nuxt\u002Fcontent`**: nothing to do, `source: 'auto'` detects it.\n- **comark**: write the accessor, since comark sites construct their own content instance. `source: 'comark'` throws on purpose.\n\n```ts\n\u002F\u002F server\u002Futils\u002Fagent-source.ts\nimport { createComarkSource } from '#agent-discovery\u002Fcomark'\n\nexport default createComarkSource(() => getProdContent())\n```\n\n- **Anything else**: a file default-exporting `defineAgentContentSource({ routes, get })`. Implement `list()` too when the backend can return metadata in one call, since `sitemap.md` and the `llms.txt` bridge otherwise fall back to a `get()` per page, and `firstLeaf()` so a section URL redirects to its first document instead of 404ing. A custom adapter rendering straight to markdown must call `absolutizeMarkdownLinks()` itself.\n\n## Step 5: port the site-specific code to hooks\n\nEverything the site knows and the module cannot. This is where a hand-rolled raw route's extras go.\n\n```ts\n\u002F\u002F server\u002Fplugins\u002Fagent-discovery.ts\nexport default defineNitroPlugin((nitroApp) => {\n  \u002F\u002F The MDC transform the old raw route called itself, now applied to the\n  \u002F\u002F minimark tree before it is stringified.\n  nitroApp.hooks.hook('agent-discovery:document', async (event, page) => {\n    await transformMDC(event, page as any)\n  })\n\n  \u002F\u002F The module already lists what `@nuxtjs\u002Fmcp-toolkit` reports. This is for\n  \u002F\u002F what the toolkit cannot know, and it runs last, so append rather than assign.\n  nitroApp.hooks.hook('agent-discovery:mcp-server-card', (event, card) => {\n    card.tools = [...(card.tools ?? []), { name: 'external', description: 'Served elsewhere.' }]\n  })\n\n  \u002F\u002F The generated `\u002Fraw\u002Findex.md`, when the landing page is a Vue page and the\n  \u002F\u002F adapter has no `\u002F` entry to read a title and description off.\n  nitroApp.hooks.hook('agent-discovery:index', (event, index) => {\n    index.title = 'Example'\n    index.description = 'What the site is'\n    index.body.push('...')\n  })\n})\n```\n\nThree helpers replace hand-written equivalents:\n\n- `renderAgentResources(event)` renders the discovery registry as a markdown block, for a page the site renders by hand. The module appends it to the `\u002F` document and to each locale landing document itself, and leaves a body that already carries the heading alone, so those documents have no reason to render it.\n- `agentDiscoveryOpenApi(event)` returns the discovery layer as OpenAPI fragments. Spread the site's own paths last so they win.\n- `rawUrl(event, href)` resolves a page URL to its markdown twin from the same route config, for links the site builds itself inside `llms:generate` hooks.\n\nA site that rewrote its own `llms.txt` links to raw twins can delete that code: the module rewrites every same-origin link. What stays is ordering, prose, and sections.\n\n## Step 6: port the MCP tools\n\nA site with an MCP server always has a hand-rolled duplicate of the adapter pipeline underneath it: a `fetchPageMarkdown`, a section slicer, a hardcoded site URL, usually in a `server\u002Futils\u002Fmcp.ts`. Replace them with the exports from `#agent-discovery` (the README's Agent tooling section) and delete the site util:\n\n- `getAgentDocument(event, path, { sections })` replaces the fetch-and-parse, and returns the exact bytes the raw route serves.\n- `listAgentPages(event, { search, prefix })` replaces the hand-written page listing.\n- `extractSections(markdown, titles)` replaces the section slicer.\n- `getAgentSiteUrl(event)` replaces every hardcoded site URL.\n\nThe tools themselves stay in the site. Names, descriptions and schemas are prompt engineering, and the module ships none.\n\nTwo wrinkles. A tool that deliberately serves excluded content, say a nightly docs version the site's `excludePrefixes` hides, needs `includeExcluded: true` on both `getAgentDocument()` and `listAgentPages()`, since both skip excluded prefixes by default. And a listing over a dimension the adapter does not model, versions for instance, that must include excluded pages can keep its `queryCollection` for the listing while still reading every document through `getAgentDocument()`.\n\n## Step 7: delete the old layer\n\n| Delete | Replaced by |\n|---|---|\n| `modules\u002Fmd-rewrite.ts`, `vercel.json` negotiation routes | the vercel preset in the module |\n| `server\u002Futils\u002FmarkdownNegotiation.ts` and friends | the negotiation core |\n| `server\u002Fmiddleware\u002Fmarkdown.ts` | the module's middleware |\n| `server\u002Ferror.ts` plus the `nitro:config` errorHandler chaining in `nuxt.config` | `errors: true` |\n| `server\u002Froutes\u002Fraw\u002F[...slug].md.get.ts` | the module's raw route |\n| `server\u002Froutes\u002Fraw\u002Findex.md.get.ts` | a `\u002F` document in the content source, or `agent-discovery:index` |\n| `server\u002Froutes\u002F.well-known\u002Fapi-catalog.get.ts`, `.well-known\u002Fmcp\u002Fserver-card.json.get.ts` | `discovery.apiCatalog`, `discovery.mcpServerCard` |\n| `server\u002Froutes\u002Fsitemap.md.get.ts` | `sitemap.markdown` |\n| `public\u002Frobots.txt` with its `Disallow` lines, hand-maintained agent lists | `robots.aiPolicy` and `robots.disallow` |\n| `skills\u002Findex.json`, `publicAssets` entries for skills | `skills.dir` |\n| `app\u002Fcomposables\u002FuseCanonical.ts` | the module's composable |\n| `routeRules` entries setting `Vary` or the discovery `Link` | emitted by the module |\n| llms link-rewriting helpers | the llms bridge |\n| the markdown pipeline under MCP tools (`server\u002Futils\u002Fmcp.ts` and friends) | `getAgentDocument()`, `listAgentPages()`, `extractSections()`, `getAgentSiteUrl()` |\n\nTwo that are easy to miss: the `Vary`\u002F`Link` `routeRules` block, which now conflicts with what the module emits, and the `nitro:config` hook chaining the error handler, which the module does itself.\n\nKeep a hand-written `\u002Fraw\u002Findex.md` handler only when the site wants full control of that document. Otherwise prefer a `\u002F` document in the content source, which the module wraps with the resources block and the sitemap footer, and keep the `agent-discovery:index` hook for metadata when no `\u002F` document exists. On an i18n site the landing documents live at the locale roots, and the module wraps `content\u002Fen\u002Findex.md` and its siblings the same way; the hook only ever fills the generated `\u002F`.\n\n## Adopting `@nuxtjs\u002Fsitemap` in the same PR\n\nA migration often replaces a hand-rolled `sitemap.xml` route at the same time. The raw twins are dropped from the sitemap by this module's exclude either way, but two `@nuxtjs\u002Fsitemap` traps bite hard on exactly the sites this skill targets:\n\n- **Never set `routeRules['\u002Fsitemap.xml'] = { prerender: true }`.** During prerender `@nuxtjs\u002Fsitemap` resolves its own route against the canonical site URL and ingests the live production sitemap, so every deploy ships a copy of the previous one. The proof on a bitten site: the build artifact is a strict subset of production's entries, old changefreq values included.\n- **The `nuxt:prerender` app source lists every prerendered page.** On a versioned docs site that means legacy and nightly versions and unversioned redirect stubs all land in the sitemap. Set `excludeAppSources: true`, point `sitemap.sources` at a small server route querying the content collections, and list the handful of Vue-only pages in `urls`. Content stays the single source of truth.\n\n## Step 8: verify\n\nLocally first:\n\n```sh\npnpm dev\ncurl -sS -D - -H 'Accept: text\u002Fmarkdown' http:\u002F\u002Flocalhost:3000\u002Fdocs\u002F... | head -20\ncurl -sS -D - -A ClaudeBot http:\u002F\u002Flocalhost:3000\u002F | head -20\ncurl -sS http:\u002F\u002Flocalhost:3000\u002Fsitemap.md | head\ncurl -sS http:\u002F\u002Flocalhost:3000\u002F.well-known\u002Fapi-catalog | jq .\ncurl -sS -A ClaudeBot http:\u002F\u002Flocalhost:3000\u002Fnope | head\n```\n\nThen build, and read the log rather than skimming it. The module logs at info when a cached route rule covers a negotiated pattern, so grep for `response cache` rather than for warnings. It warns when no content source resolves, when a static `robots.txt` shadows the generated one, and when `extend` and `replace` are both set, and it throws on an invented link rel or a `siteUrl` carrying a path. Compare the prerendered route count against the old build.\n\nStale state fakes results at every stage here, three ways. Nuxt's build cache can keep serving a server route whose file was deleted: clear `node_modules\u002F.cache\u002Fnuxt` after deleting route files, and remember a CDN's build cache restores it on preview deploys. A built server persists cached functions under `.data` between boots: wipe it, knowing a `@nuxthub` site needs `.data\u002Fdb` back before it boots. And before trusting any curl against a locally booted `.output\u002Fserver`, check the PID actually listening on the port (`lsof -iTCP:\u003Cport>`): a zombie server from a previous build answers every probe and never sees an on-disk edit.\n\nThen deploy a preview and run the `verify-nuxt-agent-discovery` skill against it. Local checks cannot exercise `Vary`, the `Link` header or the CDN rewrites, which is where the interesting failures live.\n\n## Gotchas\n\n- **Never register `\u002Fllms.txt` or `\u002Fllms-full.txt`.** The module refuses to, and a site that adds its own collides with `nuxt-llms`.\n- `@nuxt\u002Fcontent` installs its llms feature, and therefore `\u002Fraw\u002F**:slug.md`, only when `nuxt-llms` is present. The module sets `llms.contentRawMarkdown: false` and takes over both the route and the link rewriting, so the raw route survives a later backend swap.\n- On a cached page the module redirects instead of rewriting, and disables request-time negotiation in production. Dev keeps negotiating.\n- Off Vercel, Nitro serves prerendered files ahead of user handlers, so a prerendered page stays HTML for agents. Either render those routes on demand or accept that agents reach markdown through the `.md` URL.\n- Paths whose last segment is dotted never negotiate, which is what keeps `_payload.json` and images out.\n- A markdown document is read detached from its site, so relative links have to be absolutised. The `@nuxt\u002Fcontent` adapter does it; a custom one has to call `absolutizeMarkdownLinks()`.\n\n## Deliverable\n\nOne PR per site. The description should say what was deleted, what became configuration, what stayed behind a hook, and which behaviour changed for agents (usually: more user agents honoured, `Vary` now correct, markdown errors, and negotiation working in dev). Link the preview check results.\n",{"name":5,"description":7},"main","6e69c10dc62009e1c26a6c5217742847c55e185a","skills\u002Fmigrate-to-nuxt-agent-discovery",2,"https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery","https:\u002F\u002Fnuxt-agent-discovery.vercel.app","https:\u002F\u002Fgithub.com\u002Fbenjamincanac","Software Engineer @vercel •\r\nWorking on @nuxt ecosystem • Author of Nuxt UI","2026-11-11 19:42:14",{"source":8,"skillName":5,"commands":64,"prompt":74,"zipAvailable":75},[65,68,71],{"tool":66,"command":67},"npx","npx skills add benjamincanac\u002Fnuxt-agent-discovery --skill migrate-to-nuxt-agent-discovery",{"tool":69,"command":70},"bunx","bunx skills add benjamincanac\u002Fnuxt-agent-discovery --skill migrate-to-nuxt-agent-discovery",{"tool":72,"command":73},"pnpm","pnpm dlx skills add benjamincanac\u002Fnuxt-agent-discovery --skill migrate-to-nuxt-agent-discovery","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery\u002Fblob\u002Fmain\u002Fskills\u002Fmigrate-to-nuxt-agent-discovery\u002FSKILL.md",true,[],[78,100,114],{"skillNo":79,"name":80,"nameZh":6,"description":81,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":90,"publishedAt":49},"01M71FSDMKFW2AQ6H22H23HMWM","writing","Fixture agent skill for the module unit tests, which pin the prerender list.","https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery\u002Fblob\u002Fmain\u002Ftest\u002Ffixtures\u002Funit-skills\u002Fskills\u002Fwriting\u002FSKILL.md",[84,87],{"categoryNo":85,"categoryName":86,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":88,"categoryName":89,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000036","内容创作者",[80,91,92,93,43,94,95,96,97,98,99],"unit","fixture","tests","which","list","agent skill","pin","for","unit tests",{"skillNo":101,"name":102,"nameZh":6,"description":103,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":104,"categories":105,"keywords":106,"publishedAt":49},"01M71FSC4D3DG0TPVX9ASRNDG7","basic-site","Fixture agent skill used to test the skills catalog and file serving.","https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery\u002Fblob\u002Fmain\u002Ftest\u002Ffixtures\u002Fbasic\u002Fskills\u002Fbasic-site\u002FSKILL.md",[],[107,108,109,110,111,96,112,45,92,113],"basic","basic site","site","serving","skills catalog","file","test",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":121,"publishedAt":49},"01M71FS9436X5WSRAYCY75X1EC","verify-nuxt-agent-discovery","Verify a deployed Nuxt site running the nuxt-agent-discovery module, from a URL alone: markdown content negotiation, Vary and Link headers, the raw markdown route, discovery documents, the llms.txt bridge, agent error bodies and a full page sweep. Use after migrating a site to the module, when asked to test a preview deployment, or to check markdown negotiation on a deployed site.","https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery\u002Fblob\u002Fmain\u002Fskills\u002Fverify-nuxt-agent-discovery\u002FSKILL.md",[120],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[29,30,122,123,33,124,125,126,127,128,129,130,40,131,41,43,132,133,134,135],"verify","verify nuxt agent discovery","running","route","sweep","markdown content","check","preview","URL","documents","deployment","page","bridge","error"]