Agents

What a build publishes for AI agents, where it lives and how servers hand it out.

Language models read documentation too: coding agents, chat assistants that fetch a page for a user, crawlers that index it. HTML is an expensive way to give them the text. Every Pholio build therefore also publishes the documentation as Markdown, indexes that list every page, a skill that explains how to use them, and the headers that point agents there.

All of it comes from the same page tree as the sidebar and the search index: the order from meta.json, titles, descriptions and keywords from the frontmatter, drafts left out. There is nothing to maintain by hand, and nothing is written by a model.

The files

Paths are relative to base_path. A site served under /manuals/ publishes /manuals/llms.txt.

FileContentsNeeds
<page>.md, index.mdThe Markdown twin of every page and of the start page
llms.txt, .well-known/llms.txtIndex of all pages in navigation order
_llms/**.mdSection indexes, only when llms.txt would grow past 100,000 characters
llms-full.txt, .well-known/llms-full.txtEvery page in full
skill.mdA skill describing the documentation and how to read it
.well-known/agent-skills/index.jsonAgent Skills discovery index 0.2.0, with digests
.well-known/skills/index.jsonThe same skills in the 0.1.0 format
.well-known/agent-card.jsonA2A agent card, protocol 0.3site.url
robots.txtAllows every crawler and names the sitemap
sitemap.xmlEvery indexable pagesite.url
_headersDiscovery headers for Cloudflare Pages and Netlify

Every HTML page also gets JSON-LD (WebSite, TechArticle, BreadcrumbList), a <link rel="alternate" type="text/markdown"> to its twin and the page actions next to its title.

Set site.url to the origin the site is served from. Without it, the files use base-path URLs (/manuals/guide/install.md), and the sitemap and the agent card, which need absolute URLs, are skipped with a warning.

'site' => [
    'url' => 'https://docs.example.org',
    'description' => 'How to install, configure and run Example.',
],

Markdown twins

A page at /guide/install has its twin at /guide/install.md; the start page's is index.md. The twin is the page's own Markdown, cleaned up:

> Documentation index: https://docs.example.org/llms.txt, a list of every page in this documentation.

# Installation

> Install Example with your package manager of choice.

## Requirements

…

## Related topics

- [Configuration](https://docs.example.org/guide/configuration.md)
- Previous: [Overview](https://docs.example.org/guide.md)
- Next: [Configuration](https://docs.example.org/guide/configuration.md)

The frontmatter becomes the title and the description. Component tags become what they mean in plain Markdown:

TagIn the twin
CalloutA blockquote starting with its type and title: > **Warning:** Careful
Tabs, Accordions, code tabsOne heading per tab or item, a level below the surrounding heading
CardsA list of links with their descriptions
StepsNumbered headings, or an ordered list when a step has no heading
FilesA nested list, folders with a trailing slash
TypeTableA list of properties with type, default and description
Screenshot, ImageZoom, imagesImages with their alt text and absolute URLs
BannerA blockquote
InlineTOCLeft out

Links to other pages point at their twins. Notation comments in code blocks (// [!code highlight]) are removed.

The first line only states where the index is. Pholio writes nothing into a twin that addresses the reader with a command, because assistants that read fetched pages treat instructions in them as a possible prompt injection and may refuse to follow them.

Language

The headings and fixed sentences Pholio writes into these files follow language: the index line, "Related topics" with its previous and next links, callout labels, the type table words, the section labels of llms.txt, the Source: lines of llms-full.txt, the whole generated skill.md and the agent card's fallback description. A German site gets "Verwandte Themen", "Quelle" and "Hinweise für Agenten". Field names that a specification defines, such as name and description in skill.md or the keys of the agent card, stay as they are, and so does the ## Optional heading of llms.txt, whose name has a meaning in the llms.txt format.

llms.txt

llms.txt follows llmstxt.org: the site title, the site description as a blockquote, a ## Notes for agents section when agents.instructions is set, one section per top-level folder with a line per page, and the external navigation links under ## Optional.

# Example

> How to install, configure and run Example.

## Guide

- [Installation](https://docs.example.org/guide/install.md): Install Example with your package manager of choice.

The summary is the page's description, or its first paragraph cut at 300 characters. When the file would exceed 100,000 characters, llms.txt lists section indexes below _llms/ instead, with their page counts and a sentence saying that together they list every page. A section index that is still too long lists its subfolders' indexes, and a folder with too many pages is split into parts. No page is ever left out.

llms-full.txt has every listed page in navigation order: its title, a Source: line with the page URL, the description and the whole twin. It has no size limit.

Skills and the agent card

skill.md is a SKILL.md generated from the configuration and the content: what the documentation covers, section by section, how to fetch llms.txt and the Markdown pages, and how to search them. The same file is published below .well-known/agent-skills/<name>/ and listed in the discovery index with its SHA-256 digest, so agents can tell when it changed.

To write the skill yourself, put skill.md next to pholio.config.php. Further skills go into skills/<name>/SKILL.md; a skill with the generated skill's name replaces it. Each file needs a one-line name and description in its frontmatter.

.well-known/agent-card.json describes the site as an A2A agent with Markdown and plain text as input and output modes and the skills above. The site is static, so the card advertises no endpoint beyond the documentation.

Choosing what agents see

  • Drafts (file names starting with _) are never listed.
  • noindex: true in the frontmatter keeps a page out of llms.txt, llms-full.txt, skill.md and the sitemap, and adds <meta name="robots" content="noindex">. The page and its twin are still built.
  • agents.exclude takes globs on page URLs and content files, for example ['/guide/internal/*', 'drafts/*.md']. Matching pages stay out of llms.txt, llms-full.txt and skill.md.
  • Each file can be switched off in the agents block, and agents.enabled => false switches off all of them. See Configuration.
  • A robots.txt that a copy directory already provides is left alone.

Serving

Agents find the files through headers and ask for Markdown with the Accept header. On every response, 404s included, Pholio's server configurations send:

  • Link with rel="llms-txt", rel="llms-full-txt", rel="agent-card" and rel="agent-skills", for the files the build wrote
  • X-Llms-Txt with the URL of llms.txt
  • Vary: Accept, User-Agent

A page URL is answered with its twin for Accept: text/markdown and for the user agents of AI assistants fetching a page for a user (Claude-User, ChatGPT-User, OAI-SearchBot, PerplexityBot, Perplexity-User, Google-Agent, MistralAI-User, DuckAssistBot, cohere-ai), and as text/plain for Accept: text/plain. Browsers get the HTML page as before.

OpenAI's agents (ChatGPT-User, OAI-SearchBot, GPTBot) reject the text/markdown content type, so they get every twin as text/plain; charset=utf-8, both for a page's .md URL and for a negotiated page. Everyone else gets text/markdown. The page menu's "Open in ChatGPT" prompt names the page's own URL and mentions its .md version; "Open in Claude" names the .md URL.

Only the Markdown files the build writes are served: a page's twin (x.md next to x/index.html, index.md next to index.html), skill.md, the _llms/ indexes and the files below .well-known/. Any other .md file below the site, such as a note left in the output directory, answers 404.

Apache. The generated .htaccess does all of this with mod_rewrite and mod_headers.

pholio dev behaves like production. Try it:

curl -H 'Accept: text/markdown' http://127.0.0.1:8080/guide/install
curl -A 'Claude-User' http://127.0.0.1:8080/

Cloudflare Pages, Netlify, Workers static assets read the _headers file, so the headers work there. Choosing the twin per request needs a small edge function: examples/cloudflare-worker in the repository is a ready Worker for Cloudflare, and the rules above are all a Netlify Edge Function has to reproduce. Without one, agents still find everything through llms.txt and the .md URLs.

robots.txt only counts at the root of a host. When the site lives below a base path, the generated robots.txt below it is ignored by crawlers: merge its lines into the robots.txt at the host root.

Checking a site

verify/agent-score.mjs runs the checks of Mintlify's mint score against any running site: llms.txt and llms-full.txt (format, size, links), skill.md and its digests, content negotiation for Markdown and plain text, robots.txt, the sitemap, JSON-LD, the Link header on a page and on a 404, and response time.

node verify/agent-score.mjs --base https://docs.example.org
node verify/agent-score.mjs --demo --min-score 100

node verify/run.mjs runs the second line: the demo served by the pholio dev router has to score 100.

An MCP server

Pholio's output is static, so it ships no MCP server. A host that runs PHP could add one later without touching the build: a small endpoint that answers search from search-index.json, which already ranks pages and sections, and fetch from the Markdown twins. It would be listed in the agent card and the Link header next to the files above.

On this page