Routes and sources
Map page paths to literal, local, remote and parameterized markdown sources.
routes maps a page path to a source. Every path must start with /, otherwise agentside() throws when it is created.
import { agentside, remote } from "@usenotra/agentside";
const twins = agentside({
routes: {
"/": "# Acme\n\nAcme sends invoices your customers actually pay.",
"/pricing": () => renderPricingMarkdown(),
"/changelog": async () => (await fetchChangelog()).markdown,
"/blog/[slug]": ({ params }) => getPost(params.slug)?.markdown ?? null,
"/docs/[...path]": remote(({ params }) => `https://cms.acme.dev/docs/${params.path}.md`),
"/terms": remote("https://cms.acme.dev/terms.md", { headers: { authorization: `Bearer ${token}` } }),
},
});
Literal strings
A string is served as is. It is the fastest source and a good fit for pages whose copy rarely changes.
Local functions
A function receives a RenderContext and returns markdown, null or undefined, either directly or as a promise.
interface RenderContext {
readonly path: string;
readonly params: Readonly<Record<string, string>>;
readonly request: Request;
readonly agent: Agent | null;
}
path is the page path without .md or a trailing slash. agent is the detected AI agent, or null when the request came from a client that asked for markdown without a known user agent. A thrown error or a rejected promise becomes a RenderFailed error.
Twins are cached per path and variant, not per request. If a function reads request or agent to build different output, give each output its own variant or turn the cache off.
Returning null
Return null or undefined when a page has no twin. Agentside treats that as NotFound: a negotiated request falls through to your HTML page and an explicit .md request gets a markdown 404.
"/blog/[slug]": ({ params }) => {
const post = getPost(params.slug);
return post?.published ? post.markdown : null;
},
Remote sources
remote(url, options) fetches markdown over HTTP. The URL can be a string or a function of the render context. Requests send Accept: text/markdown, text/plain;q=0.9 plus any headers you pass.
| Upstream response | Result |
|---|---|
| 2xx | The response body is the twin. |
| 404 | NotFound, same as returning null. |
| Other 4xx | RemoteFailed with status, not retried. |
| 5xx or network error | RemoteFailed, retried with exponential backoff. |
Slower than timeoutMs |
SourceTimeout. |
See Error handling for retries and timeouts.
Parameters
Patterns use bracket segments.
| Pattern | Matches | params |
|---|---|---|
/blog/[slug] |
/blog/hello |
{ slug: "hello" } |
/docs/[...path] |
/docs/guides/setup |
{ path: "guides/setup" } |
/[lang]/docs/[...path] |
/de/docs/api |
{ lang: "de", path: "api" } |
A [name] segment matches exactly one path segment and [...name] matches one or more. Values are URL decoded. Exact routes win over parameterized ones and parameterized routes are tried in the order you declared them.
Paths and URLs
Agentside normalizes the request path before matching. /pricing, /pricing/ and /pricing.md all resolve to the /pricing route, and / is served at /index.md.
Unicode normalization
By default Agentside replaces typographic characters that waste tokens or confuse tokenizers, such as zero-width spaces, smart quotes, non-breaking spaces and currency symbols, with plain ASCII equivalents. Set normalize: false to serve your markdown byte for byte. The same function is exported as normalizeUnicode.