Skip to content

feat(gnoweb): serve realm pages as markdown via Accept negotiation#5794

Open
gfanton wants to merge 8 commits into
gnolang:masterfrom
gfanton:feat/gnoweb-markdown-negotiation
Open

feat(gnoweb): serve realm pages as markdown via Accept negotiation#5794
gfanton wants to merge 8 commits into
gnolang:masterfrom
gfanton:feat/gnoweb-markdown-negotiation

Conversation

@gfanton

@gfanton gfanton commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

gnoweb now serves a realm page (and static-markdown aliases) as raw text/markdown
when the client asks for it through the Accept header. The bytes are the exact
output of the realm's Render() function, written without the HTML page layout and
without 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 WebFetch sends Accept: 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

  • Negotiation is Accept-only: a response is markdown when the header names
    text/markdown (or the alias text/x-markdown) with a non-zero q-value. It never
    matches via */*, so browsers keep receiving HTML. An explicit q=0 is honored as a refusal.
  • For a realm, the raw Render() bytes are written verbatim with
    Content-Type: text/markdown; charset=utf-8, bypassing IndexLayout; the goldmark
    parse is skipped on this path.
  • Vary: Accept is set on GET responses so shared caches key markdown and HTML
    separately for the same URL.
  • Scope is realm pages and static-markdown aliases. Source, help, directory and user
    views fall back to HTML for now (follow-up work).

Test plan

  • negotiate_test.go: table covering the header rules (aliases, q-values, */* and
    text/* wildcards, q=0 refusal, the real WebFetch Accept string).
  • view_markdown_test.go: verbatim render of the markdown view.
  • handler_http_test.go: end-to-end through ServeHTTP, asserting Content-Type,
    Vary, markdown vs HTML bodies, and that a realm with no Render() still falls back to HTML.
  • Verified live against gnodev:
    curl -H 'Accept: text/markdown' http://127.0.0.1:8888/r/gnoland/home returns markdown;
    a plain request returns the HTML page.

gfanton and others added 7 commits June 3, 2026 19:12
🤖 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>
@github-actions github-actions Bot added 📦 ⛰️ gno.land Issues or PRs gno.land package related 🌍 gnoweb Issues & PRs related to gnoweb and render labels Jun 8, 2026
@Gno2D2 Gno2D2 requested a review from alexiscolin June 8, 2026 14:22
@Gno2D2

Gno2D2 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

🛠 PR Checks Summary

🔴 Changes related to gnoweb must be reviewed by its codeowners

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
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)
🔴 Changes related to gnoweb must be reviewed by its codeowners

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 The pull request was created from a fork (head branch repo: gfanton/gno)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

Changes related to gnoweb must be reviewed by its codeowners

If

🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 A changed file matches this pattern: ^gno.land/pkg/gnoweb/ (filename: gno.land/pkg/gnoweb/components/view_markdown.go)

Then

🔴 Requirement not satisfied
└── 🔴 Or
    ├── 🔴 Or
    │   ├── 🔴 And
    │   │   ├── 🔴 Pull request author is user: alexiscolin
    │   │   └── 🔴 This user reviewed pull request: gfanton (with state "APPROVED")
    │   └── 🔴 And
    │       ├── 🟢 Pull request author is user: gfanton
    │       └── 🔴 This user reviewed pull request: alexiscolin (with state "APPROVED")
    └── 🔴 And
        ├── 🟢 Not (🔴 Pull request author is user: alexiscolin)
        ├── 🔴 Not (🟢 Pull request author is user: gfanton)
        └── 🔴 Or
            ├── 🔴 This user reviewed pull request: alexiscolin (with state "APPROVED")
            └── 🔴 This user reviewed pull request: gfanton (with state "APPROVED")

Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

@davd-gzl davd-gzl self-requested a review June 8, 2026 14:44

@davd-gzl davd-gzl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Clean and well-tested; verified on the current head (4ab2753). Nothing blocking.

Full review: https://github.com/samouraiworld/gno-agent-workspace/blob/main/reviews/pr/5xxx/5794-markdown-accept-negotiation/2-4ab275316/claude-opus-4-8_davd-gzl.md

(AI Agent)


Code LGTM!

Comment on lines +15 to +32
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
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌍 gnoweb Issues & PRs related to gnoweb and render 📦 ⛰️ gno.land Issues or PRs gno.land package related

Projects

Development

Successfully merging this pull request may close these issues.

3 participants