[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71FS9436X5WSRAYCY75X1EC":3,"skill-same:01M71FS9436X5WSRAYCY75X1EC":72},{"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":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":6,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M71FS9436X5WSRAYCY75X1EC","verify-nuxt-agent-discovery",null,"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.","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\u002Fverify-nuxt-agent-discovery\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"verify nuxt agent discovery","discovery","verify","nuxt","agent discovery","use","running","when","URL","error","module","preview","route","documents","bridge","check","markdown content","sweep","page","deployment","2026-11-10 10:12:30",false,"---\nname: verify-nuxt-agent-discovery\ndescription: \"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.\"\n---\n\n# nuxt-agent-discovery deployment check\n\nCheck a deployed site running [`nuxt-agent-discovery`](https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery), usually a preview deployment of the PR that adopts or bumps the module.\n\n**Everything here runs against the URL.** The deployment is the subject: no repo, no local build, no config file is required. The site's shape is discovered by probing it. Where a repo happens to be at hand, use it to confirm a finding, never as the source of truth, since what shipped is what matters.\n\n`Vary`, `Link` and the CDN rewrites only exercise properly through a real edge, which is why this is not a local test.\n\nReport findings, don't fix anything.\n\n## Inputs\n\nThe URL comes from the skill argument or the conversation. Ask when it is missing.\n\n```sh\nPREVIEW=            # the deployment under test\n```\n\nShell setup:\n\n```sh\nBOT='ClaudeBot\u002F1.0 (+https:\u002F\u002Fanthropic.com\u002Fclaudebot)'\nGPT='Mozilla\u002F5.0 AppleWebKit\u002F537.36 (KHTML, like Gecko); compatible; GPTBot\u002F1.2; +https:\u002F\u002Fopenai.com\u002Fgptbot'\nBROWSER='Mozilla\u002F5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F133.0.0.0 Safari\u002F537.36'\nHTML='text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8'\n\nprobe() { curl -sS -D - -o \u002Fdev\u002Fnull \"$@\"; }      # headers only\nshow()  { curl -sS -D - \"$@\" | head -60; }        # headers plus the top of the body\n```\n\n## Ground rules\n\n- Never pass `-L`. Redirects are part of what's under test.\n- Absolute URLs inside markdown bodies point at the site's configured `siteUrl`, which is production, not the preview host. That is by design and it is also how Step 1 finds the production URL.\n- If the deployment answers 401 with a Vercel SSO page, ask for a protection bypass token and send it as `-H \"x-vercel-protection-bypass: \u003Ctoken>\"`, or stop and say so.\n- The CDN caches. When a result looks stale, re-run with a `?cb=1` query and note whether behaviour changed.\n- When the subject is a locally booted server rather than a deployment, confirm which process owns the port before the first curl (`lsof -iTCP:\u003Cport> -sTCP:LISTEN`), and confirm again after every rebuild. A zombie server from a previous build answers every probe and never sees an on-disk edit. The tells: byte-identical responses across config changes, sentinel edits that never appear, routes that exist in the build answering 404.\n- Every failure gets a copy-pasteable curl repro and a call on whether it belongs to the site or to the module.\n\n## Step 1: profile the deployment\n\nEverything the later steps need comes out of the deployment itself.\n\n```sh\nprobe \"$PREVIEW\u002F\"                                        # host, headers, Link\ncurl -sS \"$PREVIEW\u002Fllms.txt\" | head -30\ncurl -sS \"$PREVIEW\u002Fsitemap.md\" | head -30\ncurl -sS \"$PREVIEW\u002F.well-known\u002Fapi-catalog\" | jq .\n```\n\nEstablish and write down:\n\n- **Host.** `x-vercel-id` means Vercel, where negotiation happens at the edge from the Build Output route table and everything in this skill applies. Anything else means only the Nitro middleware runs, and Nitro serves prerendered files ahead of user handlers, so **a prerendered page keeps returning HTML to an agent**. That is a documented limitation, not a regression: expect negotiation on never-prerendered pages and on explicit `.md` URLs, and say so in the report.\n- **The raw prefix.** Read it off the links in `sitemap.md`, which point at raw twins. `\u002Fraw` is the default, and every command below uses `$RAW` rather than assuming it:\n\n```sh\n# Taken from the homepage twin, which is always `\u003Cprefix>\u002Findex.md`. Inferring\n# a common prefix instead collapses on a site whose pages all share a section:\n# `\u002Fraw\u002Fdocs\u002Fa.md` and `\u002Fraw\u002Fdocs\u002Fb.md` would give `\u002Fraw\u002Fdocs`.\nRAW=$(curl -sS \"$PREVIEW\u002Fsitemap.md\" | grep -oE 'https?:\u002F\u002F[^)]+\u002Findex\\.md' | head -1 | sed -e 's|.*:\u002F\u002F[^\u002F]*||' -e 's|\u002Findex\\.md$||')\nRAW=${RAW:-\u002Fraw}\necho \"$RAW\"\n```\n- **The production URL**, baked into every prerendered document by `siteUrl`:\n\n```sh\nPROD=$(curl -sS \"$PREVIEW$RAW\u002Findex.md\" | grep -oE 'https?:\u002F\u002F[^\u002F)]+' | sort | uniq -c | sort -rn | head -1 | awk '{print $2}')\necho \"$PROD\"\n```\n\n  Sanity-check it resolves to the live site. If it comes back as the preview host, the site resolves `siteUrl` per request rather than baking it, which is worth noting but not a fault.\n- **The page inventory**, from `sitemap.xml`, falling back to `sitemap.md` or `llms.txt`:\n\n```sh\ncurl -sS \"$PREVIEW\u002Fsitemap.xml\" | grep -o '\u003Cloc>[^\u003C]*' | sed -E 's|\u003Cloc>||; s|https?:\u002F\u002F[^\u002F]+||' | sort -u > \u002Ftmp\u002Fpages\nwc -l \u002Ftmp\u002Fpages\n```\n\n- **Which documents exist**: `\u002Fsitemap.md`, `\u002Frobots.txt`, `\u002Fllms.txt`, `\u002Fllms-full.txt`, `\u002F.well-known\u002Fapi-catalog`, `\u002F.well-known\u002Fmcp\u002Fserver-card.json`, `\u002F.well-known\u002Fskills\u002Findex.json`, any OpenAPI document linked from the api-catalog. A 404 on one of these means the feature is off, not that it is broken. Check the api-catalog and the `Link` header on `\u002F` to see what the site claims to serve, then verify each claim.\n\nThen pick concrete URLs from the inventory:\n\n```sh\nHOME=\u002F\nDOC=            # a deep documentation page\nSECTION=        # a directory path with no page of its own, e.g. the parent of $DOC\nUNCOVERED=      # a page in the inventory that does NOT negotiate, found in Step 2\n```\n\nNegotiation config is not readable over HTTP, so infer it: a page that answers markdown at its own URL sits under a rewrite, a page that 307s to its raw twin sits under a cached route rule (`isr`, `swr`, `cache`), and a page that stays HTML to an agent is not covered by any `routes` pattern. Step 3's sweep classifies every page this way, which is more reliable than reading the config anyway.\n\n## Step 2: negotiation matrix\n\nRun every row against `$HOME` and `$DOC`, plus one page from each class the sweep finds.\n\n| Request | Expected |\n|---|---|\n| `probe -A \"$BROWSER\" -H \"Accept: $HTML\" \"$PREVIEW$DOC\"` | 200, `content-type: text\u002Fhtml`, `vary` contains `Accept` and `User-Agent` |\n| `show -H 'Accept: text\u002Fmarkdown' \"$PREVIEW$DOC\"` | markdown body, `content-type: text\u002Fmarkdown`, 200 at the same URL (307 to the raw twin on a cached pattern, HTML on a non-Vercel host when the page is prerendered) |\n| `show -A \"$BOT\" \"$PREVIEW$DOC\"` | same markdown, no `Accept` header needed |\n| `probe -A \"$GPT\" \"$PREVIEW$DOC\"` | same as ClaudeBot |\n| `show \"$PREVIEW$DOC.md\"` | markdown, 200, a rewrite even on a cached pattern, `vary` contains `Accept` and `User-Agent` |\n| `probe \"$PREVIEW$RAW${DOC}.md\"` | markdown, 200, same `vary`. This is where a 307 lands, so it is the response a shared cache stores |\n| `probe \"$PREVIEW\u002Fsitemap.md\"` | markdown, 200, same `vary` |\n| `probe -H 'Accept: text\u002Fmarkdown;q=0.5, text\u002Fhtml;q=0.9' \"$PREVIEW$DOC\"` | HTML **on an uncached pattern whose twin is not prerendered**, which is the only deterministic case: pick that `$DOC`. A CDN matcher cannot rank q-values, so a prerendered twin answers markdown at the edge and a cached pattern answers 307, both before the origin can rank anything. Documented in the README, not a bug |\n| `probe -H 'Accept: text\u002Fmarkdown;q=0.9, text\u002Fhtml;q=0.5' \"$PREVIEW$DOC\"` | markdown |\n| `probe -H 'Accept: text\u002Fmarkdown;q=0' \"$PREVIEW$DOC\"` | HTML |\n| `probe -H 'Accept: *\u002F*' \"$PREVIEW$DOC\"` | HTML, a wildcard never counts as asking |\n| `probe -H 'Accept: text\u002Fplain' \"$PREVIEW$DOC\"` | HTML, only `text\u002Fmarkdown` counts |\n| `probe -H 'Accept: application\u002Fxml' \"$PREVIEW$DOC\"` | HTML, unless the site set `notAcceptable: true`, which makes it a 406. Check the site's config before calling either one a failure |\n| `probe -A \"$BOT\" \"$PREVIEW$UNCOVERED\"` | HTML, an unconfigured page must not start negotiating |\n| `probe -A \"$BOT\" \"$PREVIEW$DOC?ref=x\"` | markdown, and if it is a 307 the `location` keeps `?ref=x` |\n| `probe -A \"$BOT\" -I \"$PREVIEW$DOC\"` | same status and headers as the GET. `-I`, not `-X HEAD`, which makes curl wait for a body that never comes |\n\nTwo things that fail quietly, so check them explicitly:\n\n- **`Vary: Accept, User-Agent` is present on the markdown responses too**, including the ones served straight off the CDN rewrite and the prerendered `\u002Fsitemap.md` and `\u002Fraw\u002Findex.md`. This is the `continue: true` fix most hand-rolled implementations get wrong. A markdown response with no `Vary` poisons the shared cache for browsers, and a checker following a 307 to the raw twin sees a URL with two representations behind it and no header saying so.\n- **The three markdown representations are byte-identical.**\n\n```sh\ncurl -sS -H 'Accept: text\u002Fmarkdown' \"$PREVIEW$DOC\" > \u002Ftmp\u002Fa.md\ncurl -sS \"$PREVIEW$DOC.md\" > \u002Ftmp\u002Fb.md\ncurl -sS \"$PREVIEW$RAW${DOC}.md\" > \u002Ftmp\u002Fc.md\ndiff \u002Ftmp\u002Fa.md \u002Ftmp\u002Fb.md && diff \u002Ftmp\u002Fb.md \u002Ftmp\u002Fc.md\n```\n\n## Step 3: sweep every page\n\nThe sample above proves the mechanism. This proves the coverage, and it is the only way to catch a page that failed to prerender, a subtree a `routes` pattern misses, or a document the adapter cannot stringify. Run it over the whole inventory, not a sample.\n\n```sh\ncat > \u002Ftmp\u002Fsweep.sh \u003C\u003C'EOF'\n#!\u002Fbin\u002Fsh\np=\"$1\"\nagent=$(curl -sS -o \u002Fdev\u002Fnull -w '%{http_code} %{content_type} %{redirect_url}' -A \"$BOT\" \"$PREVIEW$p\")\nhtml=$(curl -sS -o \u002Fdev\u002Fnull -w '%{http_code} %{content_type}' -A \"$BROWSER\" -H \"Accept: $HTML\" \"$PREVIEW$p\")\ntwin=$([ \"$p\" = \"\u002F\" ] && echo skip || curl -sS -o \u002Fdev\u002Fnull -w '%{http_code} %{content_type}' \"$PREVIEW${p%\u002F}.md\")\nraw=$(curl -sS -o \u002Fdev\u002Fnull -w '%{http_code}' \"$PREVIEW$RAW$([ \"$p\" = \"\u002F\" ] && echo \u002Findex || echo \"${p%\u002F}\").md\")\necho \"$p | agent: $agent | html: $html | twin: $twin | raw: $raw\"\nEOF\nchmod +x \u002Ftmp\u002Fsweep.sh\nexport PREVIEW BOT BROWSER HTML\nxargs -P 8 -n 1 \u002Ftmp\u002Fsweep.sh \u003C \u002Ftmp\u002Fpages > \u002Ftmp\u002Fsweep.txt\n```\n\nThen classify. Every line should fall into one of two shapes:\n\n- **negotiated**: agent `200 text\u002Fmarkdown`, or `307` with a `redirect_url` at the raw twin\n- **not negotiated**: agent `200 text\u002Fhtml`, and the page is genuinely outside the `routes` patterns\n\nEverything else is a finding:\n\n```sh\ngrep -v 'text\u002Fmarkdown' \u002Ftmp\u002Fsweep.txt | grep -v ' 307 '     # pages serving HTML to an agent\ngrep -v 'html: 200 text\u002Fhtml' \u002Ftmp\u002Fsweep.txt                  # pages broken for browsers\ngrep -v 'twin: 200 text\u002Fmarkdown\\|twin: skip' \u002Ftmp\u002Fsweep.txt  # .md twins that 404\ngrep -v 'raw: 200' \u002Ftmp\u002Fsweep.txt                             # raw route holes\nawk '{print $NF}' \u002Ftmp\u002Fsweep.txt | sort | uniq -c              # raw status distribution\n```\n\nA page that is HTML-only to agents is a finding when its neighbours negotiate, and expected when the site deliberately leaves that section out. Group them by prefix before deciding, and use one of them as `$UNCOVERED`.\n\nAlso sweep for empty or truncated documents, which return 200 and look fine:\n\n```sh\nwhile read -r p; do\n  n=$(curl -sS \"$PREVIEW$RAW$([ \"$p\" = \"\u002F\" ] && echo \u002Findex || echo \"${p%\u002F}\").md\" | wc -c)\n  [ \"$n\" -lt 200 ] && echo \"THIN $n $p\"\ndone \u003C \u002Ftmp\u002Fpages\n```\n\n## Step 4: the raw route\n\n- `show \"$PREVIEW$RAW${DOC}.md\"` returns 200, `text\u002Fmarkdown`, and a `Link` header with both `rel=\"canonical\"` and `rel=\"alternate\"; type=\"text\u002Fhtml\"` pointing at the page URL.\n- `probe \"$PREVIEW$RAW${SECTION}.md\"` returns 302 to the first document under that section, not a 404.\n- `show \"$PREVIEW$RAW\u002Fdoes-not-exist.md\"` returns 404 with a markdown body.\n- `curl -sS \"$PREVIEW$RAW\u002Findex.md\" | head -30` returns the homepage document, or the generated landing page: frontmatter, canonical and alternate links, and a resources block.\n- Site-relative links are absolutised. Every `](\u002F` outside a fenced block is a bug, across the whole inventory:\n\n```sh\nwhile read -r p; do\n  curl -sS \"$PREVIEW$RAW$([ \"$p\" = \"\u002F\" ] && echo \u002Findex || echo \"${p%\u002F}\").md\" \\\n    | awk '\u002F^ {0,3}(```|~~~)\u002F{f=!f} !f' | grep -q '](\u002F' && echo \"RELATIVE $p\"\ndone \u003C \u002Ftmp\u002Fpages\n```\n\n  Read each match in context before reporting it. The scan skips fenced blocks but not inline code spans, so a page writing `](\u002Fx)` inside backticks is a false positive, and the module leaves those alone on purpose. Sites still running a hand-rolled layer do carry relative links here, so this check is expected to differ between production and the preview.\n\n- Fenced blocks survived. Spot-check a page documenting markdown or MDC and confirm nothing inside a fence was rewritten.\n- Markdown structure survived the stringifier: pick five pages with tables, callouts, nested lists, code groups and frontmatter, and read them. A component that stringified to nothing leaves a 200 with a hole in it.\n\n## Step 5: discovery documents\n\n```sh\nshow \"$PREVIEW\u002F\"                               # Link header lives on \u002F only\ncurl -sS \"$PREVIEW\u002F.well-known\u002Fapi-catalog\" | jq .\ncurl -sS \"$PREVIEW\u002F.well-known\u002Fmcp\u002Fserver-card.json\" | jq .\ncurl -sS \"$PREVIEW\u002Fsitemap.md\" | head -40\ncurl -sS \"$PREVIEW\u002F.well-known\u002Fskills\u002Findex.json\" | jq .\ncurl -sS \"$PREVIEW\u002Frobots.txt\"\n```\n\n- The `Link` header on `\u002F` carries only IANA-registered rels. Any `rel=\"llms\"`, `rel=\"mcp\"`, `rel=\"design\"` or other invented relation means something outside the module is emitting headers, since the module rejects them at build time.\n- Every href in the `Link` header and the api-catalog resolves. Fetch each one and record the status.\n- `api-catalog` answers `application\u002Flinkset+json`, parses, and every `anchor` and `href` is absolute.\n- The server card carries `serverInfo.name` and an `endpoints` array (`endpoint` and `name` are the config keys, not the served fields), and lists live tools where the site runs `@nuxtjs\u002Fmcp-toolkit`. The endpoint it names answers: POST an MCP `initialize` to it. There is no `$schema`: the MCP spec has no server card yet.\n- `sitemap.md` links point at a raw twin for every page a `routes` pattern covers. A page outside those patterns correctly links to its HTML URL, and a path under `excludePrefixes` is left out entirely, so compare against the negotiated set rather than against `sitemap.xml`.\n- The skills index lists skills, and every file it names fetches 200:\n\n```sh\n# Fail loudly when the index itself is missing: an empty pipeline below would\n# otherwise report a clean pass on a site that serves no index at all.\nif ! curl -fsS \"$PREVIEW\u002F.well-known\u002Fskills\u002Findex.json\" > \u002Ftmp\u002Fskills.json; then\n  echo 'FAIL: no skills index'   # a finding whenever the site ships skills\nelse\n  jq -r '.skills[] | .name as $n | .files[] | \"\\($n)\u002F\\(.)\"' \u002Ftmp\u002Fskills.json \\\n    | while read -r p; do printf '%s %s\\n' \"$(curl -sS -o \u002Fdev\u002Fnull -w '%{http_code}' \"$PREVIEW\u002F.well-known\u002Fskills\u002F$p\")\" \"$p\"; done\nfi\n```\n\n- `robots.txt` names the agents negotiation actually matches. Cross-check it: every user agent named there gets markdown on `$DOC`, and the list is not shorter than the 18 defaults unless the site replaced it.\n\n## Step 6: the nuxt-llms bridge\n\n`\u002Fllms.txt` and `\u002Fllms-full.txt` stay owned by `nuxt-llms`. The module only feeds them.\n\n```sh\ncurl -sS -D - -o \u002Ftmp\u002Fllms.txt \"$PREVIEW\u002Fllms.txt\" | head -5\ncurl -sS -o \u002Ftmp\u002Fllms-full.txt \"$PREVIEW\u002Fllms-full.txt\"\nwc -c \u002Ftmp\u002Fllms.txt \u002Ftmp\u002Fllms-full.txt\ngrep -oE '\\]\\([^)]+\\)' \u002Ftmp\u002Fllms.txt | grep -v '\\.md)' | head       # same-origin links that missed the raw rewrite\n```\n\n- Both answer 200 as plain text at their existing paths.\n- Every same-origin link in `llms.txt` points at its raw twin, hand-written links included, and every one of them resolves.\n- The link set matches the inventory. A section that lost its links, or a page that fell out, is a finding:\n\n```sh\n# The homepage is `\u002F` in the sitemap and `\u002Findex` in a raw twin, so both sides\n# are folded to `\u002F` before the diff. Without that a correct site reports one.\nhome() { sed -e 's|\\(.\\)\u002F$|\\1|' -e 's|^\u002Findex$|\u002F|' -e 's|^$|\u002F|' | sort -u; }\ngrep -oE 'https?:\u002F\u002F[^)]+\\.md' \u002Ftmp\u002Fllms.txt | sed \"s|.*$RAW||;s|\\.md$||\" | home > \u002Ftmp\u002Fllms-pages\ndiff \u003C(home \u003C \u002Ftmp\u002Fpages) \u002Ftmp\u002Fllms-pages | head -40\n```\n\n- A page's body inside `llms-full.txt` is byte-identical to the **body** of `\u002Fraw\u002F\u003Cpage>.md`, which is that document minus its frontmatter block and its `## Sitemap` footer. They come from one pipeline now, so any difference beyond that envelope is a bug. Check three pages from different sections.\n\n## Step 7: error bodies\n\n| Request | Expected |\n|---|---|\n| `show -A \"$BOT\" \"$PREVIEW\u002Fnope\"` | 404, `text\u002Fmarkdown`, frontmatter with `title` and `status`, recovery links absolute and resolving |\n| `probe -A \"$BROWSER\" -H \"Accept: $HTML\" \"$PREVIEW\u002Fnope\"` | 404 HTML error page |\n| `probe -H 'Accept: *\u002F*' -H 'Sec-Fetch-Mode: cors' \"$PREVIEW\u002Fnope\"` | not markdown, a browser `fetch()` keeps what it was written against |\n| `probe -H 'Accept: application\u002Fjson' \"$PREVIEW\u002Fapi\u002Fdoes-not-exist\"` | JSON error, never markdown |\n| `probe -A \"$BOT\" \"$PREVIEW\u002F_nuxt\u002Fnope.js\"` | not markdown |\n| `probe -A \"$BOT\" \"$PREVIEW\u002Fnope.png\"` | not markdown, a dotted path is an asset |\n| `curl -sS \"$PREVIEW\u002Fnope\"` | markdown, plain curl with no `Accept` counts as an agent |\n| `probe -A \"$BOT\" \"$PREVIEW\u002Fnope\u002Fdeeper\u002Fstill\"` | 404 markdown, not a redirect loop |\n\nThe 404 must be a real 404. A 200 carrying the app shell tells every agent that every path exists.\n\n## Step 8: no collateral damage\n\n- `\u002Fsitemap.xml` is XML, resolves, and does not list a single raw URL: `curl -sS \"$PREVIEW\u002Fsitemap.xml\" | grep -c '\u002Fraw\u002F'` must print 0.\n- On a site running `@nuxtjs\u002Fsitemap`, prove the sitemap was built from the deployment's own data: fetch `$PREVIEW\u002Fsitemap.xml` and `$PROD\u002Fsitemap.xml` and compare entries. A preview sitemap that is a copy or strict subset of production's, same `lastmod` shape, entries the preview's own content cannot produce, means the prerender ingested the live production sitemap (a `prerender: true` route rule on `\u002Fsitemap.xml` does this) and the deploy ships stale SEO data. Also assert no prerendered legacy-version pages or redirect stubs leaked in through app sources.\n- Assets are untouched: a favicon, an og-image, a `_payload.json`, anything dotted under a negotiated pattern.\n- The site's own endpoints answer. Enumerate them from the HTML the app ships (`grep -o '\u002Fapi\u002F[a-z0-9.\u002F-]*'` over a rendered page) and from the api-catalog, then fetch each one.\n- Any OpenAPI document parses and its `paths` are populated:\n\n```sh\ncurl -sS \"$PREVIEW\u002Fopenapi.json\" | jq -r '.paths | keys[]' | head -40\n```\n\n- A doc page's head still carries its canonical, alternate, og and JSON-LD tags. `useCanonical()` moving from a site file into the module is exactly how a trailing slash, a locale alternate or an og tag goes missing:\n\n```sh\ncurl -sS -A \"$BROWSER\" -H \"Accept: $HTML\" \"$PREVIEW$DOC\" \\\n  | grep -oE '\u003C(link|meta|script)[^>]*(canonical|alternate|og:|ld\\+json)[^>]*>'\n```\n\n- The rendered HTML carries real content, not just an app shell: an `\u003Ch1>` and a few hundred characters of text before hydration.\n- If a browser tool is available (Playwright, a devtools MCP), load three pages and check for console errors and hydration mismatches. Say so in the report when no browser was available.\n\n## Step 9: production diff\n\n`$PROD` came out of Step 1. Skip this when it resolves to the same deployment, or when production already runs the same module version.\n\nWhen production still runs a prior implementation, this is the strongest check available. Diff a sample of at least ten pages spread across sections, rewriting hosts so only content differences remain.\n\n```sh\nfor p in $(awk 'NR % 18 == 1' \u002Ftmp\u002Fpages); do        # every 18th page, spread across the inventory\n  curl -sS \"$PROD\u002Fraw${p%\u002F}.md\"    | sed \"s|$PROD||g\" > \u002Ftmp\u002Fprod.md\n  curl -sS \"$PREVIEW$RAW${p%\u002F}.md\" | sed \"s|$PREVIEW||g;s|$PROD||g\" > \u002Ftmp\u002Fprev.md\n  echo \"== $p\"; diff \u002Ftmp\u002Fprod.md \u002Ftmp\u002Fprev.md || true\ndone\n```\n\nDo the same for `\u002Fllms.txt`, `\u002Fsitemap.md`, `\u002Frobots.txt`, `\u002F.well-known\u002Fapi-catalog`, the head tags from Step 8, and the page inventory:\n\n```sh\ncurl -sS \"$PROD\u002Fsitemap.xml\" | grep -o '\u003Cloc>[^\u003C]*' | sed -E 's|\u003Cloc>||; s|https?:\u002F\u002F[^\u002F]+||' | sort -u > \u002Ftmp\u002Fprod-pages\ndiff \u002Ftmp\u002Fprod-pages \u002Ftmp\u002Fpages | head -40\ncomm -23 \u003C(curl -sS \"$PROD\u002Fopenapi.json\" | jq -r '.paths|keys[]' | sort) \\\n         \u003C(curl -sS \"$PREVIEW\u002Fopenapi.json\" | jq -r '.paths|keys[]' | sort)\n```\n\nDifferences worth accepting: prose the PR intentionally changed, links that were relative before and are absolute now, and MDC output the adapter renders differently on purpose. Everything else is a regression: a dropped page, a lost frontmatter field, a mangled fence, a table rendered as prose, a code block that lost its language, a component that stringified to nothing, an API path that disappeared.\n\n## Step 10: is-agentic score\n\n[`is-agentic`](https:\u002F\u002Fgithub.com\u002Fvercel-labs\u002Fis-agentic) (Vercel Labs) scores agent readiness.\n\n```sh\nnpx -y is-agentic \"$PREVIEW\" --json > \u002Ftmp\u002Fis-agentic-preview.json\nnpx -y is-agentic \"$PROD\"\n```\n\nThe preview score must be at least production's, and no check may move from pass to fail. Report the delta and the named checks that changed.\n\nCaveats to state rather than work around:\n\n- It is a hosted scanner. The URL is submitted to `is-agentic.com`, which keeps the report and can serve it to anyone asking for that domain. Ask before scanning a preview of unreleased work.\n- It probes with browser-ish headers, so a check can come back partial on a site that handles agents correctly. Its \"Agent-friendly 404s\" check reads a browser 404, not the markdown body an agent gets. Reproduce a partial by hand before reporting it.\n- A \"crawler unknown\" or \"blocked\" verdict for a user agent that demonstrably gets 200 markdown via curl means the scanner's own egress was challenged by the platform, a Vercel or WAF setting rather than site code. Reproduce with the exact UA before reporting it.\n- The brand-discoverability check is meaningless against a `*.vercel.app` preview URL. Score it only against production.\n\n## Optional: the build output table\n\nOnly when the repo is at hand and something in Step 2 or 3 looked structurally wrong. Build with the vercel preset and read `.vercel\u002Foutput\u002Fconfig.json`:\n\n```sh\njq '.routes | length' .vercel\u002Foutput\u002Fconfig.json\njq '.routes[0:6]' .vercel\u002Foutput\u002Fconfig.json\n```\n\n- The first route sets `Vary: Accept, User-Agent` on the pages with `continue: true`, ahead of anything Nitro emits from `routeRules`. The second sets it on the markdown representations: the raw prefix, the `.md` twins and `\u002Fsitemap.md`. The `Link` route on `\u002F` carries `continue: true` too.\n- Two negotiated routes per configured pattern, one matching `Accept`, one matching the agent user agents. Prerendered patterns rewrite with `check: true`, cached patterns 307.\n- Total route count tracks the pattern count, not the page count.\n\n## What this does not cover\n\nSay so in the report rather than implying full coverage:\n\n- Anything behind auth, and anything the CDN serves differently to a real bot IP than to curl.\n- Content or schema changes whose effect shows up only in search indexes, navigation payloads or component metadata that the sweep does not fetch.\n- Visual regressions, and client-side behaviour beyond console errors when a browser tool was available.\n- Performance, cold starts and function count.\n\n## Attributing a finding\n\nEvery finding lands in one of two repos, and the fix is different in each. Attribute before reporting, and say which evidence you used.\n\n**Rule of thumb.** The site owns its configuration, its content and its hooks. The module owns behaviour no configuration can change. So: a whole section that never negotiates is a `routes` pattern, a single page whose table came out mangled is content or an adapter, and a missing `Vary` on a CDN rewrite is the module, because no site config can produce or prevent it.\n\n| Symptom | Likely owner | Confirm by |\n|---|---|---|\n| No `Vary` on a markdown response, or none on `\u002F` | module, `src\u002Fpresets\u002Fvercel.ts` | check the site does not override headers for that path in `routeRules`. Off Vercel a prerendered `\u002Fsitemap.md` or `\u002Fraw\u002Findex.md` is served off disk and carries none, which is the documented caveat, not a bug |\n| `text\u002Fplain` or `q=0` decided wrong, wildcard treated as asking | module, `src\u002Fruntime\u002Fshared\u002Fnegotiation.ts` | a unit test in the module's `test\u002F`, it is pure logic |\n| A whole section serves HTML to agents | site, `routes` patterns | does the pattern actually cover it? if it plainly does, the module's `compilePattern`\u002F`matchRoute` |\n| `.md` twin 404s while `\u002Fraw\u002F**.md` answers | module, `rawDestination` or the preset rewrite | unless an `excludePrefixes` entry on the site swallows that path |\n| A standalone `.md` the site serves itself got rewritten | site, add it to `excludePrefixes` | |\n| Invented `rel` in the `Link` header | site, something outside the module emits it | the module validates rels at build time and throws |\n| A discovery link missing from the api-catalog | site, `discovery.links` | only `service-desc` and `service-doc` entries carrying an `anchor` are grouped, by design |\n| Mangled table, dropped component, lost code language | site content, its MDC transform, or its `agent-discovery:document` hook | reproduce the same construct on the module playground; if it breaks there too it is the stringifier in `src\u002Fruntime\u002Fserver\u002Fsources\u002Fcontent.ts` |\n| Relative links left in a raw document | module, `absolutizeMarkdownLinks` | unless the site ships a custom adapter, which has to call it itself |\n| Section URL 404s instead of redirecting to its first page | module `firstLeaf` handling, or the site's adapter not implementing `firstLeaf` | which source the site configures |\n| Agent gets an HTML 404, or a browser gets markdown | module, `src\u002Fruntime\u002Fserver\u002Ferror.ts` | unless the site chained its own `errorHandler` after the module's |\n| `llms.txt` links not pointing at raw twins | module, the llms bridge plugin | unless the site sets its own `llms:generate` hooks or `contentRawMarkdown` |\n| `robots.txt` short on agents | site, `userAgents.replace` or `@nuxtjs\u002Frobots` config | compare with the module's 18 defaults |\n| `sitemap.xml` listing raw URLs | module, the `@nuxtjs\u002Fsitemap` exclude | unless the site configures its own `sitemap.exclude` |\n| A cached page serving mixed variants | module, cached-route detection | which `routeRules` the site added, and whether the pattern overlaps |\n\n**Three ways to settle a doubtful one:**\n\n1. **Second deployment.** If another site running the same module version shows the same failure, it is the module. If only this deployment shows it, it is the site.\n2. **The playground.** The module repo's `pnpm dev` serves a minimal fixture on `localhost:3000`. Send the same headers. A failure that reproduces there is the module's, on a site whose config you control.\n3. **A failing test.** The negotiation core is pure and unit-tested in the module's `test\u002F`. For any decision bug (`Accept` parsing, pattern matching, exclusions, error preference) write the case as a test rather than arguing from a curl. That test is the deliverable.\n\nNever push a fix to either repo from this skill. Hand back the finding and, for the module, the test that proves it.\n\n## Report\n\nGive, in this order:\n\n1. A one-line verdict: ship, or blocked.\n2. A table of every step with pass, fail or n\u002Fa, and the sweep counts: pages checked, negotiated, HTML-only, twin failures, raw failures.\n3. **Findings for the site PR**, each with: what broke, the curl repro, the config or content line to change, and the evidence for attributing it here.\n4. **Findings for the module**, each with: what broke, the curl repro, the invariant it violates, the file that owns it, and a test case that would fail today. Write these so they paste into the module repo as an issue body without editing.\n5. Differences judged intentional, so the user can confirm the call.\n6. What could not be checked and why.\n","# nuxt-agent-discovery deployment check\n\nCheck a deployed site running [`nuxt-agent-discovery`](https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery), usually a preview deployment of the PR that adopts or bumps the module.\n\n**Everything here runs against the URL.** The deployment is the subject: no repo, no local build, no config file is required. The site's shape is discovered by probing it. Where a repo happens to be at hand, use it to confirm a finding, never as the source of truth, since what shipped is what matters.\n\n`Vary`, `Link` and the CDN rewrites only exercise properly through a real edge, which is why this is not a local test.\n\nReport findings, don't fix anything.\n\n## Inputs\n\nThe URL comes from the skill argument or the conversation. Ask when it is missing.\n\n```sh\nPREVIEW=            # the deployment under test\n```\n\nShell setup:\n\n```sh\nBOT='ClaudeBot\u002F1.0 (+https:\u002F\u002Fanthropic.com\u002Fclaudebot)'\nGPT='Mozilla\u002F5.0 AppleWebKit\u002F537.36 (KHTML, like Gecko); compatible; GPTBot\u002F1.2; +https:\u002F\u002Fopenai.com\u002Fgptbot'\nBROWSER='Mozilla\u002F5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F133.0.0.0 Safari\u002F537.36'\nHTML='text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,image\u002Favif,image\u002Fwebp,*\u002F*;q=0.8'\n\nprobe() { curl -sS -D - -o \u002Fdev\u002Fnull \"$@\"; }      # headers only\nshow()  { curl -sS -D - \"$@\" | head -60; }        # headers plus the top of the body\n```\n\n## Ground rules\n\n- Never pass `-L`. Redirects are part of what's under test.\n- Absolute URLs inside markdown bodies point at the site's configured `siteUrl`, which is production, not the preview host. That is by design and it is also how Step 1 finds the production URL.\n- If the deployment answers 401 with a Vercel SSO page, ask for a protection bypass token and send it as `-H \"x-vercel-protection-bypass: \u003Ctoken>\"`, or stop and say so.\n- The CDN caches. When a result looks stale, re-run with a `?cb=1` query and note whether behaviour changed.\n- When the subject is a locally booted server rather than a deployment, confirm which process owns the port before the first curl (`lsof -iTCP:\u003Cport> -sTCP:LISTEN`), and confirm again after every rebuild. A zombie server from a previous build answers every probe and never sees an on-disk edit. The tells: byte-identical responses across config changes, sentinel edits that never appear, routes that exist in the build answering 404.\n- Every failure gets a copy-pasteable curl repro and a call on whether it belongs to the site or to the module.\n\n## Step 1: profile the deployment\n\nEverything the later steps need comes out of the deployment itself.\n\n```sh\nprobe \"$PREVIEW\u002F\"                                        # host, headers, Link\ncurl -sS \"$PREVIEW\u002Fllms.txt\" | head -30\ncurl -sS \"$PREVIEW\u002Fsitemap.md\" | head -30\ncurl -sS \"$PREVIEW\u002F.well-known\u002Fapi-catalog\" | jq .\n```\n\nEstablish and write down:\n\n- **Host.** `x-vercel-id` means Vercel, where negotiation happens at the edge from the Build Output route table and everything in this skill applies. Anything else means only the Nitro middleware runs, and Nitro serves prerendered files ahead of user handlers, so **a prerendered page keeps returning HTML to an agent**. That is a documented limitation, not a regression: expect negotiation on never-prerendered pages and on explicit `.md` URLs, and say so in the report.\n- **The raw prefix.** Read it off the links in `sitemap.md`, which point at raw twins. `\u002Fraw` is the default, and every command below uses `$RAW` rather than assuming it:\n\n```sh\n# Taken from the homepage twin, which is always `\u003Cprefix>\u002Findex.md`. Inferring\n# a common prefix instead collapses on a site whose pages all share a section:\n# `\u002Fraw\u002Fdocs\u002Fa.md` and `\u002Fraw\u002Fdocs\u002Fb.md` would give `\u002Fraw\u002Fdocs`.\nRAW=$(curl -sS \"$PREVIEW\u002Fsitemap.md\" | grep -oE 'https?:\u002F\u002F[^)]+\u002Findex\\.md' | head -1 | sed -e 's|.*:\u002F\u002F[^\u002F]*||' -e 's|\u002Findex\\.md$||')\nRAW=${RAW:-\u002Fraw}\necho \"$RAW\"\n```\n- **The production URL**, baked into every prerendered document by `siteUrl`:\n\n```sh\nPROD=$(curl -sS \"$PREVIEW$RAW\u002Findex.md\" | grep -oE 'https?:\u002F\u002F[^\u002F)]+' | sort | uniq -c | sort -rn | head -1 | awk '{print $2}')\necho \"$PROD\"\n```\n\n  Sanity-check it resolves to the live site. If it comes back as the preview host, the site resolves `siteUrl` per request rather than baking it, which is worth noting but not a fault.\n- **The page inventory**, from `sitemap.xml`, falling back to `sitemap.md` or `llms.txt`:\n\n```sh\ncurl -sS \"$PREVIEW\u002Fsitemap.xml\" | grep -o '\u003Cloc>[^\u003C]*' | sed -E 's|\u003Cloc>||; s|https?:\u002F\u002F[^\u002F]+||' | sort -u > \u002Ftmp\u002Fpages\nwc -l \u002Ftmp\u002Fpages\n```\n\n- **Which documents exist**: `\u002Fsitemap.md`, `\u002Frobots.txt`, `\u002Fllms.txt`, `\u002Fllms-full.txt`, `\u002F.well-known\u002Fapi-catalog`, `\u002F.well-known\u002Fmcp\u002Fserver-card.json`, `\u002F.well-known\u002Fskills\u002Findex.json`, any OpenAPI document linked from the api-catalog. A 404 on one of these means the feature is off, not that it is broken. Check the api-catalog and the `Link` header on `\u002F` to see what the site claims to serve, then verify each claim.\n\nThen pick concrete URLs from the inventory:\n\n```sh\nHOME=\u002F\nDOC=            # a deep documentation page\nSECTION=        # a directory path with no page of its own, e.g. the parent of $DOC\nUNCOVERED=      # a page in the inventory that does NOT negotiate, found in Step 2\n```\n\nNegotiation config is not readable over HTTP, so infer it: a page that answers markdown at its own URL sits under a rewrite, a page that 307s to its raw twin sits under a cached route rule (`isr`, `swr`, `cache`), and a page that stays HTML to an agent is not covered by any `routes` pattern. Step 3's sweep classifies every page this way, which is more reliable than reading the config anyway.\n\n## Step 2: negotiation matrix\n\nRun every row against `$HOME` and `$DOC`, plus one page from each class the sweep finds.\n\n| Request | Expected |\n|---|---|\n| `probe -A \"$BROWSER\" -H \"Accept: $HTML\" \"$PREVIEW$DOC\"` | 200, `content-type: text\u002Fhtml`, `vary` contains `Accept` and `User-Agent` |\n| `show -H 'Accept: text\u002Fmarkdown' \"$PREVIEW$DOC\"` | markdown body, `content-type: text\u002Fmarkdown`, 200 at the same URL (307 to the raw twin on a cached pattern, HTML on a non-Vercel host when the page is prerendered) |\n| `show -A \"$BOT\" \"$PREVIEW$DOC\"` | same markdown, no `Accept` header needed |\n| `probe -A \"$GPT\" \"$PREVIEW$DOC\"` | same as ClaudeBot |\n| `show \"$PREVIEW$DOC.md\"` | markdown, 200, a rewrite even on a cached pattern, `vary` contains `Accept` and `User-Agent` |\n| `probe \"$PREVIEW$RAW${DOC}.md\"` | markdown, 200, same `vary`. This is where a 307 lands, so it is the response a shared cache stores |\n| `probe \"$PREVIEW\u002Fsitemap.md\"` | markdown, 200, same `vary` |\n| `probe -H 'Accept: text\u002Fmarkdown;q=0.5, text\u002Fhtml;q=0.9' \"$PREVIEW$DOC\"` | HTML **on an uncached pattern whose twin is not prerendered**, which is the only deterministic case: pick that `$DOC`. A CDN matcher cannot rank q-values, so a prerendered twin answers markdown at the edge and a cached pattern answers 307, both before the origin can rank anything. Documented in the README, not a bug |\n| `probe -H 'Accept: text\u002Fmarkdown;q=0.9, text\u002Fhtml;q=0.5' \"$PREVIEW$DOC\"` | markdown |\n| `probe -H 'Accept: text\u002Fmarkdown;q=0' \"$PREVIEW$DOC\"` | HTML |\n| `probe -H 'Accept: *\u002F*' \"$PREVIEW$DOC\"` | HTML, a wildcard never counts as asking |\n| `probe -H 'Accept: text\u002Fplain' \"$PREVIEW$DOC\"` | HTML, only `text\u002Fmarkdown` counts |\n| `probe -H 'Accept: application\u002Fxml' \"$PREVIEW$DOC\"` | HTML, unless the site set `notAcceptable: true`, which makes it a 406. Check the site's config before calling either one a failure |\n| `probe -A \"$BOT\" \"$PREVIEW$UNCOVERED\"` | HTML, an unconfigured page must not start negotiating |\n| `probe -A \"$BOT\" \"$PREVIEW$DOC?ref=x\"` | markdown, and if it is a 307 the `location` keeps `?ref=x` |\n| `probe -A \"$BOT\" -I \"$PREVIEW$DOC\"` | same status and headers as the GET. `-I`, not `-X HEAD`, which makes curl wait for a body that never comes |\n\nTwo things that fail quietly, so check them explicitly:\n\n- **`Vary: Accept, User-Agent` is present on the markdown responses too**, including the ones served straight off the CDN rewrite and the prerendered `\u002Fsitemap.md` and `\u002Fraw\u002Findex.md`. This is the `continue: true` fix most hand-rolled implementations get wrong. A markdown response with no `Vary` poisons the shared cache for browsers, and a checker following a 307 to the raw twin sees a URL with two representations behind it and no header saying so.\n- **The three markdown representations are byte-identical.**\n\n```sh\ncurl -sS -H 'Accept: text\u002Fmarkdown' \"$PREVIEW$DOC\" > \u002Ftmp\u002Fa.md\ncurl -sS \"$PREVIEW$DOC.md\" > \u002Ftmp\u002Fb.md\ncurl -sS \"$PREVIEW$RAW${DOC}.md\" > \u002Ftmp\u002Fc.md\ndiff \u002Ftmp\u002Fa.md \u002Ftmp\u002Fb.md && diff \u002Ftmp\u002Fb.md \u002Ftmp\u002Fc.md\n```\n\n## Step 3: sweep every page\n\nThe sample above proves the mechanism. This proves the coverage, and it is the only way to catch a page that failed to prerender, a subtree a `routes` pattern misses, or a document the adapter cannot stringify. Run it over the whole inventory, not a sample.\n\n```sh\ncat > \u002Ftmp\u002Fsweep.sh \u003C\u003C'EOF'\n#!\u002Fbin\u002Fsh\np=\"$1\"\nagent=$(curl -sS -o \u002Fdev\u002Fnull -w '%{http_code} %{content_type} %{redirect_url}' -A \"$BOT\" \"$PREVIEW$p\")\nhtml=$(curl -sS -o \u002Fdev\u002Fnull -w '%{http_code} %{content_type}' -A \"$BROWSER\" -H \"Accept: $HTML\" \"$PREVIEW$p\")\ntwin=$([ \"$p\" = \"\u002F\" ] && echo skip || curl -sS -o \u002Fdev\u002Fnull -w '%{http_code} %{content_type}' \"$PREVIEW${p%\u002F}.md\")\nraw=$(curl -sS -o \u002Fdev\u002Fnull -w '%{http_code}' \"$PREVIEW$RAW$([ \"$p\" = \"\u002F\" ] && echo \u002Findex || echo \"${p%\u002F}\").md\")\necho \"$p | agent: $agent | html: $html | twin: $twin | raw: $raw\"\nEOF\nchmod +x \u002Ftmp\u002Fsweep.sh\nexport PREVIEW BOT BROWSER HTML\nxargs -P 8 -n 1 \u002Ftmp\u002Fsweep.sh \u003C \u002Ftmp\u002Fpages > \u002Ftmp\u002Fsweep.txt\n```\n\nThen classify. Every line should fall into one of two shapes:\n\n- **negotiated**: agent `200 text\u002Fmarkdown`, or `307` with a `redirect_url` at the raw twin\n- **not negotiated**: agent `200 text\u002Fhtml`, and the page is genuinely outside the `routes` patterns\n\nEverything else is a finding:\n\n```sh\ngrep -v 'text\u002Fmarkdown' \u002Ftmp\u002Fsweep.txt | grep -v ' 307 '     # pages serving HTML to an agent\ngrep -v 'html: 200 text\u002Fhtml' \u002Ftmp\u002Fsweep.txt                  # pages broken for browsers\ngrep -v 'twin: 200 text\u002Fmarkdown\\|twin: skip' \u002Ftmp\u002Fsweep.txt  # .md twins that 404\ngrep -v 'raw: 200' \u002Ftmp\u002Fsweep.txt                             # raw route holes\nawk '{print $NF}' \u002Ftmp\u002Fsweep.txt | sort | uniq -c              # raw status distribution\n```\n\nA page that is HTML-only to agents is a finding when its neighbours negotiate, and expected when the site deliberately leaves that section out. Group them by prefix before deciding, and use one of them as `$UNCOVERED`.\n\nAlso sweep for empty or truncated documents, which return 200 and look fine:\n\n```sh\nwhile read -r p; do\n  n=$(curl -sS \"$PREVIEW$RAW$([ \"$p\" = \"\u002F\" ] && echo \u002Findex || echo \"${p%\u002F}\").md\" | wc -c)\n  [ \"$n\" -lt 200 ] && echo \"THIN $n $p\"\ndone \u003C \u002Ftmp\u002Fpages\n```\n\n## Step 4: the raw route\n\n- `show \"$PREVIEW$RAW${DOC}.md\"` returns 200, `text\u002Fmarkdown`, and a `Link` header with both `rel=\"canonical\"` and `rel=\"alternate\"; type=\"text\u002Fhtml\"` pointing at the page URL.\n- `probe \"$PREVIEW$RAW${SECTION}.md\"` returns 302 to the first document under that section, not a 404.\n- `show \"$PREVIEW$RAW\u002Fdoes-not-exist.md\"` returns 404 with a markdown body.\n- `curl -sS \"$PREVIEW$RAW\u002Findex.md\" | head -30` returns the homepage document, or the generated landing page: frontmatter, canonical and alternate links, and a resources block.\n- Site-relative links are absolutised. Every `](\u002F` outside a fenced block is a bug, across the whole inventory:\n\n```sh\nwhile read -r p; do\n  curl -sS \"$PREVIEW$RAW$([ \"$p\" = \"\u002F\" ] && echo \u002Findex || echo \"${p%\u002F}\").md\" \\\n    | awk '\u002F^ {0,3}(```|~~~)\u002F{f=!f} !f' | grep -q '](\u002F' && echo \"RELATIVE $p\"\ndone \u003C \u002Ftmp\u002Fpages\n```\n\n  Read each match in context before reporting it. The scan skips fenced blocks but not inline code spans, so a page writing `](\u002Fx)` inside backticks is a false positive, and the module leaves those alone on purpose. Sites still running a hand-rolled layer do carry relative links here, so this check is expected to differ between production and the preview.\n\n- Fenced blocks survived. Spot-check a page documenting markdown or MDC and confirm nothing inside a fence was rewritten.\n- Markdown structure survived the stringifier: pick five pages with tables, callouts, nested lists, code groups and frontmatter, and read them. A component that stringified to nothing leaves a 200 with a hole in it.\n\n## Step 5: discovery documents\n\n```sh\nshow \"$PREVIEW\u002F\"                               # Link header lives on \u002F only\ncurl -sS \"$PREVIEW\u002F.well-known\u002Fapi-catalog\" | jq .\ncurl -sS \"$PREVIEW\u002F.well-known\u002Fmcp\u002Fserver-card.json\" | jq .\ncurl -sS \"$PREVIEW\u002Fsitemap.md\" | head -40\ncurl -sS \"$PREVIEW\u002F.well-known\u002Fskills\u002Findex.json\" | jq .\ncurl -sS \"$PREVIEW\u002Frobots.txt\"\n```\n\n- The `Link` header on `\u002F` carries only IANA-registered rels. Any `rel=\"llms\"`, `rel=\"mcp\"`, `rel=\"design\"` or other invented relation means something outside the module is emitting headers, since the module rejects them at build time.\n- Every href in the `Link` header and the api-catalog resolves. Fetch each one and record the status.\n- `api-catalog` answers `application\u002Flinkset+json`, parses, and every `anchor` and `href` is absolute.\n- The server card carries `serverInfo.name` and an `endpoints` array (`endpoint` and `name` are the config keys, not the served fields), and lists live tools where the site runs `@nuxtjs\u002Fmcp-toolkit`. The endpoint it names answers: POST an MCP `initialize` to it. There is no `$schema`: the MCP spec has no server card yet.\n- `sitemap.md` links point at a raw twin for every page a `routes` pattern covers. A page outside those patterns correctly links to its HTML URL, and a path under `excludePrefixes` is left out entirely, so compare against the negotiated set rather than against `sitemap.xml`.\n- The skills index lists skills, and every file it names fetches 200:\n\n```sh\n# Fail loudly when the index itself is missing: an empty pipeline below would\n# otherwise report a clean pass on a site that serves no index at all.\nif ! curl -fsS \"$PREVIEW\u002F.well-known\u002Fskills\u002Findex.json\" > \u002Ftmp\u002Fskills.json; then\n  echo 'FAIL: no skills index'   # a finding whenever the site ships skills\nelse\n  jq -r '.skills[] | .name as $n | .files[] | \"\\($n)\u002F\\(.)\"' \u002Ftmp\u002Fskills.json \\\n    | while read -r p; do printf '%s %s\\n' \"$(curl -sS -o \u002Fdev\u002Fnull -w '%{http_code}' \"$PREVIEW\u002F.well-known\u002Fskills\u002F$p\")\" \"$p\"; done\nfi\n```\n\n- `robots.txt` names the agents negotiation actually matches. Cross-check it: every user agent named there gets markdown on `$DOC`, and the list is not shorter than the 18 defaults unless the site replaced it.\n\n## Step 6: the nuxt-llms bridge\n\n`\u002Fllms.txt` and `\u002Fllms-full.txt` stay owned by `nuxt-llms`. The module only feeds them.\n\n```sh\ncurl -sS -D - -o \u002Ftmp\u002Fllms.txt \"$PREVIEW\u002Fllms.txt\" | head -5\ncurl -sS -o \u002Ftmp\u002Fllms-full.txt \"$PREVIEW\u002Fllms-full.txt\"\nwc -c \u002Ftmp\u002Fllms.txt \u002Ftmp\u002Fllms-full.txt\ngrep -oE '\\]\\([^)]+\\)' \u002Ftmp\u002Fllms.txt | grep -v '\\.md)' | head       # same-origin links that missed the raw rewrite\n```\n\n- Both answer 200 as plain text at their existing paths.\n- Every same-origin link in `llms.txt` points at its raw twin, hand-written links included, and every one of them resolves.\n- The link set matches the inventory. A section that lost its links, or a page that fell out, is a finding:\n\n```sh\n# The homepage is `\u002F` in the sitemap and `\u002Findex` in a raw twin, so both sides\n# are folded to `\u002F` before the diff. Without that a correct site reports one.\nhome() { sed -e 's|\\(.\\)\u002F$|\\1|' -e 's|^\u002Findex$|\u002F|' -e 's|^$|\u002F|' | sort -u; }\ngrep -oE 'https?:\u002F\u002F[^)]+\\.md' \u002Ftmp\u002Fllms.txt | sed \"s|.*$RAW||;s|\\.md$||\" | home > \u002Ftmp\u002Fllms-pages\ndiff \u003C(home \u003C \u002Ftmp\u002Fpages) \u002Ftmp\u002Fllms-pages | head -40\n```\n\n- A page's body inside `llms-full.txt` is byte-identical to the **body** of `\u002Fraw\u002F\u003Cpage>.md`, which is that document minus its frontmatter block and its `## Sitemap` footer. They come from one pipeline now, so any difference beyond that envelope is a bug. Check three pages from different sections.\n\n## Step 7: error bodies\n\n| Request | Expected |\n|---|---|\n| `show -A \"$BOT\" \"$PREVIEW\u002Fnope\"` | 404, `text\u002Fmarkdown`, frontmatter with `title` and `status`, recovery links absolute and resolving |\n| `probe -A \"$BROWSER\" -H \"Accept: $HTML\" \"$PREVIEW\u002Fnope\"` | 404 HTML error page |\n| `probe -H 'Accept: *\u002F*' -H 'Sec-Fetch-Mode: cors' \"$PREVIEW\u002Fnope\"` | not markdown, a browser `fetch()` keeps what it was written against |\n| `probe -H 'Accept: application\u002Fjson' \"$PREVIEW\u002Fapi\u002Fdoes-not-exist\"` | JSON error, never markdown |\n| `probe -A \"$BOT\" \"$PREVIEW\u002F_nuxt\u002Fnope.js\"` | not markdown |\n| `probe -A \"$BOT\" \"$PREVIEW\u002Fnope.png\"` | not markdown, a dotted path is an asset |\n| `curl -sS \"$PREVIEW\u002Fnope\"` | markdown, plain curl with no `Accept` counts as an agent |\n| `probe -A \"$BOT\" \"$PREVIEW\u002Fnope\u002Fdeeper\u002Fstill\"` | 404 markdown, not a redirect loop |\n\nThe 404 must be a real 404. A 200 carrying the app shell tells every agent that every path exists.\n\n## Step 8: no collateral damage\n\n- `\u002Fsitemap.xml` is XML, resolves, and does not list a single raw URL: `curl -sS \"$PREVIEW\u002Fsitemap.xml\" | grep -c '\u002Fraw\u002F'` must print 0.\n- On a site running `@nuxtjs\u002Fsitemap`, prove the sitemap was built from the deployment's own data: fetch `$PREVIEW\u002Fsitemap.xml` and `$PROD\u002Fsitemap.xml` and compare entries. A preview sitemap that is a copy or strict subset of production's, same `lastmod` shape, entries the preview's own content cannot produce, means the prerender ingested the live production sitemap (a `prerender: true` route rule on `\u002Fsitemap.xml` does this) and the deploy ships stale SEO data. Also assert no prerendered legacy-version pages or redirect stubs leaked in through app sources.\n- Assets are untouched: a favicon, an og-image, a `_payload.json`, anything dotted under a negotiated pattern.\n- The site's own endpoints answer. Enumerate them from the HTML the app ships (`grep -o '\u002Fapi\u002F[a-z0-9.\u002F-]*'` over a rendered page) and from the api-catalog, then fetch each one.\n- Any OpenAPI document parses and its `paths` are populated:\n\n```sh\ncurl -sS \"$PREVIEW\u002Fopenapi.json\" | jq -r '.paths | keys[]' | head -40\n```\n\n- A doc page's head still carries its canonical, alternate, og and JSON-LD tags. `useCanonical()` moving from a site file into the module is exactly how a trailing slash, a locale alternate or an og tag goes missing:\n\n```sh\ncurl -sS -A \"$BROWSER\" -H \"Accept: $HTML\" \"$PREVIEW$DOC\" \\\n  | grep -oE '\u003C(link|meta|script)[^>]*(canonical|alternate|og:|ld\\+json)[^>]*>'\n```\n\n- The rendered HTML carries real content, not just an app shell: an `\u003Ch1>` and a few hundred characters of text before hydration.\n- If a browser tool is available (Playwright, a devtools MCP), load three pages and check for console errors and hydration mismatches. Say so in the report when no browser was available.\n\n## Step 9: production diff\n\n`$PROD` came out of Step 1. Skip this when it resolves to the same deployment, or when production already runs the same module version.\n\nWhen production still runs a prior implementation, this is the strongest check available. Diff a sample of at least ten pages spread across sections, rewriting hosts so only content differences remain.\n\n```sh\nfor p in $(awk 'NR % 18 == 1' \u002Ftmp\u002Fpages); do        # every 18th page, spread across the inventory\n  curl -sS \"$PROD\u002Fraw${p%\u002F}.md\"    | sed \"s|$PROD||g\" > \u002Ftmp\u002Fprod.md\n  curl -sS \"$PREVIEW$RAW${p%\u002F}.md\" | sed \"s|$PREVIEW||g;s|$PROD||g\" > \u002Ftmp\u002Fprev.md\n  echo \"== $p\"; diff \u002Ftmp\u002Fprod.md \u002Ftmp\u002Fprev.md || true\ndone\n```\n\nDo the same for `\u002Fllms.txt`, `\u002Fsitemap.md`, `\u002Frobots.txt`, `\u002F.well-known\u002Fapi-catalog`, the head tags from Step 8, and the page inventory:\n\n```sh\ncurl -sS \"$PROD\u002Fsitemap.xml\" | grep -o '\u003Cloc>[^\u003C]*' | sed -E 's|\u003Cloc>||; s|https?:\u002F\u002F[^\u002F]+||' | sort -u > \u002Ftmp\u002Fprod-pages\ndiff \u002Ftmp\u002Fprod-pages \u002Ftmp\u002Fpages | head -40\ncomm -23 \u003C(curl -sS \"$PROD\u002Fopenapi.json\" | jq -r '.paths|keys[]' | sort) \\\n         \u003C(curl -sS \"$PREVIEW\u002Fopenapi.json\" | jq -r '.paths|keys[]' | sort)\n```\n\nDifferences worth accepting: prose the PR intentionally changed, links that were relative before and are absolute now, and MDC output the adapter renders differently on purpose. Everything else is a regression: a dropped page, a lost frontmatter field, a mangled fence, a table rendered as prose, a code block that lost its language, a component that stringified to nothing, an API path that disappeared.\n\n## Step 10: is-agentic score\n\n[`is-agentic`](https:\u002F\u002Fgithub.com\u002Fvercel-labs\u002Fis-agentic) (Vercel Labs) scores agent readiness.\n\n```sh\nnpx -y is-agentic \"$PREVIEW\" --json > \u002Ftmp\u002Fis-agentic-preview.json\nnpx -y is-agentic \"$PROD\"\n```\n\nThe preview score must be at least production's, and no check may move from pass to fail. Report the delta and the named checks that changed.\n\nCaveats to state rather than work around:\n\n- It is a hosted scanner. The URL is submitted to `is-agentic.com`, which keeps the report and can serve it to anyone asking for that domain. Ask before scanning a preview of unreleased work.\n- It probes with browser-ish headers, so a check can come back partial on a site that handles agents correctly. Its \"Agent-friendly 404s\" check reads a browser 404, not the markdown body an agent gets. Reproduce a partial by hand before reporting it.\n- A \"crawler unknown\" or \"blocked\" verdict for a user agent that demonstrably gets 200 markdown via curl means the scanner's own egress was challenged by the platform, a Vercel or WAF setting rather than site code. Reproduce with the exact UA before reporting it.\n- The brand-discoverability check is meaningless against a `*.vercel.app` preview URL. Score it only against production.\n\n## Optional: the build output table\n\nOnly when the repo is at hand and something in Step 2 or 3 looked structurally wrong. Build with the vercel preset and read `.vercel\u002Foutput\u002Fconfig.json`:\n\n```sh\njq '.routes | length' .vercel\u002Foutput\u002Fconfig.json\njq '.routes[0:6]' .vercel\u002Foutput\u002Fconfig.json\n```\n\n- The first route sets `Vary: Accept, User-Agent` on the pages with `continue: true`, ahead of anything Nitro emits from `routeRules`. The second sets it on the markdown representations: the raw prefix, the `.md` twins and `\u002Fsitemap.md`. The `Link` route on `\u002F` carries `continue: true` too.\n- Two negotiated routes per configured pattern, one matching `Accept`, one matching the agent user agents. Prerendered patterns rewrite with `check: true`, cached patterns 307.\n- Total route count tracks the pattern count, not the page count.\n\n## What this does not cover\n\nSay so in the report rather than implying full coverage:\n\n- Anything behind auth, and anything the CDN serves differently to a real bot IP than to curl.\n- Content or schema changes whose effect shows up only in search indexes, navigation payloads or component metadata that the sweep does not fetch.\n- Visual regressions, and client-side behaviour beyond console errors when a browser tool was available.\n- Performance, cold starts and function count.\n\n## Attributing a finding\n\nEvery finding lands in one of two repos, and the fix is different in each. Attribute before reporting, and say which evidence you used.\n\n**Rule of thumb.** The site owns its configuration, its content and its hooks. The module owns behaviour no configuration can change. So: a whole section that never negotiates is a `routes` pattern, a single page whose table came out mangled is content or an adapter, and a missing `Vary` on a CDN rewrite is the module, because no site config can produce or prevent it.\n\n| Symptom | Likely owner | Confirm by |\n|---|---|---|\n| No `Vary` on a markdown response, or none on `\u002F` | module, `src\u002Fpresets\u002Fvercel.ts` | check the site does not override headers for that path in `routeRules`. Off Vercel a prerendered `\u002Fsitemap.md` or `\u002Fraw\u002Findex.md` is served off disk and carries none, which is the documented caveat, not a bug |\n| `text\u002Fplain` or `q=0` decided wrong, wildcard treated as asking | module, `src\u002Fruntime\u002Fshared\u002Fnegotiation.ts` | a unit test in the module's `test\u002F`, it is pure logic |\n| A whole section serves HTML to agents | site, `routes` patterns | does the pattern actually cover it? if it plainly does, the module's `compilePattern`\u002F`matchRoute` |\n| `.md` twin 404s while `\u002Fraw\u002F**.md` answers | module, `rawDestination` or the preset rewrite | unless an `excludePrefixes` entry on the site swallows that path |\n| A standalone `.md` the site serves itself got rewritten | site, add it to `excludePrefixes` | |\n| Invented `rel` in the `Link` header | site, something outside the module emits it | the module validates rels at build time and throws |\n| A discovery link missing from the api-catalog | site, `discovery.links` | only `service-desc` and `service-doc` entries carrying an `anchor` are grouped, by design |\n| Mangled table, dropped component, lost code language | site content, its MDC transform, or its `agent-discovery:document` hook | reproduce the same construct on the module playground; if it breaks there too it is the stringifier in `src\u002Fruntime\u002Fserver\u002Fsources\u002Fcontent.ts` |\n| Relative links left in a raw document | module, `absolutizeMarkdownLinks` | unless the site ships a custom adapter, which has to call it itself |\n| Section URL 404s instead of redirecting to its first page | module `firstLeaf` handling, or the site's adapter not implementing `firstLeaf` | which source the site configures |\n| Agent gets an HTML 404, or a browser gets markdown | module, `src\u002Fruntime\u002Fserver\u002Ferror.ts` | unless the site chained its own `errorHandler` after the module's |\n| `llms.txt` links not pointing at raw twins | module, the llms bridge plugin | unless the site sets its own `llms:generate` hooks or `contentRawMarkdown` |\n| `robots.txt` short on agents | site, `userAgents.replace` or `@nuxtjs\u002Frobots` config | compare with the module's 18 defaults |\n| `sitemap.xml` listing raw URLs | module, the `@nuxtjs\u002Fsitemap` exclude | unless the site configures its own `sitemap.exclude` |\n| A cached page serving mixed variants | module, cached-route detection | which `routeRules` the site added, and whether the pattern overlaps |\n\n**Three ways to settle a doubtful one:**\n\n1. **Second deployment.** If another site running the same module version shows the same failure, it is the module. If only this deployment shows it, it is the site.\n2. **The playground.** The module repo's `pnpm dev` serves a minimal fixture on `localhost:3000`. Send the same headers. A failure that reproduces there is the module's, on a site whose config you control.\n3. **A failing test.** The negotiation core is pure and unit-tested in the module's `test\u002F`. For any decision bug (`Accept` parsing, pattern matching, exclusions, error preference) write the case as a test rather than arguing from a curl. That test is the deliverable.\n\nNever push a fix to either repo from this skill. Hand back the finding and, for the module, the test that proves it.\n\n## Report\n\nGive, in this order:\n\n1. A one-line verdict: ship, or blocked.\n2. A table of every step with pass, fail or n\u002Fa, and the sweep counts: pages checked, negotiated, HTML-only, twin failures, raw failures.\n3. **Findings for the site PR**, each with: what broke, the curl repro, the config or content line to change, and the evidence for attributing it here.\n4. **Findings for the module**, each with: what broke, the curl repro, the invariant it violates, the file that owns it, and a test case that would fail today. Write these so they paste into the module repo as an issue body without editing.\n5. Differences judged intentional, so the user can confirm the call.\n6. What could not be checked and why.\n",{"name":5,"description":7},"main","6e69c10dc62009e1c26a6c5217742847c55e185a","skills\u002Fverify-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":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add benjamincanac\u002Fnuxt-agent-discovery --skill verify-nuxt-agent-discovery",{"tool":64,"command":65},"bunx","bunx skills add benjamincanac\u002Fnuxt-agent-discovery --skill verify-nuxt-agent-discovery",{"tool":67,"command":68},"pnpm","pnpm dlx skills add benjamincanac\u002Fnuxt-agent-discovery --skill verify-nuxt-agent-discovery","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery\u002Fblob\u002Fmain\u002Fskills\u002Fverify-nuxt-agent-discovery\u002FSKILL.md",true,[],[73,97,112],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":87,"publishedAt":44},"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",[79,82],{"categoryNo":80,"categoryName":81,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":83,"categoryName":84,"taxonomyType":85,"taxonomyTypeDesc":86},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[75,88,89,90,34,91,92,93,94,95,96],"unit","fixture","tests","which","list","agent skill","pin","for","unit tests",{"skillNo":98,"name":99,"nameZh":6,"description":100,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":101,"categories":102,"keywords":103,"publishedAt":44},"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",[],[104,105,106,107,108,93,109,110,89,111],"basic","basic site","site","serving","skills catalog","file","catalog","test",{"skillNo":113,"name":114,"nameZh":6,"description":115,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":122,"publishedAt":44},"01M71FS50MM46P5JZS1Y65VDE5","migrate-to-nuxt-agent-discovery","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.","https:\u002F\u002Fgithub.com\u002Fbenjamincanac\u002Fnuxt-agent-discovery\u002Fblob\u002Fmain\u002Fskills\u002Fmigrate-to-nuxt-agent-discovery\u002FSKILL.md",[118,119],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":120,"categoryName":121,"taxonomyType":85,"taxonomyTypeDesc":86},"01JBSEEDCAT000000000000030","开发工程师",[25,28,123,124,27,125,126,127,128,129,130,131,31,29,132,34,133,110,134,135],"migrate","migrate to nuxt agent discovery","modules","llms","with","hand","api","code","rewrite","MCP","hooks","remove","pipeline"]