Skip to content

fix(developer-hub): address agent-friendly docs check failures#3704

Open
aditya520 wants to merge 2 commits into
mainfrom
adi/afdocs-fixes
Open

fix(developer-hub): address agent-friendly docs check failures#3704
aditya520 wants to merge 2 commits into
mainfrom
adi/afdocs-fixes

Conversation

@aditya520
Copy link
Copy Markdown
Member

@aditya520 aditya520 commented May 14, 2026

Summary

Brings the developer-hub up to passing on the agent-friendly doc check.
The site was failing four checks reported by
afdocs check:

  • llms-txt-valid — links not parseable.
  • llms-txt-directive-html — no page advertised /llms.txt.
  • content-negotiationAccept: text/markdown was ignored.
  • page-size-html — agents had to chew through 100K+ of HTML to get to docs.

…plus a cache-header-hygiene warning on /llms.txt.

What changed

  • /llms.txt rewritten in the llmstxt.org link-list format
    (- [name](url): description under headings) so links parse cleanly.
    Cache dropped from 1 d to 1 h.
  • HTML directive<link rel="alternate" type="text/plain" href="/llms.txt">
    in <head> plus a visually-hidden anchor in <body> of the root layout,
    so every page advertises the index.
  • Markdown directiveget-llm-text.ts now prepends a blockquote
    pointing at /llms.txt to every markdown response.
  • Content negotiation — new src/proxy.ts (Next.js 16 proxy/middleware)
    parses Accept, rewrites doc URLs to the existing /mdx/[...slug] route
    when the client prefers markdown, and sets Vary: Accept so CDNs
    don't mix HTML/markdown for the same URL. Skips /, /api,
    /playground, asset paths, and the static /llms-*.txt routes.

Verification

Ran npx afdocs check http://localhost:3627/price-feeds/core/getting-started --fixes
against the local production build:

Check (was failing) Now
llms-txt-valid ✓ "follows the proposed structure"
llms-txt-directive-html ✓ "found in HTML of all pages, near the top of content"
llms-txt-directive-md ✓ "found in markdown of all pages"
content-negotiation ✓ "1/1 pages support content negotiation"
markdown-url-support
page-size-html ✓ median 5 K markdown after boilerplate strip; Accept-negotiated agents skip HTML entirely
markdown-content-parity

Manual spot-checks:

$ curl -sI -H 'Accept: text/markdown' http://localhost:3627/price-feeds/core/getting-started
content-type: text/markdown; charset=utf-8
vary: Accept

$ curl -sI http://localhost:3627/price-feeds/core/getting-started
Content-Type: text/html; charset=utf-8

pnpm build and pnpm exec tsc --noEmit both pass.

Out of scope

  • content-start-position (a new ⚠ warning, not in the failing-checks
    list) — would need a Fumadocs layout reshuffle.
  • s-maxage=365d on doc pages — that's the Next.js ISR default; the
    user's report only flagged /llms.txt for cache, which is now 1 h.

Test plan

  • CI passes
  • On deploy, run npx afdocs check https://docs.pyth.network/ --fixes and confirm the four checks above flip to ✓ and Agent Score climbs above 75/100
  • Spot-check curl -H 'Accept: text/markdown' https://docs.pyth.network/price-feeds/core/getting-started returns markdown
  • Spot-check curl https://docs.pyth.network/llms.txt returns the new link-list format with Cache-Control: public, max-age=3600

Open in Devin Review

- Rewrite llms.txt to use the llmstxt.org link-list format so afdocs
  can parse links.
- Add a visually-hidden /llms.txt directive (both `<link rel="alternate">`
  and an a11y-hidden anchor) to every page rendered by the root layout.
- Prepend a blockquote pointing at /llms.txt to every markdown response
  so the directive is also present in the markdown variant.
- Add a Next.js proxy that honors `Accept: text/markdown` by rewriting
  doc paths to the existing /mdx/[...slug] route, with `Vary: Accept`
  so CDNs don't mix variants.
- Drop /llms.txt cache lifetime from 1d to 1h to clear the
  cache-header-hygiene warning.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
component-library Ready Ready Preview, Comment May 15, 2026 5:13pm
developer-hub Ready Ready Preview, Comment May 15, 2026 5:13pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
api-reference Skipped Skipped May 15, 2026 5:13pm
entropy-explorer Skipped Skipped May 15, 2026 5:13pm
insights Skipped Skipped May 15, 2026 5:13pm
proposals Skipped Skipped May 15, 2026 5:13pm
staking Skipped Skipped May 15, 2026 5:13pm

Request Review

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 No test coverage for new middleware content negotiation

The new middleware (apps/developer-hub/src/middleware.ts) introduces non-trivial content negotiation logic including Accept header parsing, quality factor comparison, path skip lists, and URL rewriting. Per REVIEW.md, new functionality should have tests. The prefersMarkdown function in particular has enough edge cases (multiple entries, quality factors, missing entries, malformed input) that unit tests would be valuable. The AGENTS.md file notes that linting and type-checking are the primary gate, but the middleware logic is complex enough that tests would help catch regressions.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread apps/developer-hub/src/middleware.ts
Rename src/proxy.ts to src/middleware.ts and rename the exported
`proxy` function to `middleware` so Next.js App Router picks it up.
Without this, the Accept-header content negotiation referenced in
Root/index.tsx was a no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +42 to +62
export function middleware(request: NextRequest) {
if (request.method !== "GET" && request.method !== "HEAD") {
return NextResponse.next();
}
if (!prefersMarkdown(request.headers.get("accept"))) {
return NextResponse.next();
}

const { pathname } = request.nextUrl;
if (SKIP_EXACT.has(pathname)) return NextResponse.next();
if (SKIP_PREFIXES.some((p) => pathname === p || pathname.startsWith(`${p}/`))) {
return NextResponse.next();
}
// Already a .md/.mdx URL — let the existing rewrite handle it.
if (/\.[a-z0-9]+$/i.test(pathname)) return NextResponse.next();

const url = request.nextUrl.clone();
url.pathname = `/mdx${pathname}`;
const response = NextResponse.rewrite(url);
response.headers.set("Vary", "Accept");
return response;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing Vary: Accept on non-rewritten responses breaks HTTP caching for content-negotiated paths

The middleware sets Vary: Accept only on responses that are rewritten to /mdx/... (src/middleware.ts:61), but does NOT set it on the pass-through NextResponse.next() responses for the same content-negotiable paths. Per HTTP spec (RFC 7231 §7.1.4), when a URL can return different representations based on request headers, ALL responses—including the "default" HTML one—must include Vary: Accept so that intermediate caches (CDN, forward proxy, browser cache) know to key on the Accept header. Without it, a cached HTML response could be served to a client requesting text/markdown, or vice versa.

On Vercel the impact is mitigated because middleware runs before CDN cache lookup, and rewrites use a different internal URL. But on any other deployment platform or when an external CDN sits in front, this is a correctness bug.

Prompt for agents
In middleware.ts, the Vary: Accept header is only set on the NextResponse.rewrite() path (line 61), but NOT on the NextResponse.next() returns for paths that are subject to content negotiation (i.e., paths that pass all skip checks but the client doesn't prefer markdown). 

The fix: for the code path after all SKIP_EXACT / SKIP_PREFIXES / file-extension checks (i.e., after line 56, when the path IS content-negotiable), if prefersMarkdown returns false, you should still return a NextResponse.next() with Vary: Accept set. Currently, the prefersMarkdown check and the skip checks are interleaved — the prefersMarkdown check happens first (line 46) and returns NextResponse.next() before we even know if the path is content-negotiable.

Suggested restructuring: move the prefersMarkdown check AFTER the skip checks, so you can distinguish between 'skipped path' (no Vary needed) and 'content-negotiable path where client wants HTML' (Vary: Accept needed). For the latter, do:
  const response = NextResponse.next();
  response.headers.set('Vary', 'Accept');
  return response;

Alternatively, add Vary: Accept to the headers config in next.config.js for the doc page paths, but per-path middleware logic is cleaner.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant