Making My Static Sites Agent-Ready
This is part 3 of a short series on modernizing my static sites. Part 1 was the Jekyll-to-Bridgetown migration. Part 2 was the deploy workflow. This is the one I’ve been quietly the most excited about: making each site legible to AI agents, not just to browsers.
The premise of all this is unfashionable to argue against in 2026, but I’ll state it plainly anyway: the next set of users for my marketing sites are agents. Sometimes they’re acting on behalf of a person evaluating Stoked or Causey. Sometimes they’re answering a question their human asked, where the answer happens to be on my site. Sometimes they’re a search index that prefers structured data over scraped HTML. None of those readers want me to ship them a 200kb HTML page wrapped in JS and ad scripts. They want the content, in a form they can use, with a clear contract about what they can and can’t do with it.
The good news is that there’s now a small but real stack of standards for advertising exactly that. Most of them are emerging — half-spec, half-convention — but they’re all cheap to implement, and they compose nicely. I scored each of my four sites — rickychilcott.com, Rakefire, Causey, and get.stokedhq.com — on isitagentready.com, and over the past few months I’ve worked them up the leaderboard. Stoked is the furthest along; the others are catching up. Here’s what’s in the stack.
The pieces, end to end
There are six artifacts that matter, and one piece of edge configuration that ties them together.
robots.txt with a Content-Signal line. Plain old robots.txt, plus an emerging convention that lets you say what AI systems are allowed to do with your content — train on it, surface it in search, use it as input to a model session. Mine looks like this:
User-agent: *
Allow: /
Content-Signal: ai-train=yes, search=yes, ai-input=yes
Sitemap: https://www.rickychilcott.com/sitemap.xml
It’s a one-liner and it sets the default for everything else.
llms.txt. A flat, markdown-friendly index of the site’s content — pages, posts, projects, and links to agent skills — designed to be what an LLM reads when it lands on your domain and wants to understand what’s here. The llmstxt.org convention. I generate mine with an ERB template that walks the Bridgetown collections and emits a sectioned outline (Core pages, Projects, Agent skills, Blog, Optional). Per-resource opt-out is llms_txt: false in front matter. The whole thing is one file and rebuilds on every deploy.
Markdown twins of every page. This is the part I think is most underrated. For every .html Bridgetown writes, a builder writes a sibling .md file. For blog posts, the twin is the original markdown source (cleaner than round-tripping). For everything else — landing pages, projects, the resume — the builder strips nav and footer with Nokogiri, then runs the body through reverse_markdown to produce a clean markdown rendering. Each page advertises its twin in the head:
<link rel="alternate" type="text/markdown" href="/path/index.md" />
So an agent that wants markdown can either negotiate with Accept: text/markdown (more on that in a minute) or just pull the .md URL directly. Either way, it gets a version of the page that’s not buried in layout markup.
/.well-known/agent-skills/index.json. This one is my favorite. The Agent Skills Discovery RFC defines a /.well-known/agent-skills/ directory containing an index.json and a set of SKILL.md files. Each skill is a markdown document that tells an agent what the site can do for it and how. On rickychilcott.com I have four skills: about, hire-collaborate, blog-search, and projects-portfolio. The index lists them with names, descriptions, URLs, and SHA-256 digests so a client can verify integrity.
The skills themselves are authored by hand at the repo root in agent_skills/<name>/SKILL.md. A small Bridgetown builder copies them to the output directory at build time, hashes each one, and writes the index:
class Builders::AgentSkills < SiteBuilder
def build
hook :site, :post_write do
skills = (site.data["agent_skills"]["skills"] || []).map do |s|
bytes = File.binread("agent_skills/#{s["name"]}/SKILL.md")
File.binwrite(".well-known/agent-skills/#{s["name"]}/SKILL.md", bytes)
{
"name" => s["name"],
"type" => "skill-md",
"description" => s["description"],
"url" => "/.well-known/agent-skills/#{s["name"]}/SKILL.md",
"digest" => "sha256:#{Digest::SHA256.hexdigest(bytes)}"
}
end
File.write(
".well-known/agent-skills/index.json",
JSON.pretty_generate({"$schema" => SCHEMA_URL, "skills" => skills}) + "\n"
)
end
end
end
The skills themselves read like the briefest possible operating manual. My about skill is a 50-line markdown file that tells an agent who I am, what stack I use, what I write about, and where to find more. My hire-collaborate skill explains the right way to reach me for work. The blog-search skill describes how to find posts on a topic. The projects-portfolio skill summarizes what I’ve built. None of them are clever. They’re just the FAQ I’ve answered a hundred times, written down in one place, in a format an agent can read.
/.well-known/api-catalog. The RFC 9727 linkset for advertising APIs. Mine is data-driven from _data/api_catalog.yml if present, with a sensible default that points at the agent-skills index:
{
"linkset": [
{
"anchor": "https://www.rickychilcott.com/",
"service-doc": [
{
"href": "https://www.rickychilcott.com/.well-known/agent-skills/index.json",
"type": "application/json"
}
]
}
]
}
Small file, small builder, and another standard place an agent can land.
WebMCP. This is the one I only have on Stoked, and the reason Stoked scores higher than the others. WebMCP is a draft community-group spec that lets a page register tools with a browser-side navigator.modelContext, so a model running in the browser (or a browser-controlling agent) can call them. On Stoked, I expose six:
get_pricing— returns the current pricing tiers, overage rates, annual discount, and onboarding costs.list_features— returns product features, optionally filtered to a category.find_vertical_page— given a vertical slug likee-bikesortiny-houses, returns the right landing page URL and a summary.search_blog— searches blog posts by query and/or category.book_demo— opens the demo booking page, optionally prefilled with name, email, company, or vertical.contact_sales— opens the contact form, optionally prefilled.
The first four are read-only — annotated { readOnlyHint: true } so a respectful agent knows it can call them without asking. book_demo and contact_sales actually navigate the user, so they’re treated as actions. The whole thing is generated at build time from a YAML data file plus the blog index, baked into a single webmcp.js, and loaded on every page.
I didn’t add WebMCP to the personal site or Rakefire because they don’t have the surface area that makes it valuable — there’s no demo flow, no pricing API, no contact form worth calling. Causey’s marketing site is the next candidate. The pattern transfers cleanly.
And then the edge
All of those artifacts ship from the build. But two of them — the markdown content negotiation and the discovery Link header on / — actually require the edge to participate. That’s where Cloudflare Transform Rules come in.
I have a script that applies four rules to a zone. One in the request phase, three in the response-headers phase:
- URL rewrite (request phase). When a request comes in with
Accept: text/markdownand the path doesn’t already end in.md, rewrite the URL to/<path>/index.md. This is what turns the markdown twins into a real content-negotiation story. - Markdown Content-Type (response phase). Any response whose path ends in
.mdgetsContent-Type: text/markdown; charset=utf-8. GitHub Pages serves them astext/plainby default, which technically works but isn’t right. - Discovery Link header on
/(response phase). The homepage gets aLink:header pointing at/.well-known/api-catalog(withrel="api-catalog") and/.well-known/agent-skills/index.json(withrel="https://schemas.agentskills.io/discovery/0.2.0/"). An agent that does aHEAD /finds everything else from there. - API catalog Content-Type (response phase).
/.well-known/api-catalogis served asapplication/linkset+jsonrather than the default JSON.
The script reads CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID from the environment, GETs the existing entrypoint ruleset for each phase, removes any rule whose description matches one of the four it manages, and PUTs the merged set back. So it’s idempotent — running it twice doesn’t double up — and it preserves any unrelated rules you already had.
The full script is here as a gist. The gist of the gist is this one block, which builds the request-phase rule:
REQUEST_RULES=$(jq -n --arg host "$HOST" '[
{
description: "Markdown for Agents — URL rewrite",
expression: ("(http.host eq \"" + $host + "\" and any(http.request.headers[\"accept\"][*] contains \"text/markdown\") and not ends_with(http.request.uri.path, \".md\"))"),
action: "rewrite",
action_parameters: {
uri: { path: { expression: "concat(http.request.uri.path, \"index.md\")" } }
},
enabled: true
}
]')
Verifying it works is three curls:
curl -sI -H 'Accept: text/markdown' https://<host>/ | grep -i content-type
curl -sI https://<host>/ | grep -i ^link
curl -sI https://<host>/.well-known/api-catalog | grep -i content-type
The honest scorecard
isitagentready.com scores sites across five categories: Discoverability, Content Accessibility, Bot Access Control, Protocol Discovery, and Commerce. Here’s where each of mine actually sits today.
Stoked is the furthest along. Cloudflare Transform Rules are applied. WebMCP is live with six tools. Agent skills, llms.txt, markdown twins, Content-Signal, the api-catalog — all there. The only categories where Stoked doesn’t score full marks are the ones it can’t, structurally: there’s no e-commerce flow yet, so the Commerce category (x402, MPP, UCP, ACP) is empty by definition, and there’s no real MCP server endpoint — WebMCP is the in-page equivalent, but a hosted MCP server is a separate thing. Within what it can do, it’s done.
rickychilcott.com — this site, the one you’re reading — has the build-time artifacts in place as of last week’s deploy: robots Content-Signal, four agent skills, llms.txt, markdown twins, api-catalog, the per-page <link rel="alternate">. The Cloudflare Transform Rules are pending. Once those land, the score should match Stoked minus WebMCP and Commerce.
Causey (the marketing site) and Rakefire are at roughly the same level as the personal site — agent-ready at the build, edge wiring still to come. WebMCP for Causey is on the short list, since unlike the personal sites it has a real pricing API and demo flow worth exposing.
The thing I want to be honest about is that the score isn’t the goal. The goal is that an agent visiting any of these sites can answer questions about my work, find the right page for the question it’s been asked, and — on Stoked — actually invoke a tool to make something happen. The score is a useful proxy for whether I’ve done the small implementation steps; the real test is whether agents in the wild start using any of it. That part takes longer than three blog posts to find out.
Closing
The thing that surprised me most, doing this work, was how cheap it all was. The Bridgetown plugin model meant every artifact was a 30–80 line builder. The Cloudflare script was an afternoon. The hardest part was reading the specs and figuring out what my sites actually had to say — which is a writing problem, not a code problem.
If you run a static site and you’ve been wondering whether any of this is worth doing yet: it is, and it’s not as much work as you’d think. Start with llms.txt and a Content-Signal line in robots.txt. Add the markdown twins. Write three or four SKILL.md files about who you are and what you do. Wire up Cloudflare or your CDN of choice for the negotiation piece. You’ll have done in a weekend what would have taken a small team six months to spec from scratch.
That’s the third post. Thanks for reading the series. The next set of posts on this blog will probably go back to Rails, but I’ll come back to the agentic-web stuff as it evolves — there’s a lot more to write about (real MCP servers, the commerce specs, the long story of how Accept headers might finally become useful again). For now, four sites, one stack, and a stack of /.well-known/ files that didn’t exist three months ago. That feels like enough.
Series: Part 1 — Why I moved from Jekyll to Bridgetown · Part 2 — A shared GitHub Actions workflow · Part 3 (this post).