Redocly publishes a plain Markdown copy of every page for AI clients. The .md version of a page, the llms.txt file, AI search, and the MCP server all read this copy instead of the rendered HTML.
Custom Markdoc tags render through React components, so Redocly can't convert their output to Markdown on its own. By default, the Markdown copy keeps the tag titles, taken from the title, name, or label attributes, followed by the tag bodies. The Markdown copy drops all other attributes.
This default behavior causes information loss in many tags. A link tag loses its URL, an image tag loses the image, and a tag that loads data in the browser loses the data. To control what the Markdown copy contains, add renderForLlms to the tag schema.
Make sure you have the following:
- a custom Markdoc tag in
@theme/markdoc/schema.ts - the tag schema typed as
MarkdocTagSchemafrom@redocly/theme/markdoc/tags/types, which definesrenderForLlms
renderForLlms is a function in the tag schema. When Redocly builds the Markdown copy, it calls renderForLlms for each use of the tag, on Markdown pages and in API descriptions.
The function receives the Markdoc AST node for the tag and a context object. It returns a Markdown string:
- The string replaces the default output for the tag.
- An empty string leaves the tag and its body out of the Markdown copy.
The tag body is included only when you call getBody().
Redocly publishes the string that renderForLlms returns to LLMs and to anyone who requests the Markdown copy. Never include secrets, API keys, tokens, or other sensitive information.
node.attributes holds the tag's attribute values:
- Literal values, such as
title="Refunds", arrive as written. - Attributes set with
$frontmatteror$envvariables arrive with the variable's value, the same value the tag's component receives.$envholds only environment variables whose names start withPUBLIC_. - Other variables, such as
$rbacor$user, depend on the reader and are unknown when Redocly builds the Markdown copy. They arrive as Markdoc variable objects, not values. - Function calls, such as
default($frontmatter.title, "Overview"), also arrive unresolved.
Read only literal, $frontmatter, and $env attributes in renderForLlms.
The context object has the following fields:
| Field | Type | Description |
|---|---|---|
| getBody | () => string | Returns the tag body as Markdown. |
| locale | string | Code of the locale the page is built for, such as en-US. Redocly takes the locale from the page URL, so each localized copy of a page gets its own locale. Use it to format numbers and dates with Intl. |
| translate | (key: string, fallback?: string) => string | Returns the value of a translation key from the current locale's translations.yaml file, or fallback when the key has no translation. |
The following examples come from patterns that documentation teams use in their own projects. Each example demonstrates the renderForLlms function and the Markdown it produces.
A source-link tag links to a file in the SDK repository, pinned to the release the docs describe. The release comes from the PUBLIC_SDK_RELEASE environment variable, so the links update when the variable changes.
Use the tag on a page as follows:
Refunds are created in {% source-link path="src/payments/refunds.ts" release=$env.PUBLIC_SDK_RELEASE /%}.
The tag has no title, name, or label attribute, so the default output drops the link. Build the same URL in renderForLlms:
import type { MarkdocTagSchema } from '@redocly/theme/markdoc/tags/types';
const SDK_REPOSITORY = 'https://github.com/museum-api/sdk';
export const tags: Record<string, MarkdocTagSchema> = {
'source-link': {
attributes: {
path: { type: String, required: true },
release: { type: String, default: 'main' },
},
render: 'SourceLink',
selfClosing: true,
renderForLlms: (node) => {
const { path, release = 'main' } = node.attributes;
return `[${path}](${SDK_REPOSITORY}/blob/${release}/${path})`;
},
},
};
With PUBLIC_SDK_RELEASE=v2.4.0, the Markdown copy contains:
Refunds are created in [src/payments/refunds.ts](https://github.com/museum-api/sdk/blob/v2.4.0/src/payments/refunds.ts).
A responsive-image tag displays one image on wide screens and another on narrow ones. It has an alt attribute but no title, so by default nothing tells an LLM that the page has an image. Use the tag on a page as follows:
{% responsive-image alt="Ticket purchase flow" wide="/images/ticket-flow-wide.png" narrow="/images/ticket-flow-narrow.png" /%}
Return a Markdown image that points to the wide version:
import type { MarkdocTagSchema } from '@redocly/theme/markdoc/tags/types';
export const tags: Record<string, MarkdocTagSchema> = {
'responsive-image': {
attributes: {
alt: { type: String, required: true },
wide: { type: String, required: true },
narrow: { type: String, required: true },
},
render: 'ResponsiveImage',
selfClosing: true,
renderForLlms: (node) => ``,
},
};
The Markdown copy contains the following image:

An exhibit-status tag displays which exhibits are open. Its component fetches the live status in the reader's browser, so the Markdown copy has no data to include. Keep a snapshot of the data in the project and render it as a table. For example, a scheduled job can update the snapshot. State how old the data is and where to find the live status. Add a fallback sentence for when the snapshot is missing.
The snapshot is a JSON file in the theme folder:
{
"updatedAt": "2026-10-01",
"exhibits": [
{ "name": "Dinosaurs", "gallery": "North wing", "status": "Open" },
{ "name": "Ancient Egypt", "gallery": "East wing", "status": "Closed for renovation" }
]
}
Render the snapshot in renderForLlms:
import type { MarkdocTagSchema } from '@redocly/theme/markdoc/tags/types';
import snapshot from '../data/exhibit-status.json';
const LIVE_STATUS_URL = 'https://museum.example.com/exhibits';
export const tags: Record<string, MarkdocTagSchema> = {
'exhibit-status': {
render: 'ExhibitStatus',
selfClosing: true,
renderForLlms: () => {
if (!snapshot.exhibits?.length) {
return `Exhibit status is unavailable. See the live status at ${LIVE_STATUS_URL}.`;
}
return [
`Exhibit status as of ${snapshot.updatedAt}. See the live status at ${LIVE_STATUS_URL}.`,
'',
'| Exhibit | Gallery | Status |',
'|:--------|:--------|:-------|',
...snapshot.exhibits.map((exhibit) => `| ${exhibit.name} | ${exhibit.gallery} | ${exhibit.status} |`),
].join('\n');
},
},
};
The Markdown copy contains:
Exhibit status as of 2026-10-01. See the live status at https://museum.example.com/exhibits.
| Exhibit | Gallery | Status |
|:--------|:--------|:-------|
| Dinosaurs | North wing | Open |
| Ancient Egypt | East wing | Closed for renovation |
The Render data as a table example adds sentences that aren't part of the page content. On a localized copy of the page, they stay in English unless you translate them. renderForLlms runs once for each localized copy of a page. Use translate to look up the sentences in the current locale's translations.yaml file, and locale to format the date:
renderForLlms: (_node, { locale, translate }) => {
const liveStatus = `${translate('exhibits.liveStatus', 'See the live status at')} ${LIVE_STATUS_URL}.`;
if (!snapshot.exhibits?.length) {
return `${translate('exhibits.unavailable', 'Exhibit status is unavailable.')} ${liveStatus}`;
}
const updatedAt = new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeZone: 'UTC' }).format(
new Date(snapshot.updatedAt),
);
return [
`${translate('exhibits.asOf', 'Exhibit status as of')} ${updatedAt}. ${liveStatus}`,
'',
`| ${translate('exhibits.name', 'Exhibit')} | ${translate('exhibits.gallery', 'Gallery')} | ${translate('exhibits.status', 'Status')} |`,
'|:--------|:--------|:-------|',
...snapshot.exhibits.map((exhibit) => `| ${exhibit.name} | ${exhibit.gallery} | ${exhibit.status} |`),
].join('\n');
},
Add the keys to the translations.yaml file of each locale you support:
exhibits.unavailable: El estado de las exposiciones no está disponible.
exhibits.asOf: Estado de las exposiciones a fecha de
exhibits.liveStatus: Consulta el estado actual en
exhibits.name: Exposición
exhibits.gallery: Galería
exhibits.status: Estado
The default locale has no translations for these keys, so translate returns the fallbacks. The Spanish copy of the page gets the translated text and a Spanish date:
Estado de las exposiciones a fecha de 1 de octubre de 2026. Consulta el estado actual en https://museum.example.com/exhibits.
| Exposición | Galería | Estado |
|:--------|:--------|:-------|
| Dinosaurs | North wing | Open |
| Ancient Egypt | East wing | Closed for renovation |
translate looks up only the keys in your translations.yaml files. Data from outside the page, such as the exhibit names, stays in its original language. To translate that data too, keep a snapshot for each locale.
- Build Markdoc tags - Create the custom tags and components that
renderForLlmsapplies to - Make docs AI ready - Where the Markdown copy of your pages is used:
llms.txt, AI search, and the MCP server - Visibility tag - Show content only to readers or only to AI tools, without writing code
- Markdoc variables - Variables available in Markdoc, including
$frontmatterand$env - Localize UI labels - Add translation keys and translations for each locale