Act Accordingly: Making Next.js Apps Readable to AI Agents
Agents navigate the web by trial and error. @affordances/md gives every Next.js page a Markdown version, generated from what your app already renders.

tl;dr
- AI agents use most websites by trial and error, because interfaces built for people don't tell them what's on the page.
- @affordances/md is an open-source Next.js plugin that serves every page as Markdown, generated from the HTML your app already renders.
- Agents request it at /page.md or with Accept: text/markdown, and find it through a Link header.
- It respects authentication and caching, supports llms.txt, and lets you tune the output with a few React components.
- Next, we're researching a protocol that lets agents act on pages, not just read them. Early benchmarks show 8.4× fewer tokens than a screenshot-based agent.
The Norman Door Problem of the Agentic Web
To agents today, every website is like a Norman door.
A Norman door is any door that is confusing or difficult to use because its design gives the wrong signals about how it works (such as having a pull handle on a door that must be pushed).
An agent can see the "Add to cart" button. It can't see that it needs a size first, that size 11 is sold out, or what changes after it clicks. So it pushes, checks, and tries again.

Affordances grant an agent what a person gets for free: what's here, what it can do, what it needs, and what changed.
Affordances
The web doesn't need to be rebuilt for agents. It needs a way to expose the information and capabilities applications already have, in a form agents can naturally understand.
That's what we're building with Affordances. Rather than requiring agents to reverse-engineer interfaces designed for humans, we want applications to communicate their content and capabilities directly.
There are two sides to this problem: understanding what's there, and knowing what to do with it. We're starting with the former.
Making the web readable
Markdown is already a natural format for language models. It's structured, lightweight, and strips away much of the presentation overhead of HTML.
Projects like llms.txt and other Markdown-for-agents implementations are helping move the web in this direction. But there's still a gap between making static content available as Markdown and serving an accurate representation of a dynamic application.
Some approaches require maintaining separate Markdown files, while others convert HTML without accounting for how the application renders, caches, or protects its content.
With @affordances/md, we're taking a Next.js-native approach. Existing pages get Markdown representations generated from their server-rendered content, including streamed React content.
text
/products/shoes
/products/shoes.mdAgents can also request Markdown using HTTP content negotiation:
bash
curl -H 'Accept: text/markdown' \
https://example.com/products/shoesThe plugin advertises alternate representations through HTTP Link headers, making them discoverable without requiring agents to guess URL conventions.
One application, two ways to read it.

Native to Next.js
Getting started doesn't require changing how your application is structured.
Install the plugin and run its initializer:
bash
vlt install @affordances/md
vlx -y @affordances/md initThen wrap your Next.js configuration:
ts
// next.config.ts
import { withMd } from '@affordances/md/next'
export default withMd(nextConfig)The initializer creates a route handler that connects the plugin to your application:
ts
// app/api/affordances/md/[[...path]]/route.ts
export { GET } from '@affordances/md/next'Your existing pages can now be requested as Markdown, without creating or maintaining a separate version of each one.
An interface for both
Automatic HTML conversion works well for most content, but interactive interfaces sometimes need a more meaningful text representation.
A product page might display available variants as visual swatches, for example. An agent would benefit from seeing the actual variant identifiers instead.
Affordances provides React markers for these cases:
tsx
import { AgentMain, AgentSkip, AgentText } from '@affordances/md/react'
export function ProductDetails({ product }) {
return (
<AgentMain>
<h1>{product.name}</h1>
<AgentSkip>
<ShareMenu />
</AgentSkip>
<AgentText md={product.variants.map((v) => v.sku).join(', ')}>
<VariantSwatches variants={product.variants} />
</AgentText>
</AgentMain>
)
}AgentMain identifies the relevant content, AgentSkip excludes unnecessary UI, and AgentText supplies an alternative representation for something that's primarily visual.

These markers are server components that don't change the page's layout or ship additional client-side JavaScript.
For content that loads entirely in the browser, there's also AgentSlot. It lets developers replace a specific section of the Markdown output using data from the server, while the rest of the page converts normally.
tsx
// app/package/[pkg]/page.tsx
import { AgentSlot } from '@affordances/md/react'
export default function PackagePage() {
return (
<AgentSlot name="readme">
<Readme />
</AgentSlot>
)
}The replacement lives alongside the page in markdown.ts:
ts
// app/package/[pkg]/markdown.ts
import type { Slots } from '@affordances/md/next'
import { getPackage } from '@/lib/api'
export const slots = {
readme: async ({ params }) => {
const pkg = await getPackage(decodeURIComponent(params.pkg))
return pkg.readme
},
} satisfies Slots<'/package/[pkg]'>The route parameters are typed, the rest of the page is converted automatically, and the human-facing interface stays exactly as it was.
For pages that need complete control, markdown.ts can also generate the entire Markdown response directly.
This is the developer experience we're aiming for: automatic by default, explicit when necessary, and built around the conventions Next.js developers already use.
More than conversion
A Markdown representation still needs to behave like part of the application. That means respecting authentication, caching, and the freshness of the underlying content.
@affordances/md handles streamed server rendering, keeps credentialed responses private, and supports caching for eligible public pages using Next.js cache profiles. It also supports llms.txt and llms-full.txt for content discovery.
And because Markdown is useful to people too, the plugin provides page actions for copying a page's Markdown or opening it in tools like ChatGPT and Claude.
The goal isn't simply to convert HTML into text. It's to make Markdown a dependable, discoverable representation of an existing application.
Reading is only the beginning
Making a page readable doesn't necessarily make it operable.
An agent might understand a product's description, price, and available sizes perfectly in Markdown, but still need to figure out how to add it to a cart.
We're actively exploring that next part of Affordances: an agent-native protocol for communicating what an application can do, what its actions require, and how its state changes.

Drawing inspiration from hypermedia approaches like Siren, HAL, and HATEOAS, we're investigating how applications could expose richer semantic meaning. Instead of describing an input as simply a number, for example, a protocol could communicate whether it represents a price, range, integer, or scale.
Much of our research centers on context management and token efficiency. We're experimenting with protocol designs that let an agent read only the state it needs, make targeted writes, and learn what changed, without re-reading an application's entire state on every turn.
Early results are promising. In a preliminary benchmark, three agents ran the same shopping tasks on an example store, all on Claude Opus 5.5: a browser agent reading accessibility snapshots, a screenshot agent using computer use, and an agent using an experimental version of the protocol, through the same two tools an MCP client gets from @affordances/mcp. Every agent completed every task. The Affordances agent used 8.4× fewer tokens than the screenshot agent, finished 1.9× faster than the browser agent, and cost 5.1× less per task than the screenshot agent. The gap widens as tasks get longer: editing a cart took the browser agent about 219,000 tokens, and the Affordances agent about 16,000.

These are five runs per task on a single store, so read them as a direction rather than a precise ratio. The full method and per-model results are on the benchmarks page.
The protocol is still evolving, but the direction is to make applications easier for agents to operate while keeping their capabilities understandable to humans.
For now, Markdown is a practical starting point. Developers can make their existing applications easier for agents to understand without adopting an entirely new architecture.
Act accordingly
The Norman door isn't confusing because the mechanism is complicated. It's confusing because the design doesn't communicate how it works.
Our applications already contain the content, state, and capabilities agents need. We just haven't designed good ways to communicate all of that meaning to them.
@affordances/md is our first step toward closing that gap, starting with the content applications already serve.
Affordances is an experimental, open-source project, and there's still plenty we're exploring. We're actively building, refining, and rethinking how applications can communicate with agents. If these ideas interest you, we'd love for you to explore the project, try it out, and contribute.
Our goal is simple: The web doesn't need another interface. It needs to communicate the one it already has and make the web a better place for intelligence of all kinds.
Explore Affordances, read the @affordances/md documentation, or contribute on GitHub.