Skip to content

[finding] llms-full.txt emits HTML numeric entities into a plain-text file, and two of them break the markdown link they sit in #197

Description

@hotlong

Found while implementing #178. Pre-existing on main — the counts below are byte-identical before and after that change, which is how it was spotted.

Notation. This tracker's body sanitizer decodes HTML numeric character references, including inside code fences — the first version of this card was rewritten by it into one where the before and after examples read identically. So throughout this card, AMP stands for one literal ampersand character. AMP#x2A; means the six characters that would otherwise be typed as an ampersand, hash, x, 2, A, semicolon. That behaviour is itself worth knowing about when filing evidence here.

What is on main

The body served at /llms-full.txt contains HTML numeric character references, in text that is not HTML:

entity character it encodes occurrences
AMP#x2A; asterisk 58
AMP#x60; backtick 5
AMP#x29; closing parenthesis 4

They come from page.data.getText('processed'), via getLLMText in apps/docs/lib/source.ts.

The authored MDX is plain. content/docs/build/index.mdx:36:

1. **Just exploring?** → Open ObjectOS, start the [AI Builder](/docs/build/ai-builder),
   say *"I need to track customer support tickets ..."*

The served text for that same line, with the entities spelled in the notation above:

1. **Just exploring?** → Open ObjectOS, start the [AI Builder](/docs/build/ai-builderAMP#x29;,
   say AMP#x2A;"I need to track customer support tickets ..."*

Two things happened: the closing parenthesis of the link target became AMP#x29;, and the opening asterisk of the emphasis pair became AMP#x2A; while its closing partner stayed a literal asterisk.

Why it is worth a card

Two effects, one cosmetic and one not.

Cosmetic: an LLM reading this file reads AMP#x2A; literally. It is not markdown emphasis and it is not an asterisk; it is six characters of noise inside a sentence, 58 times. The whole file exists to be read as text by something that will not run it through an HTML parser.

Not cosmetic: where the escaped character is a link's closing parenthesis, the markdown link stops being a link. Two of the four AMP#x29; occurrences do exactly that, both on [AI Builder](/docs/build/ai-builder... — one in content/docs/build/index.mdx, one in content/docs/build/ai-builder.

Both render correctly on the website, because there the entity decodes back to a parenthesis before markdown ever sees it. In llms-full.txt nothing decodes it, so the link is delivered as broken literal text.

These are the only 2 of the 646 link targets in the served body that are not well-formed; the other 644 are fine.

Measured on main @ 6653744, production build, fetched over HTTP.

Shape of the fix

Not obvious enough to prescribe, which is why this is a finding rather than a task. getText('processed') is fumadocs' own pipeline and the escaping happens inside it. Candidates: ask for a different text mode; decode the numeric references on the way out in getLLMText; report it upstream. Worth checking which characters fumadocs escapes and why before picking — a blind decode of every numeric reference would also rewrite one an author typed on purpose.

/llms.txt is unaffected: it emits titles and descriptions from frontmatter, not processed body text.

Out of scope for #178, which changes how URLs in these bodies are spelled and leaves the 2 malformed targets exactly as it found them. #178 is not addressed here.

Reproduce

cd apps/docs && pnpm run build
grep -o 'AMP#x[0-9A-Fa-f][0-9A-Fa-f];' .next/server/app/llms-full.txt.body | sort | uniq -c

(with AMP typed as a literal ampersand)


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions