LH AI Legibility
Makes LocalHero content legible to AI systems. Serves Markdown via content negotiation (Accept: text/markdown) and generates llms.txt for AI crawler discovery.
by Peter Shaw · github.com/lhero-org/lh-ai-legibility · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/lhero-org/lh-ai-legibility/archive/refs/heads/main.zipMakes LocalHero content legible to AI systems via two complementary mechanisms:
- Content negotiation — any singular post or page served with
Accept: text/markdownreturns Markdown instead of HTML, with YAML front matter for document context. - llms.txt generation — a block-editor–authored document generates
/llms.txtand/llms-full.txtendpoints per the llms.txt specification.
Part of the LocalHero open-source platform for community organisations.
Features
Markdown content negotiation
Any AI agent or crawler that sends Accept: text/markdown on a singular post or page request receives:
- A YAML front matter block with
title,url,date,modified,type, and optionallyexcerptandcategory. - The post content converted to clean Markdown.
- A
Content-Type: text/markdownresponse header. - A
Vary: Acceptheader on all HTML responses so CDN/proxy caches handle the two representations correctly.
Password-protected and private posts are excluded. Per-post opt-out is available via the lh_ai_legibility_markdown_enabled filter.
llms.txt and llms-full.txt
Activate the plugin and a draft LLMs.txt Document post is created automatically. Publish it to make the following endpoints live:
| URL | Content |
|---|---|
/llms.txt |
Site name, excerpt (as blockquote), free-form paragraphs and lists, then ## Section headings with linked items. |
/llms-full.txt |
Everything in /llms.txt, then the full Markdown content of each linked page appended inline. |
The document is edited using the block editor with a restricted inserter — only the following blocks are available:
- LLMs.txt Section (
llms-txt/section) — a## headingplus a linked list, which maps directly to a section in the spec. - Paragraph — free-form detail text.
- List — free-form bullet lists.
Only one LLMs.txt Document can be published at a time. Attempting to publish a second one saves it as a draft instead, with an admin notice.
HTML-to-Markdown converter
The LH_AI_Legibility_Converter static class handles HTML→Markdown conversion throughout the plugin. It prefers league/html-to-markdown if it is already loaded (e.g. by a Composer-managed parent plugin), and falls back to a built-in converter that handles standard WordPress block output:
- Headings (h1–h6), paragraphs, divs, line breaks
- Bold, italic, inline code, fenced code blocks
- Links, images
- Unordered and ordered lists
- Blockquotes
- Horizontal rules
Requirements
- WordPress 6.5+
- PHP 8.1+
- No required Composer dependencies (league/html-to-markdown is an optional enhancement)
Installation
- Upload the
lh-ai-legibilityfolder to/wp-content/plugins/. - Activate via Plugins › Installed Plugins.
- On activation, a draft
LLMs.txt Documentis created and a Settings › LLMs.txt menu link appears. - Edit the document, add sections and links, then publish it to bring
/llms.txtlive. - Visit
/llms.txtin your browser to verify the output.
Filters
| Filter | Arguments | Description |
|---|---|---|
lh_ai_legibility_markdown_enabled |
bool $enabled, WP_Post $post |
Return false to disable Markdown serving for a specific post or post type. |
lh_ai_legibility_markdown_output |
string $markdown, WP_Post $post |
Filter the final Markdown string before it is sent for content-negotiation responses. |
lh_ai_legibility_llms_txt_output |
string $output |
Filter the final /llms.txt output string. |
lh_ai_legibility_llms_full_output |
string $output |
Filter the final /llms-full.txt output string. |
Document structure
The LLMs.txt Document is a standard block-editor post with constrained block usage:
Post excerpt → blockquote in llms.txt
core/paragraph blocks → free-form text between blockquote and sections
core/list blocks → free-form lists in the detail zone
llms-txt/section blocks → each ## section heading + link list
└── core/list inner block → list items, format: [Label](url): Description
List item link format inside a section block:
- [Label](https://example.com): Optional description text
- [Label](https://example.com) — Also works with an em-dash separator
- [Label](https://example.com)
llms-full.txt content fetching
For internal URLs, llms-full.txt fetches content directly from the database (no HTTP round-trip) and passes it through the_content filters. For external URLs it issues a wp_remote_get request with Accept: text/markdown, text/html — so external sites that implement content negotiation themselves will return clean Markdown directly.
Caching
Both /llms.txt and Markdown responses send Cache-Control: public, max-age=3600. Rewrite rules are flushed automatically whenever the LLMs.txt Document is saved. If you use a full-page cache, exclude /llms.txt and /llms-full.txt from it, or ensure they are invalidated on post save.
Plugin architecture
lh-ai-legibility/
├── lh-ai-legibility.php # Bootstrap, autoloader, init hooks
├── includes/
│ ├── class-llms-txt.php # CPT, rewrite rules, /llms.txt generation
│ ├── class-markdown-server.php # Accept: text/markdown content negotiation
│ ├── class-converter.php # HTML → Markdown (league or built-in)
│ └── class-section-block.php # Block registration + allowed-blocks filter
└── blocks/
└── section/
├── block.json # Block metadata
├── index.js # Edit/save components (vanilla wp.element)
└── editor.css # Editor-only styles
Classes are autoloaded from includes/ using the LH_AI_Legibility_ prefix convention.
Changelog
0.3 — 2026-05-31
- Add missing
index.phpsilence files to plugin root,includes/,blocks/, andblocks/section/.
0.2 — 2026-05-31
- Fix missing
returnafter fallbacksend()inmaybe_serve()— prevents potential fatal ifsend()is ever refactored to not exit. - Fix
nocache_headers()conflicting withCache-Control: publicin bothclass-llms-txt.phpandclass-markdown-server.php— headers are now set directly. - Fix
addslashes()incorrectly escaping single quotes in YAML scalars — replaced with targetedstr_replacefor\and"only. - Fix
ltrim()em-dash byte-corruption inparse_list_item()— replaced withpreg_replacewithuflag for correct UTF-8 handling. - Fix
imgalt attribute dropped whenalt=appears beforesrc=in converter — regex now captures both attributes independently.
0.1
- Initial release.
- Markdown content negotiation for singular posts and pages with YAML front matter.
llms_txt_documentCPT with singleton enforcement and admin menu integration./llms.txtand/llms-full.txtrewrite endpoints.llms-txt/sectionblock with restricted inserter inside the CPT editor.- Built-in HTML→Markdown converter with optional league/html-to-markdown delegation.
License
GPL-2.0+. See https://www.gnu.org/licenses/gpl-2.0.html.
Author
Peter Shaw — shawfactor.com