feat(gnoweb): serve realm pages as markdown via Accept negotiation#5794
feat(gnoweb): serve realm pages as markdown via Accept negotiation#5794gfanton wants to merge 8 commits into
Conversation
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Replace the hand-rolled media-type/q-value splitting with mime.ParseMediaType, removing the markdownQualityIsZero helper. Behavior is unchanged and covered by TestNegotiatesMarkdown. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
WebFetch sends "Accept: text/markdown, text/html, */*" (verified via httpbin.org/headers), so an agent fetch gets markdown automatically with no manual header. Pin the exact string as a regression case. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…kdownRealmView Split the realm render path into GetRealmView (HTML) and GetMarkdownRealmView, sharing a fetchRealm helper for the fetch and error fallbacks. The wantMarkdown bool remains only as a dispatch selector in prepareIndexBodyView/GetPackageView. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
🛠 PR Checks Summary🔴 Changes related to gnoweb must be reviewed by its codeowners Manual Checks (for Reviewers):
Read More🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers. ✅ Automated Checks (for Contributors):🟢 Maintainers must be able to edit this pull request (more info) ☑️ Contributor Actions:
☑️ Reviewer Actions:
📚 Resources:Debug
|
There was a problem hiding this comment.
Clean and well-tested; verified on the current head (4ab2753). Nothing blocking.
(AI Agent)
Code LGTM!
| func negotiatesMarkdown(accept string) bool { | ||
| for _, part := range strings.Split(accept, ",") { | ||
| mediaType, params, err := mime.ParseMediaType(strings.TrimSpace(part)) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| if mediaType != "text/markdown" && mediaType != "text/x-markdown" { | ||
| continue | ||
| } | ||
| if q, ok := params["q"]; ok { | ||
| if v, err := strconv.ParseFloat(q, 64); err == nil && v <= 0 { | ||
| continue | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
text/html;q=0.9, text/markdown;q=0.8 returns markdown: any non-zero q wins regardless of client ranking, a shortcut over RFC 9110 preference ordering. Harmless since browsers never send text/markdown; confirming it's intentional.
repro
# from a local clone of gnolang/gno:
gh pr checkout 5794 -R gnolang/gno
go test ./gno.land/pkg/gnoweb/ -run 'TestNegotiatesMarkdown/markdown_present_with_non-zero_q_among_others' -v=== RUN TestNegotiatesMarkdown/markdown_present_with_non-zero_q_among_others
--- PASS: TestNegotiatesMarkdown/markdown_present_with_non-zero_q_among_others (0.00s)
PASS
negotiate_test.go:31 asserts want: true for this header.
(AI Agent)
| // The realm and static-markdown paths return a markdown view; serve its | ||
| // raw source verbatim with a text/markdown Content-Type, bypassing the layout. | ||
| if bodyView.Type == components.MarkdownViewType { | ||
| w.Header().Set("Content-Type", "text/markdown; charset=utf-8") |
There was a problem hiding this comment.
This write serves realm Render() bytes verbatim (the HTML path sanitizes through goldmark) and gnoweb sets no nosniff anywhere. Add X-Content-Type-Options: nosniff here.
(AI Agent)
Summary
gnoweb now serves a realm page (and static-markdown aliases) as raw
text/markdownwhen the client asks for it through the
Acceptheader. The bytes are the exactoutput of the realm's
Render()function, written without the HTML page layout andwithout the goldmark render step. Every other request is unchanged and still gets HTML.
Motivation
Realm
Render()already returns markdown; gnoweb converts it to HTML for the browser,and clients that prefer the source (LLM agents, scripts, doc tooling) had no way to get
it. For example, Claude Code's
WebFetchsendsAccept: text/markdown, text/html, */*,so it now receives the realm markdown directly (about 4.7 KB for the home realm) instead
of the full HTML page (about 58 KB), with no manual configuration.
How it works
Accept-only: a response is markdown when the header namestext/markdown(or the aliastext/x-markdown) with a non-zero q-value. It nevermatches via
*/*, so browsers keep receiving HTML. An explicitq=0is honored as a refusal.Render()bytes are written verbatim withContent-Type: text/markdown; charset=utf-8, bypassingIndexLayout; the goldmarkparse is skipped on this path.
Vary: Acceptis set on GET responses so shared caches key markdown and HTMLseparately for the same URL.
views fall back to HTML for now (follow-up work).
Test plan
negotiate_test.go: table covering the header rules (aliases, q-values,*/*andtext/*wildcards,q=0refusal, the real WebFetch Accept string).view_markdown_test.go: verbatim render of the markdown view.handler_http_test.go: end-to-end throughServeHTTP, asserting Content-Type,Vary, markdown vs HTML bodies, and that a realm with noRender()still falls back to HTML.gnodev:curl -H 'Accept: text/markdown' http://127.0.0.1:8888/r/gnoland/homereturns markdown;a plain request returns the HTML page.