Skip to content
Last updated

Customize how LLMs see your tags

Plans:ProScaleEnterprise

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.

Before you begin

Make sure you have the following:

  • a custom Markdoc tag in @theme/markdoc/schema.ts
  • the tag schema typed as MarkdocTagSchema from @redocly/theme/markdoc/tags/types, which defines renderForLlms

Add renderForLlms to a tag

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().

Security considerations

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.

Attribute values

node.attributes holds the tag's attribute values:

  • Literal values, such as title="Refunds", arrive as written.
  • Attributes set with $frontmatter or $env variables arrive with the variable's value, the same value the tag's component receives. $env holds only environment variables whose names start with PUBLIC_.
  • Other variables, such as $rbac or $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.

Context

The context object has the following fields:

FieldTypeDescription
getBody() => stringReturns the tag body as Markdown.
localestringCode 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) => stringReturns the value of a translation key from the current locale's translations.yaml file, or fallback when the key has no translation.

Examples

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:

@theme/markdoc/schema.ts
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).

Return an image

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:

@theme/markdoc/schema.ts
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) => `![${node.attributes.alt}](${node.attributes.wide})`,
  },
};

The Markdown copy contains the following image:

![Ticket purchase flow](/images/ticket-flow-wide.png)

Render data as a table

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:

@theme/data/exhibit-status.json
{
  "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:

@theme/markdoc/schema.ts
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 |

Localize the text you add

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:

@theme/markdoc/schema.ts
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:

@l10n/es-ES/translations.yaml
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.

Resources

  • Build Markdoc tags - Create the custom tags and components that renderForLlms applies 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 $frontmatter and $env
  • Localize UI labels - Add translation keys and translations for each locale