Skip to main content

Category 4: Content Structure

9 mins
For AI agents: a documentation index is available at /llms.txt — markdown versions of all pages are available by appending index.md to any URL path.

Part of the Web Documentation Delivery Spec. The Checks Summary lists all checks with links to their definitions.

These checks evaluate whether page content is structured in ways that agents can effectively consume. These are harder to fully automate and rely more on heuristics.

tabbed-content-serialization #

  • What it checks: Whether pages use tabbed, accordion, or dropdown UI patterns that serialize into long sequential content in the source, and if so, how large the serialized output is.
  • Why it matters: Tabbed content is great for humans but can be catastrophic for agents. A tutorial with 11 language variants serializes into a single massive document where an agent might see only the first 1-3 variants. Source order determines what the agent sees; everything past the truncation point is invisible. Asking for a specific variant (e.g., Python) does not help if that variant is beyond the truncation point.
  • Result levels:
    • Pass: No tabbed content, or tabbed content that serializes to under 50,000 characters total.
    • Warn: Tabbed content serializes to 50,000-100,000 characters.
    • Fail: Tabbed content serializes to over 100,000 characters.
  • Recommended action: Break tab variants into separate pages, or provide a mechanism for agents to request specific variants. Agents see only the first few variants; content in later tabs is truncated.
  • Automation: Heuristic. Detect common tab/accordion component patterns (e.g., <Tab>, <Tabs>, role=“tabpanel”, common CSS class patterns) and estimate serialized size.

section-header-quality #

  • What it checks: Whether section headers contain enough context to be meaningful without the surrounding UI. Specifically, when tabbed content is serialized, do headers distinguish which variant (language, platform, deployment type) a section belongs to?
  • Why it matters: When an agent sees serialized tabbed content, descriptive headers are the only way it can tell which section applies to which context. Generic headers like “Step 1” repeated across all variants are indistinguishable. Headers like “Step 1 (Python/PyMongo)” preserve the filtering context that the UI provided to human readers.
  • Result levels (evaluated both within individual tab groups and across tab groups on the same page; the overall result is the worst of both):
    • Pass: <=25% of headers within tabbed sections are generic (repeated across variants without distinguishing context).
    • Warn: 25-50% of headers are generic across variants.
    • Fail: >50% of headers are generic, or identical header sets are repeated across separate tab groups on the same page with no variant context. These thresholds are defaults; implementations should allow them to be configured.
  • Recommended action: Add variant context to headers (e.g., “Step 1 (Python)” instead of “Step 1”) so agents can distinguish which section belongs to which variant when content is serialized.
  • Automation: Heuristic. Requires detecting tabbed sections and analyzing header patterns within them.

markdown-code-fence-validity #

  • What it checks: Whether markdown content contains unclosed or improperly nested code fences (``` or ~~~ blocks without a matching closing delimiter).
  • Why it matters: An unclosed code fence causes everything after it to be interpreted as code rather than prose. The agent sees documentation text, API descriptions, and instructions as if they were inside a code block, which fundamentally changes how it processes the content. A model treats code blocks as literal content to reproduce or analyze, not as natural language instructions to follow. If an unclosed fence appears early in a page, the agent effectively loses the rest of the document’s meaning. This applies to any markdown the site serves directly: pages via .md URLs or content negotiation, and llms.txt files themselves.
  • Result levels:
    • Pass: All code fences in the markdown content are properly opened and closed.
    • Fail: One or more unclosed code fences detected.
  • Recommended action: Ensure every opening ``` or ~~~ has a matching closing delimiter. Everything after an unclosed fence is interpreted as code, causing agents to misread documentation as literal content.
  • Notes on delimiter matching: Per the CommonMark spec, a backtick fence (```) can only be closed by another backtick fence of equal or greater length, and likewise for tilde fences (~~~). Opening with ``` and attempting to close with ~~~ leaves the backtick fence unclosed. There is no intermediate “mismatched but balanced” state; mismatched delimiters produce unclosed fences and should be reported as failures.
  • Automation: Full. Parse the markdown for fence delimiters (``` and ~~~, with optional info strings) and verify each opening delimiter has a matching close. Run against markdown served via .md URLs, content negotiation responses, and llms.txt files.
  • Notes: This check applies to markdown the site authors and serves directly. Code fences broken by an HTML-to-markdown conversion pipeline are outside the site owner’s control, though implementations may optionally flag them as informational findings.
  • What it checks: Whether links in served markdown are absolute URLs, and whether a sample of them resolves to the representation they promise (a .md link returns markdown content, not an HTML error page).

  • Why it matters: Relative URL resolution is well-defined (RFC 3986), but it requires knowing the base URL, and agent pipelines routinely lose it. A browser always carries the base; markdown fetched by an agent passes through summarization models, gets chunked for RAG, or gets pasted into a context where the source URL is gone. Once the base is lost, a root-relative link is unreconstructable and a path-relative link is meaningless. This spec already recommends absolute URLs between llms.txt levels for the same reason; served markdown deserves the same rule.

    Link verification must go beyond status codes. In one observed production case, a catalog’s markdown variant emitted over 100 well-formatted links that all pointed into a wrong internal path prefix, apparently a build-time substitution error. Every link returned 200 with a body. The body was an HTML SPA shell whose only acknowledgment of failure was a serialized framework error digest inside script payload: a soft 404 served as HTML at a .md URL. A checker (or agent) that tested status codes alone would conclude the links worked; checking the Content-Type header alone would have caught it.

  • Result levels:

    • Pass: Links are absolute URLs, and sampled links resolve to the expected representation.
    • Warn: Links are root-relative (resolvable while the base URL is known, fragile once it isn’t), or sampled links resolve with minor mismatches (e.g., a .md link that redirects to an HTML page with the right content).
    • Fail: Links are path-relative, or sampled links are broken: hard 404s, soft 404s, or a content type that contradicts the link (.md links returning HTML shells).
  • Recommended action:

    • Warn: Emit absolute URLs when generating markdown variants; the site’s canonical host is known at build time.
    • Fail: Fix the link generation first, then make the links absolute. Verify generated links in CI by fetching a sample and checking both status and content type; a link set that is generated is a link set that can break wholesale.
  • Automation: Full. Parse links from served markdown, classify as absolute, root-relative, or path-relative, resolve a sample against the fetch URL, and verify status, Content-Type, and soft-404 heuristics (reusing the detection from http-status-codes).

  • Notes: Applies to markdown served via .md URLs, content negotiation, and pages discovered through llms.txt links. Links in llms.txt itself are covered by llms-txt-links-resolve, which applies the same representation verification. Same-document fragment links (#anchor with no path) are exempt from the absolute-URL requirement: they resolve within the content the agent already holds, and rewriting them to absolute URLs adds nothing.

    Why this check exempts the HTML path. Relative links in HTML are correct web practice (they are what makes staging domains, mirrors, and CDN setups work), and the HTML path does not need the site’s help: a pipeline converting HTML to markdown still holds the fetch URL at conversion time, so resolving relative links is the pipeline’s job, with full information. The base URL is only lost downstream of conversion. Served markdown is different on both ends: the generator knows the canonical host, so absolute links are free to produce, and on the best-case consumption path (direct delivery of text/markdown under the summarization threshold) the site’s bytes reach the model verbatim, with no conversion step where anything could be resolved. Tool builders implementing fetch pipelines should resolve relative links during HTML-to-markdown conversion, and likewise when ingesting raw HTML; converted content with relative links has all the same failure modes as served markdown once it leaves the converter.

embedded-data-serialization #

  • What it checks: Whether machine-generated bulk data (large uniform tables, inline JSON or data blobs, base64 payloads) dominates a page’s converted content, and attributes the page’s size to the specific elements responsible.

  • Why it matters: Dynamic widgets (compatibility matrices, model catalogs, spec browsers, pricing tables) flatten into static content when a page is rendered for agents. The result can be a page whose size wildly exceeds what its author believes they wrote: the author sees a few paragraphs and a widget; the built page carries hundreds of serialized rows under them. The size checks in Category 3 catch the symptom but don’t explain it, and without attribution the person who can fix the page has no idea what to fix, or that anything is wrong at all.

    In one measured production case, a reference page served 302KB of HTML of which 64% was table markup, including a single generated table of 218 rows. The page converts to roughly 83,000 characters (this spec’s warn band), while its non-table prose totals about 17,000 characters. The page’s truncation risk is entirely a property of its generated tables, which is invisible in an aggregate size number.

  • Result levels:

    • Pass: No bulk-data elements detected, or bulk elements are present but the page passes the Category 3 size checks regardless.
    • Warn: Bulk-data elements are the dominant contributor (for example, over half of converted content) to a page that lands in the size checks’ warn band.
    • Fail: Bulk-data elements are the dominant contributor to a page that exceeds the size checks’ fail threshold. Content after the bulk element is beyond the truncation point for most platforms.
  • Recommended action: Bulk data is often legitimate content (a support matrix is the point of a support-matrix page), so the goal is structure, not removal. Split large generated tables across per-section pages (as self-contained units reached from an index, each complete for its scope; paginating one table into windows trades this problem for the one single-fetch-completeness describes), provide filtered or queryable views, load embedded data blobs on demand, and place prose before bulk elements so truncation removes data rows rather than explanation. Report the attribution to content authors: a page that an author experiences as two paragraphs should not ship as a hundred kilobytes without the author knowing.

  • Automation: Heuristic. After HTML-to-markdown conversion (same pipeline as page-size-html), detect bulk elements: tables above a row threshold with uniform row structure, fenced or inline JSON blobs above a size threshold, and base64 runs. Report each element’s share of the converted content. Thresholds for “bulk” need calibration against real pages and should be configurable. This check sees only bulk that survives conversion into content (tables, code fences, prose-embedded data); serialized payloads inside <script> tags are stripped by conversion and are page-size-transfer’s domain. The two checks divide the data-dump problem by pipeline: script payloads burden every fetch, content-embedded bulk burdens what the model reads.

  • Notes: This is the data-widget sibling of tabbed-content-serialization, which covers the same flattening failure for tab and accordion UI. Together with content-start-position (where content sits relative to boilerplate), these checks explain why a page fails the Category 3 size checks, not just that it does.