Skip to content

fix(docs): emit absolute URLs from llms.txt and llms-full.txt - #198

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-178-absolute-llms-links
Aug 26, 2026
Merged

fix(docs): emit absolute URLs from llms.txt and llms-full.txt#198
hotlong merged 1 commit into
mainfrom
claude/issue-178-absolute-llms-links

Conversation

@hotlong

@hotlong hotlong commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #178

Both llms endpoints emitted site-relative links. That file is fetched and its text is then lifted into a context window where the origin is no longer attached to it, so a relative path resolves only if whatever moved it there also carried the base URL — which is exactly what a consumer of an llms.txt will not do. The marketing site's own /llms.txt has always emitted absolute URLs; this is the docs site catching up to it.

What changed

llms.txt rewrites page-tree node URLs before handing them to fumadocs' indexNode, rather than rewriting the rendered bullet. Titles, descriptions, indentation and nesting therefore come out of the generator untouched and no pattern ever runs over an authored description. Node identity is carried by $ref, not urlgetNodePage and getNodeMeta both look up by $ref — so the spread leaves title and description lookup working.

The three prose path examples in the header and the "Other Languages" footer go through localeUrl, which is what that helper is for: they are logical paths, and localeUrl is the thing that knows the default locale carries no prefix.

llms-full.txt resolves each page's body links against that page's own absolute URL. Site-root links (/docs/x) were the card's subject; resolving per page also fixes the page-relative (./sibling) and bare-fragment (#anchor) links, which needed the page identity that this file has never carried and were therefore less resolvable than the ones the card named. Fenced code blocks are skipped, so a future page that documents markdown link syntax is not silently edited.

Targets carrying a scheme cannot match the pattern at all, so no https: or mailto: URL is re-parsed and re-serialized; nor is a protocol-relative //host/x, which names a different origin.

robots.ts — the adjacent question the card names. It stayed a one-line change, so it is in: BASE is deleted and SITE_URL imported in its place. That takes hostname literals in apps/docs from 4 to 3 and adds none. apps/docs/lib/seo.ts is unchanged — its existing exports were already sufficient, so there is no new helper either.

The two literals that remain are app/layout.tsx's metadataBase and middleware.ts's canonical-domain redirect. Both are outside this card's declared file surface, and middleware.ts runs in the edge runtime where importing lib/seo would pull the fumadocs source loader in with it — so collapsing them is a real design question, not the same one-line move. Left alone, not filed.

Verification

Production builds (turbo run build --filter=@objectos/docs), served with next start, both bodies fetched over HTTP. Head build and the counts below are from 9073d14.

body lines bytes links before after
llms.txt 116 to 116 12,589 to 14,557 79 to 79 79 site-relative, 0 absolute 0 site-relative, 79 absolute
llms-full.txt 12,834 to 12,834 671,413 to 685,243 646 to 646 510 site-root, 24 page-relative, 14 fragment 0 of each, 549 rewritten absolute
robots.txt 6 to 6 117 to 117 byte-identical

Every one of the 79 llms.txt targets and all 549 rewritten llms-full.txt targets begin https://docs.objectos.ai and parse as absolute URLs. The 97 targets left untouched are the ones that already carried a scheme (https:, mailto:), verified byte-identical.

Composition is unchanged — the invariant #175's gate asserts.

llms.txt: the served body is byte-for-byte identical to main once the string https://docs.objectos.ai is deleted from the new side. Same 79 links in the same order, same 79 slugs, same 10 headings, same 116 lines. Nothing but the spelling of a URL moved.

llms-full.txt needs a per-link check rather than a string deletion, because ./sibling and #anchor expand to more than a prefix. Pairing the two served bodies page by page:

  • 79 page blocks on both sides; page-title multiset identical
  • every line byte-identical once link targets are masked out — no prose, heading or code changed
  • 646 link targets compared; each one either byte-identical (97, the scheme-carrying ones) or verified equal to new URL(beforeTarget, thatPagesUrl).href (549)
  • 0 discrepancies

Locale surface unchanged: 0 non-default-locale link targets in either body, before and after.

Two pre-existing defects surfaced by the comparison, filed rather than fixed here

Gates

Run on 9073d14:

  • pnpm turbo run type-check --filter=@objectos/docsTasks: 1 successful, cache miss (really executed)
  • pnpm turbo run build --filter=@objectos/docsCompiled successfully, Tasks: 1 successful
  • pnpm turbo run testTasks: 1 successful
  • node .github/scripts/check-node-floor.mjs --self-test then the real scan — both exit 0
  • node .github/scripts/check-translation-ownership.mjs against the real changed-file list — exit 0, "touches 0 translation artifact(s)"

The 310 Failed to load dynamic font lines in the build log are the OG-image route reaching for Google Fonts through a proxy with a self-signed certificate. Identical count on the baseline build of main; environment noise, not this change.

No changeset: this repository has no changeset flow.


Generated by Claude Code

Both llms endpoints emitted site-relative links. That file is fetched and
its text lifted into a context window where the origin is no longer attached
to it, so a relative path resolves only if whatever moved it there also
carried the base URL — which is exactly what a consumer of an llms.txt will
not do. The marketing site's own /llms.txt has always emitted absolute URLs;
this is the docs site catching up to it.

llms.txt rewrites page-tree node URLs before handing them to fumadocs'
indexNode, so titles, descriptions, indentation and nesting come out
untouched and no pattern runs over an authored description. Node identity is
carried by $ref, not url, so the spread leaves title lookup working. The
three prose path examples go through localeUrl.

llms-full.txt resolves each page's body links against that page's own
absolute URL, which also fixes ./sibling and #fragment links that needed the
page identity the file never carried. Fenced code blocks are skipped.

No new base-URL literal: both routes import SITE_URL and localeUrl from
lib/seo.ts, which is unchanged. robots.ts's own BASE collapses into the same
constant, taking the count of hostname literals in apps/docs from 4 to 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5zjYc2BoFV2NjKBBapC7C
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] llms.txt and llms-full.txt emit site-relative links, so the map only resolves for a fetcher that remembers where it fetched from

2 participants