fix: respect explicit Accept: text/html from AI bot user agents - #70
fix: respect explicit Accept: text/html from AI bot user agents#70abhay-codes07 wants to merge 1 commit into
Conversation
Per AEO spec section 5, UA-based markdown negotiation must not override an explicit Accept. Every edge and framework adapter served the markdown twin for any known bot UA, even when the request asked for Accept: text/html, so a crawler sending a bot UA plus Accept: text/html received text/markdown with X-Robots-Tag: noindex on the canonical URL. Add shouldServeMarkdown(accept, isBot) to @dualmark/core and use it in the cloudflare, deno, fastly, netlify, nextjs, sveltekit, vercel and nuxt adapters. A bot UA now stays on HTML when it explicitly requests text/html, while Accept: */*, no Accept, and Accept: text/markdown are unchanged.
|
@abhay-codes07 is attempting to deploy a commit to the Dodo Payments Team on Vercel. A member of the Team first needs to authorize it. |
|
@dodo-squirrels review |
Review: looks good, approving the approachI reviewed the change end-to-end and it's well-implemented and correct. Core logic — Edge cases verified:
Consistency — all 8 edge/framework adapters (cloudflare, deno, fastly, netlify, nextjs, nuxt ×2, sveltekit, vercel) were updated identically and each passes the raw Versioning — changeset is appropriate: Validation — checked out the branch and ran the suites locally:
No changes required. LGTM. |
There was a problem hiding this comment.
Review: fix: respect explicit Accept: text/html from AI bot user agents
Verdict: LGTM — approving. One minor edge-case behavior change noted below (non-blocking).
Reviewed end-to-end: new @dualmark/core primitive + all 8 adapters, exercised the core logic against a full truth table.
Spec alignment
content-negotiation spec §5 is explicit: UA-based markdown is an OPTIONAL extension that "MUST still respect explicit Accept: text/html from a bot UA (i.e. UA detection MUST NOT override an explicit Accept)." On main, every adapter used bot.isBot || fmt === "markdown", so a crawler sending a bot UA and Accept: text/html (e.g. a preview/link-unfurl fetch) was handed text/markdown + X-Robots-Tag: noindex on the canonical URL — a real spec violation. This fixes it.
shouldServeMarkdown logic — verified
The two-line implementation is subtle but correct:
negotiateFormat(accept) === "markdown"→ default order prefers HTML on ties, so this is true only when markdown strictly wins (coversAccept: text/markdownfor any UA).- bot branch:
negotiateFormat(accept, ["markdown", "html"])→ markdown listed first, so a tie (wildcard / no Accept) resolves to markdown for bots, while explicittext/html(HTML strictly outranks, or markdown unacceptable) keeps the bot on HTML.
I exercised the full matrix on the branch:
| Accept | bot | result | |
|---|---|---|---|
text/markdown |
any | md | ✓ |
"" / */* / text/* |
bot | md | ✓ |
text/html |
bot | HTML | ✓ the fix |
text/html,…,*/*;q=0.8 (browser-like) |
bot | HTML | ✓ |
"" / */* / text/html |
human | HTML | ✓ |
text/html;q=0.5, text/markdown;q=0.5 |
bot | md | ✓ |
| same tie | human | HTML | ✓ |
Adapter integration — verified
All 8 adapters (cloudflare, deno, fastly, netlify, nextjs, sveltekit, vercel, nuxt ×2 sites) now route through shouldServeMarkdown, and each ships the same regression test. The pre-existing if (fmt === null && accept) → 406 guard runs before shouldServeMarkdown in the edge adapters, so the "Accept excludes everything" case still correctly 406s rather than reaching this function.
Minor note (non-blocking)
In the Nuxt middleware the 406 guard is format === null && !botInfo.isBot, so a bot sending an Accept that excludes both types (e.g. Accept: image/png) now falls through to HTML SSR instead of the old behavior of serving markdown. That's arguably an improvement (no noindex markdown for a mismatched Accept) and still spec-defensible, but it is a behavior change on that edge — worth a one-line test if you want to pin it.
Tests
- Full suites green locally across all affected packages: core 185, cloudflare 24, deno 24, fastly 20, netlify 28, nextjs 51, sveltekit 20, vercel 29, nuxt 50.
Clean primitive, consistent rollout, good test coverage, changeset with correct semver bumps (core minor for the new export, adapters patch). Nice work.
Summary
Bot user agents currently get the markdown twin even when they explicitly ask for
Accept: text/html. The AEO spec says the opposite should happen, so this fixes the adapters to honor an explicit Accept while keeping the existing bot behavior for everything else.Closes #69.
Changes
shouldServeMarkdown(accept, isBot)to@dualmark/core. It serves markdown when RFC 7231 negotiation already picks markdown, or when the request is from a known bot and markdown is at least as acceptable as HTML. It checks the second case by negotiating with markdown listed first, so a tie (wildcard or empty Accept) resolves to markdown for bots, while an explicitAccept: text/htmlkeeps the bot on HTML.bot.isBot || fmt === "markdown"check that ignored the Accept header for bots.GPTBot/1.0UA withAccept: text/htmlnow receives HTML, not a noindex markdown body.@dualmark/core.Behavior is unchanged for
Accept: */*, noAccept, andAccept: text/markdown, so the existing conformance scores and bot handling stay the same.Type
AEO_SPEC_VERSION)Verification
bun run buildpassesbun run testpasses (541 tests across the packages)bun run typecheckpassesdualmark verifyagainst the example end-to-end (no examples touched)/spec/: ran sync-spec and committed the mirror (no spec files touched)Changeset
@dualmark/coreminor, adapters patch)cc @thepushkaraj @aagarwal1012 for a look when you have a minute(file change might look scary but indeed needed have a look). Happy to adjust the approach if you would rather keep the decision logic inside each adapter instead of a shared core helper.