Skip to content

feat: improve agent readability and discovery#2233

Open
benjamincanac wants to merge 26 commits intomainfrom
feat/agent-readability
Open

feat: improve agent readability and discovery#2233
benjamincanac wants to merge 26 commits intomainfrom
feat/agent-readability

Conversation

@benjamincanac
Copy link
Copy Markdown
Member

@benjamincanac benjamincanac commented Apr 29, 2026

📚 Description

Improves nuxt.com's discoverability and readability for AI agents, following the Vercel agent-readability spec.

  • Discovery: /.well-known/api-catalog (RFC 9727), /.well-known/mcp/server-card.json (SEP-1649), RFC 8288 Link headers, Content-Signal in robots.txt, Vary: Accept, User-Agent on negotiated routes.
  • Markdown for agents: Accept: text/markdown and AI user-agent rewrites (Vercel edge), markdown alternate <link> per page via useCanonical composable, /sitemap.md, /raw/index.md, /raw/modules.md, /raw/changelog.md.
  • Structured data: Migrate to nuxt-schema-org v6 — Organization + WebSite + per-page TechArticle/BreadcrumbList JSON-LD, site-wide canonicals, <lastmod> on sitemap.xml, 302 on docs version redirects.
  • OG Image v6: Migrate to nuxt-og-image v6 with Takumi renderer, zeroRuntime: true, replace defineOgImageComponentdefineOgImage across all pages.
  • Caching: /raw/** and /.well-known/** use defineCachedEventHandler (SWR, 1h).
  • Cleanup: Centralize docs versions in shared/utils/docs.ts, drop queryCollectionWithEvent type cast, drive /raw/index.md from content/index.yml, trim /llms.txt 127 KB → 51 KB.

Publish well-known agent endpoints (RFC 9727 API catalog, MCP server card),
declare AI usage preferences via Content-Signal in robots.txt, and advertise
related resources via RFC 8288 Link headers and Vary: Accept, User-Agent on
agent-relevant routes. Extend md-rewrite to negotiate markdown via the Accept
header and known AI user agents (Vercel only). Add /raw/index.md as the
markdown counterpart of the (data-driven) homepage.
Set canonical URLs and a markdown alternate link site-wide via app.vue, plus
Organization + WebSite JSON-LD on every page, SoftwareApplication on the
homepage, and TechArticle + BreadcrumbList on docs pages. JSON-LD output is
HTML-escaped to prevent injection. Replace the meta-refresh stub used by the
docs version middleware with a real 301 so agents and crawlers follow it
without losing the canonical and structured data.
Publish a markdown sitemap at /sitemap.md so agents can list every page
without parsing XML. Add <lastmod> entries to sitemap.xml (post date for
blog, today for docs). Add a Nitro plugin that drops the v3 (legacy) and
v5 (nightly) sections from /llms.txt to keep it under the agent context
window threshold (127 KB → 51 KB); the full content is still served via
/llms-full.txt.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nuxt Ready Ready Preview, Comment Apr 30, 2026 4:03pm

Pull all hardcoded host strings through useSiteConfig() / getSiteConfig(event)
so canonical, JSON-LD, markdown alternate, sitemap.xml, sitemap.md and the
well-known endpoints all use the runtime origin. RFC 8288 Link headers on /
become relative URIs so they resolve to the request origin on previews,
production and localhost. nuxt-site-config-kit auto-detects VERCEL_URL etc.,
so the production site.url is provided by the deployment env (and the existing
$development override keeps localhost working in dev).

Switch /sitemap.md from ISR (with a redundant defineCachedEventHandler) to
plain prerender — the underlying collections are static, so a single build-
time render is enough.

Local audit goes from 73 to 77: JSON-LD, canonical (5/5), and markdown
alternate now surface to the auditor since sitemap.xml entries finally
point at the audited origin.
- Drop the v5 (nightly) section from /sitemap.md and skip /docs/5.x/* in the
  Vercel md-rewrite rules so the agent surfaces stay consistent with
  robots.txt's `Disallow: /docs/5.x/`.
- Switch the docs version middleware redirect from 301 to 302 so the target
  can flip to /docs/5.x/* when Nuxt 5 ships without leaving stale 301s
  cached forever in browsers and CDNs.
- Drop the redundant `transport` field on the MCP server card (only
  `endpoints[]` is in the current SEP-1649 shape).
- Advertise `<rel="alternate"; type="text/html">` alongside the canonical
  Link on /raw/index.md so agents that landed on the markdown copy can
  find the rich HTML page.
- Remove the hardcoded `softwareVersion: '4'` from the homepage JSON-LD —
  it would silently go stale when 5 ships and is optional in
  schema.org/SoftwareApplication.
- Use setResponseHeader consistently across the agent-facing routes.
- Pin api-catalog and MCP server card service-doc / documentation links to
  /docs/4.x/guide/ai/mcp so agents skip the docs version redirect hop.
/sitemap.md was listing v3 (legacy) and v5 (nightly) docs while
/sitemap.xml only ships v4 + blog. Drop the v3 section so both sitemaps
agree on what's surfaced to agents and crawlers; v5 was already removed
in the previous commit.
- Hardcode `https://nuxt.com` in /.well-known/api-catalog and
  /.well-known/mcp/server-card.json so previews advertise the canonical
  MCP server (matches nuxt/ui pattern).
- Wrap getSiteConfig(event).url with withoutTrailingSlash in the deploy-
  specific routes (sitemap.xml, sitemap.md, raw/index.md) to fix double
  slashes in generated URLs.
- app/app.vue: suppress canonical on unversioned /docs/* (meta-refresh
  stubs that the docs-version middleware redirects elsewhere should not
  advertise themselves as canonical).
- app/pages/docs/[...slug].vue: guard prerenderRoutes() with
  import.meta.server so it doesn't run on every client navigation.
- app/middleware/docs-version.global.ts: comment was overstating the
  benefit — prerendered pages still emit a meta-refresh stub; the 302
  only applies to dynamic renders.
- server/routes: standardize on defineEventHandler (auto-imported) and
  drop the manual h3 imports.
- modules/md-rewrite.ts: type the nitro arg as Nitro and tighten the
  negotiated rewrites from (.*) to (.+) so /blog/, /deploy/ trailing-
  slash variants don't rewrite to /raw/blog/.md.
When Vercel rewrites /docs/foo to /raw/docs/foo.md based on Accept or
User-Agent, the cached response at /raw/docs/foo.md must also carry
Vary: Accept, User-Agent — otherwise CDNs may serve cached markdown
to a browser that asked for HTML, or vice versa.

Picked up by the @vercel/agent-readability audit:
"Vary: Accept missing — without it CDNs serve cached HTML to agents
or markdown to browsers".
`queryCollection` from `@nuxt/content/server` now accepts an H3Event
directly — the custom type alias and cast are no longer necessary.
Replace hardcoded version strings and regex patterns across middleware,
pages, md-rewrite module, and .well-known endpoints with imports from
the single source of truth in `shared/utils/docs.ts`. Remove the
redundant DOCS_VERSION re-export from agent-discovery.ts.
Only needed for edge runtimes; the project uses zeroRuntime mode which
prerenders OG images at build time on Node via @takumi-rs/core.
defineWebSite now pulls `name` from useSiteConfig() instead of a
hardcoded string, keeping it in sync with nuxt.config.ts.
@benjamincanac
Copy link
Copy Markdown
Member Author

benjamincanac commented Apr 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Rename agent-discovery.ts → site.ts (getSiteUrl) and
agent-fingerprint.ts → agent.ts (getAgentFingerprint).
Use getSiteUrl in sitemap handlers instead of inline
withoutTrailingSlash(getSiteConfig(event).url).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
shared/utils/docs.ts (1)

13-13: ⚡ Quick win

Constrain CURRENT_DOCS_VERSION to the supported-version union.

Right now it can drift from SUPPORTED_DOC_VERSIONS without a compile-time error.

Proposed change
-export const CURRENT_DOCS_VERSION = '4.x'
+export const CURRENT_DOCS_VERSION: (typeof SUPPORTED_DOC_VERSIONS)[number] = '4.x'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/utils/docs.ts` at line 13, CURRENT_DOCS_VERSION can drift from
SUPPORTED_DOC_VERSIONS causing no compile-time check; change its declaration so
its type is the union derived from SUPPORTED_DOC_VERSIONS (e.g. export const
SUPPORTED_DOC_VERSIONS = ['x','y'] as const; type SupportedDocVersion = typeof
SUPPORTED_DOC_VERSIONS[number]; export const CURRENT_DOCS_VERSION:
SupportedDocVersion = '4.x') so the compiler enforces that CURRENT_DOCS_VERSION
is one of the supported values referenced by SUPPORTED_DOC_VERSIONS; update the
declaration of CURRENT_DOCS_VERSION accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/middleware/docs-version.global.ts`:
- Line 9: The current redirect bypass check in docs-version.global.ts uses
KNOWN_DOC_VERSIONS.some(v => to.path.startsWith(`/docs/${v}`)), which
incorrectly matches prefixes like /docs/4.x-foo; update the condition to enforce
a path boundary by testing either exact match or a slash after the version
(e.g., check KNOWN_DOC_VERSIONS.some(v => to.path === `/docs/${v}` ||
to.path.startsWith(`/docs/${v}/`)) or use a regex ^/docs/${v}($|/)) so only true
version routes pass; modify the check where KNOWN_DOC_VERSIONS and to.path are
referenced to use one of these boundary-safe tests.

In `@server/routes/sitemap.xml.get.ts`:
- Line 35: The sitemap response currently returns streamToPromise(sitemap)
without setting a content type; update the request handler that calls
streamToPromise(sitemap) to explicitly set the response Content-Type to
"application/xml" (e.g., using res.setHeader or response.headers['content-type']
depending on the framework) before returning the stream result so the sitemap
buffer is served with the correct XML MIME type.

---

Nitpick comments:
In `@shared/utils/docs.ts`:
- Line 13: CURRENT_DOCS_VERSION can drift from SUPPORTED_DOC_VERSIONS causing no
compile-time check; change its declaration so its type is the union derived from
SUPPORTED_DOC_VERSIONS (e.g. export const SUPPORTED_DOC_VERSIONS = ['x','y'] as
const; type SupportedDocVersion = typeof SUPPORTED_DOC_VERSIONS[number]; export
const CURRENT_DOCS_VERSION: SupportedDocVersion = '4.x') so the compiler
enforces that CURRENT_DOCS_VERSION is one of the supported values referenced by
SUPPORTED_DOC_VERSIONS; update the declaration of CURRENT_DOCS_VERSION
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fee596ee-6057-4816-9bbd-0fc2a60c786c

📥 Commits

Reviewing files that changed from the base of the PR and between 6cf4872 and e1f35d5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • app/app.vue
  • app/middleware/docs-version.global.ts
  • app/pages/docs/[...slug].vue
  • modules/md-rewrite.ts
  • nuxt.config.ts
  • package.json
  • server/routes/.well-known/api-catalog.get.ts
  • server/routes/.well-known/mcp/server-card.json.get.ts
  • server/routes/blog/rss.xml.ts
  • server/routes/raw/index.md.get.ts
  • server/routes/sitemap.md.get.ts
  • server/routes/sitemap.xml.get.ts
  • server/utils/site.ts
  • shared/utils/docs.ts
✅ Files skipped from review due to trivial changes (3)
  • package.json
  • server/routes/raw/index.md.get.ts
  • nuxt.config.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • server/routes/sitemap.md.get.ts
  • server/routes/.well-known/mcp/server-card.json.get.ts
  • server/routes/.well-known/api-catalog.get.ts
  • modules/md-rewrite.ts
  • app/app.vue

Comment thread app/middleware/docs-version.global.ts
Comment thread server/routes/sitemap.xml.get.ts
- Explicitly set application/xml on sitemap.xml response (was relying
  on Nitro content-type guessing from the Buffer)
- Type CURRENT_DOCS_VERSION as a union of SUPPORTED_DOC_VERSIONS so
  the compiler rejects values not in the supported list
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
nuxt.config.ts (1)

145-165: ⚡ Quick win

Consider deduplicating the repeated Vary value.

'Accept, User-Agent' is repeated across several route rules. A shared constant reduces drift risk if this changes later.

♻️ Suggested cleanup
+const AGENT_VARY = 'Accept, User-Agent'
+
 export default defineNuxtConfig({
   // ...
   routeRules: {
     '/': {
       prerender: true,
       headers: {
         Link: [/* ... */].join(', '),
-        Vary: 'Accept, User-Agent'
+        Vary: AGENT_VARY
       }
     },
-    '/modules': { isr: false, prerender: false, headers: { Vary: 'Accept, User-Agent' } },
-    '/changelog': { isr: 60 * 60, headers: { Vary: 'Accept, User-Agent' } },
-    '/docs/**': { headers: { Vary: 'Accept, User-Agent' } },
-    '/blog/**': { headers: { Vary: 'Accept, User-Agent' } },
-    '/deploy/**': { headers: { Vary: 'Accept, User-Agent' } },
-    '/raw/**': { headers: { Vary: 'Accept, User-Agent' } },
+    '/modules': { isr: false, prerender: false, headers: { Vary: AGENT_VARY } },
+    '/changelog': { isr: 60 * 60, headers: { Vary: AGENT_VARY } },
+    '/docs/**': { headers: { Vary: AGENT_VARY } },
+    '/blog/**': { headers: { Vary: AGENT_VARY } },
+    '/deploy/**': { headers: { Vary: AGENT_VARY } },
+    '/raw/**': { headers: { Vary: AGENT_VARY } },
   }
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nuxt.config.ts` around lines 145 - 165, Several route entries in
nuxt.config.ts repeat the string 'Accept, User-Agent' in headers; define a
single constant (e.g., VARY_ACCEPT_USER_AGENT) near the top of the config and
replace every headers: { Vary: 'Accept, User-Agent' } occurrence (including the
objects in the '/modules', '/changelog', '/docs/**', '/blog/**', '/deploy/**',
'/raw/**' route rules) to use that constant instead so the Vary value is
centralized and easy to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@nuxt.config.ts`:
- Around line 145-165: Several route entries in nuxt.config.ts repeat the string
'Accept, User-Agent' in headers; define a single constant (e.g.,
VARY_ACCEPT_USER_AGENT) near the top of the config and replace every headers: {
Vary: 'Accept, User-Agent' } occurrence (including the objects in the
'/modules', '/changelog', '/docs/**', '/blog/**', '/deploy/**', '/raw/**' route
rules) to use that constant instead so the Vary value is centralized and easy to
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e403a06-f3aa-4ce6-8694-b047b5aa083c

📥 Commits

Reviewing files that changed from the base of the PR and between 7e43af5 and 1039061.

📒 Files selected for processing (1)
  • nuxt.config.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants