Markdown endpoints
Let agents retrieve a clean Markdown representation of each documentation page.
This functionality is already configured in the starter templates. Follow the complete setup in the Quickstart.
Every documentation page has a public Markdown mirror. The page at
/guides/installation is available as /guides/installation.md; the home page
is available as /index.md. These endpoints return the generated,
frontmatter-free Markdown source with:
content-type: text/markdown; charset=utf-8This is the preferred retrieval surface for AI agents. It preserves headings, lists, links, and code examples while avoiding navigation, search controls, and other HTML that would be irrelevant to an answer.
Public URLs, internal routing
The *.md URL is part of your public contract. Each framework routes it to an
internal handler differently:
- React Router redirects the public URL through middleware to a resource route.
- Next.js rewrites it through
proxy.tsto a Route Handler. - Astro generates a static
[...,slug].mdendpoint.
The implementation details stay internal; agents always request the same
stable public URL. A request for an unknown page returns 404 Not Found rather
than falling through to the documentation shell.
Keep the source of truth singular
Use markdownForPage() with the generated page registry rather than maintaining
separate AI-facing files. That ensures the UI, search index, llms.txt,
llms-full.txt, and Markdown mirrors all represent the same release of the
documentation.
The Markdown is public content. Do not put secrets, private runbooks, or instructions that should not be exposed to browsers into the configured documentation directory.
React Router
Create a project
The React Router template generated by create-heyo-docs already includes the
public Markdown redirect, its internal resource route, and the route
registration. An agent can request /getting-started.md immediately after the
project is deployed.
bun create @heyo-sh/heyo-docs my-docs --template react-routerManual, minimal configuration
Add an internal resource route before the documentation catch-all. It resolves the requested Markdown pathname against the server-side virtual registry:
route("__heyo-docs/markdown/*", "routes/markdown.ts"),
route("*", "routes/docs.tsx"),import { markdownForPage, pathnameFromMarkdownPath } from "@heyo-sh/heyo-docs";
import type { LoaderFunctionArgs } from "react-router";
import { pages } from "virtual:heyo-docs-content/server";
export function loader({ params }: LoaderFunctionArgs) {
const pathname = pathnameFromMarkdownPath(`/${params["*"] ?? ""}`);
const page = pathname
? pages.find((candidate) => candidate.slug === pathname)
: undefined;
if (!page)
return new Response("Not Found", {
status: 404,
headers: { "content-type": "text/plain; charset=utf-8" },
});
return new Response(markdownForPage(page), {
headers: { "content-type": "text/markdown; charset=utf-8" },
});
}Finally, redirect valid public *.md paths to the internal resource route
from app/root.tsx:
import { pathnameFromMarkdownPath } from "@heyo-sh/heyo-docs";
import type { Route } from "./+types/root";
const markdownResourcePrefix = "/__heyo-docs/markdown";
const markdownMiddleware: Route.MiddlewareFunction = async (
{ request },
next,
) => {
const url = new URL(request.url);
if (
!url.pathname.startsWith(`${markdownResourcePrefix}/`) &&
pathnameFromMarkdownPath(url.pathname) !== undefined
) {
return Response.redirect(
new URL(`${markdownResourcePrefix}${url.pathname}${url.search}`, url),
307,
);
}
return next();
};
export const middleware = [markdownMiddleware];Next.js
Create a project
The Next.js template generated by create-heyo-docs already includes proxy.ts
and its internal Markdown Route Handler. The proxy keeps *.md URLs outside
the App Router documentation catch-all.
bun create @heyo-sh/heyo-docs my-docs --template nextManual, minimal configuration
Generate the content registry before development and builds, then re-export
markdownPages from your server-only app/lib/docs.ts helper. Add the proxy:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname.endsWith(".md"))
return NextResponse.rewrite(
new URL(`/heyo-docs-internal/markdown${pathname}`, request.url),
);
return NextResponse.next();
}Then add the internal Route Handler. It returns a direct Markdown response or
a 404, never the rendered documentation shell:
import {
markdownForPage,
pathnameFromMarkdownPath,
} from "@heyo-sh/heyo-docs/node";
import { markdownPages } from "../../../lib/docs";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ slug?: string[] }> },
) {
const { slug } = await params;
const pathname = pathnameFromMarkdownPath(`/${slug?.join("/") ?? ""}`);
const page = pathname
? markdownPages.find((candidate) => candidate.slug === pathname)
: undefined;
if (!page) return new Response("Not Found", { status: 404 });
return new Response(markdownForPage(page), {
headers: { "content-type": "text/markdown; charset=utf-8" },
});
}Astro
Create a project
The Astro template generated by create-heyo-docs includes a static dynamic
route for every Markdown mirror. Astro emits these files during astro build,
without a runtime rewrite or middleware.
bun create @heyo-sh/heyo-docs my-docs --template astroManual, minimal configuration
After adding heyoDocsAstro({ config }) to astro.config.ts, add a dynamic
API route that enumerates the generated page registry:
import type { APIRoute } from "astro";
import { markdownForPage, pathnameFromMarkdownPath } from "@heyo-sh/heyo-docs";
import { pages } from "virtual:heyo-docs-content/server";
export function getStaticPaths() {
return pages.map((page) => ({
params: { slug: page.slug === "/" ? "index" : page.slug.slice(1) },
}));
}
export const GET: APIRoute = ({ params }) => {
const pathname = pathnameFromMarkdownPath(`/${params.slug ?? ""}.md`);
const page = pathname
? pages.find((candidate) => candidate.slug === pathname)
: undefined;
if (!page)
return new Response("Not Found", {
status: 404,
headers: { "content-type": "text/plain; charset=utf-8" },
});
return new Response(markdownForPage(page), {
headers: { "content-type": "text/markdown; charset=utf-8" },
});
};