Making a Nuxt site agent-ready: from Level 1 to Level 4 on isitagentready.com
AI agents are starting to operate websites, not just cite them. What it took to move a production Nuxt site on Cloudflare Workers from a score of 21 to Level 4 on the isitagentready scanner: markdown negotiation, API catalogs, agent-skills indexes, WebMCP, and the traps nobody documents.
There are now two different questions you can ask about your site and AI. The first is "will ChatGPT cite me?" — that's GEO, and it's mostly about crawler access and extractable content. The second is newer and stranger: "can an autonomous agent operate my site?" Can it discover your API, read your pages as markdown, call your tools, authenticate, and act?
Cloudflare ships a scanner for the second question: isitagentready.com. Point it at your domain and it grades you across fourteen checks, from robots.txt hygiene up to MCP servers and agent auth. I took a production Nuxt 4 site on Cloudflare Workers from a score of 21 (Level 1) to Level 4, "Agent-Integrated" — and later applied the same playbook to a platform with a real OAuth server and remote MCP server, which hit 100/100 (Level 5).
Everything I learned is distilled into an open-source skill you can drop into your own agent workflow: nuxt-agent-ready-best-practices. This post is the war-story version: what each check actually wants, and the four traps that cost me real hours.
The honesty rule comes first
Before any code: only publish discovery for services that actually exist. The scanner will happily award points for a /.well-known/openid-configuration, an MCP Server Card, a _mcp._agents DNS record. If there's no real auth server or MCP server behind them, you've built a map to a dead end. An agent will try to register, fail, and your site is now less trustworthy than if you'd published nothing.
This is why the marketing site tops out at Level 4 — and that's the correct score for what it runs. The platform that reached Level 5 got there because its OAuth server (Better-Auth's oauth-provider plugin) and its MCP server are real. The ceiling is set by what you operate, not by how many well-known files you can generate.
Markdown negotiation: agents don't want your HTML
The single most useful check: serve text/markdown when an agent asks for it. Agents burn tokens parsing your hydrated DOM; markdown is the format they actually want. The mechanism is plain HTTP content negotiation in a Nitro server middleware:
// server/middleware/agent-ready.ts
export default defineEventHandler(async (event) => {
const accept = getRequestHeader(event, 'accept') ?? '';
if (!accept.includes('text/markdown')) return;
if (!PROSE_ROUTES.has(event.path)) return;
const html = await event.$fetch<string>(event.path, {
headers: { accept: 'text/html' },
});
setResponseHeader(event, 'content-type', 'text/markdown; charset=utf-8');
setResponseHeader(event, 'vary', 'Accept');
return htmlToMarkdown(html);
});
Two load-bearing details in those few lines.
The internal re-fetch sends Accept: text/html, which is what stops the middleware from re-firing on its own subrequest. Remove that header and you've built an infinite loop.
And the fetch is event.$fetch with a relative path — not $fetch('https://mydomain.com/page'). This is the Nitro/Workers gotcha that bit me hardest: on a deployed Worker, an absolute-URL fetch to your own origin becomes a real edge subrequest, and Cloudflare returns an empty body on the same-zone loopback. On wrangler dev it works fine, because locally it's just one server talking to itself. The bug is invisible until production. A route on my site served an empty 81-byte llms-full.txt for weeks before I caught it. event.$fetch goes through Nitro's internal router — no network hop, identical behavior in dev and prod. If you take one thing from this post, take that.
Link headers: RFC 8288 as a site map for agents
Cheap check, real value: advertise your agent-relevant resources on every HTML response.
Link: <https://example.com/llms.txt>; rel="llms-txt",
<https://example.com/.well-known/api-catalog>; rel="api-catalog",
<https://example.com/sitemap.xml>; rel="sitemap"
One caveat from the security pass: skip the header on /api/** responses. Your JSON endpoints don't need it, and appending headers indiscriminately to API responses is how you end up leaking discovery hints on authenticated routes.
The API catalog: RFC 9727
/.well-known/api-catalog is a linkset document — JSON with an anchor per API and relations pointing at docs and metadata:
// server/routes/.well-known/api-catalog.ts
export default defineEventHandler((event) => {
setResponseHeader(event, 'content-type', 'application/linkset+json');
return JSON.stringify({
linkset: [
{
anchor: 'https://example.com/api/audit',
'service-doc': [{ href: 'https://example.com/docs/api' }],
describedby: [{ href: 'https://example.com/llms.txt' }],
},
],
});
});
List only endpoints that exist. The scanner reads the content; a catalog full of aspirational URLs fails the same honesty test as fake auth discovery.
Note the shape: a dynamic route returning a string, with the content-type set in the handler. On Cloudflare Pages this shape was the only one that worked at all — extensionless static files under public/.well-known/ get swallowed by the SPA fallback and served as index.html. On Workers with Nitro's cloudflare_module preset things are saner, but the dynamic-route pattern works everywhere, so it's what the skill recommends unconditionally.
Agent Skills: publishing your own SKILL.md
The Agent Skills discovery spec is the part I find most interesting, because it inverts the relationship: instead of agents scraping your site, you publish instructions for operating it. A discovery index at /.well-known/agent-skills/index.json:
{
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
"skills": [
{
"name": "run-free-audit",
"type": "skill-md",
"description": "Run a free listing audit for any business URL",
"url": "https://example.com/.well-known/agent-skills/run-free-audit/SKILL.md",
"digest": "sha256:9f2c…"
}
]
}
Each entry points at a real SKILL.md — a markdown document telling an agent exactly how to call your API — with a sha256 digest of the file.
The trap here is digest drift. My CI runs a formatter over everything, including markdown. The formatter reflowed the SKILL.md after I'd computed its digest, the served bytes no longer matched the index, and the check failed with a hash mismatch. Format first, then hash, and verify the digest of the file as actually served, not the one on your disk.
WebMCP: tools for in-browser agents
WebMCP exposes site actions to agents driving a real browser, via navigator.modelContext:
// app/plugins/webmcp.client.ts
export default defineNuxtPlugin(() => {
if (!('modelContext' in navigator)) return; // Chrome EPP only, today
navigator.modelContext.registerTool({
name: 'run_free_audit',
description: 'Run a listing audit for a business URL',
inputSchema: {
type: 'object',
properties: { url: { type: 'string' } },
required: ['url'],
},
async execute({ url }) {
return await $fetch('/api/audit', { method: 'POST', body: { url } });
},
});
});
Feature-detect and no-op everywhere else; the API only exists in Chrome's early preview today.
The gotcha that cost me an evening: the scanner validates WebMCP by loading your page in a headless, no-GPU browser with an 8-second navigation timeout, waiting for network idle. My page ships a live MapLibre globe that streams tiles continuously — the network never goes idle, the checker times out, and reports "Could not check WebMCP" even though the tools registered fine. The fix is one guard:
if (navigator.webdriver) {
// bots get a static fallback, humans get the globe
showStaticHero();
}
Agents don't need your WebGL. Give them the boring version and everyone passes.
Auth discovery: where the scanner reads your prose
This section only applies if you run a real authorization server — but the behavior is worth documenting because it's so unexpected: the scanner content-scans your auth.md. A valid 200 response with a perfectly reasonable description of your OAuth flow still fails with "auth.md exists but does not describe agent registration". The body has to follow the WorkOS AUTH.md recipe shape — addressing the agent directly, the ordered discover → register → authorize → exchange → revoke steps, explicit register_uri references.
Same story for the agent_auth block in /.well-known/oauth-authorization-server: the field names must match WorkOS's exactly. register_uri, not registration_uri. Intuitive naming fails silently. This is a young spec ecosystem consolidating around one vendor's recipe, and right now conformance means matching it byte-for-byte.
DNS-AID: discovery below HTTP
The last check lives in DNS: an HTTPS/SVCB record at _index._agents.yourdomain.com pointing agents at your discovery entrypoint. Verifiable the same way the scanner does it, over DoH:
curl -s 'https://cloudflare-dns.com/dns-query?name=_index._agents.example.com&type=65' \
-H 'accept: application/dns-json'
The catch: the draft spec requires your zone to be DNSSEC-signed, and enabling DNSSEC is a two-party operation — Cloudflare gives you a DS record, and that record has to be added at your registrar. If your registrar is also Cloudflare it's one click; if not, it's a manual step no API token will do for you. Publish _index._agents only; _a2a and _mcp subdomains are for services you actually run, per the honesty rule.
What it adds up to
None of these checks is individually hard. What makes agent-readiness real work is that the failure modes are all silent: the self-fetch that returns empty only in production, the digest that drifts when a formatter touches a file, the WebMCP check that times out because your hero animation keeps the network busy, the auth.md that fails on prose. The scanner inspects behavior, not presence — which is, honestly, the right call. Presence is easy to fake.
The full playbook — every rule, the exact record formats, the verification commands, the security-hardening pass I didn't have room for here — lives in the open-source skill: nuxt-agent-ready-best-practices. It's written to be consumed by coding agents, which feels appropriate: instructions for making sites agent-operable, delivered as instructions an agent can operate on.
GEO gets you cited. This gets you used. I think the second one is where the interesting decade is.