From b7b3fe14f6734e4c744d90eab2eb5a58fe4b1d57 Mon Sep 17 00:00:00 2001 From: mich-elle-luna Date: Wed, 24 Jun 2026 14:29:11 -0700 Subject: [PATCH 1/3] Add Vary: Accept header and document nginx content negotiation config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Vary: Accept to all hugo serve responses so caches know responses vary by Accept header - Serve .html.md files with Content-Type: text/markdown during local dev - Document the production nginx config needed for HTTP-level content negotiation (Accept: text/markdown → serve index.html.md) in AI_AGENT_DEVELOPER_GUIDE.md The element is already generated for every page by the AlternativeOutputFormats loop in baseof.html — no changes needed there. Co-Authored-By: Claude Sonnet 4.6 --- AI_AGENT_DEVELOPER_GUIDE.md | 42 +++++++++++++++++++++++++++++++++++++ config.toml | 17 ++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/AI_AGENT_DEVELOPER_GUIDE.md b/AI_AGENT_DEVELOPER_GUIDE.md index 661a655d87..731d987a38 100644 --- a/AI_AGENT_DEVELOPER_GUIDE.md +++ b/AI_AGENT_DEVELOPER_GUIDE.md @@ -212,6 +212,48 @@ When working on this project: --- +## Production Configuration for Agent Readiness + +### Content Negotiation (text/markdown) + +Every page generates a markdown version at `{path}index.html.md` (e.g. `/develop/index.html.md`). +The `` element is already present in every page's ``. + +To enable HTTP-level content negotiation so `Accept: text/markdown` returns the markdown file +with `Content-Type: text/markdown`, configure nginx as follows: + +```nginx +map $http_accept $use_markdown { + "~*text/markdown" 1; + default 0; +} + +server { + # Serve .html.md files with correct Content-Type + location ~ \.html\.md$ { + add_header Content-Type "text/markdown; charset=utf-8"; + add_header Vary "Accept"; + } + + location / { + add_header Vary "Accept"; + + # Content negotiation: rewrite to .html.md when Accept: text/markdown + if ($use_markdown) { + rewrite ^(.*/)$ $1index.html.md last; + rewrite ^([^.]+)$ $1/index.html.md last; + } + + try_files $uri $uri/ =404; + } +} +``` + +The `hugo serve` dev server already sends `Vary: Accept` and `Content-Type: text/markdown` +for `.html.md` files via the `[server]` config in `config.toml`. + +--- + **Last Updated**: 2026-01-08 **Purpose**: Help AI agents discover and understand Redis documentation architecture diff --git a/config.toml b/config.toml index 2752ff5264..0227b9e501 100644 --- a/config.toml +++ b/config.toml @@ -124,4 +124,19 @@ rdi_current_version = "1.18.1" # Comment out if you don't want the "print entire section" link enabled. [outputs] section = ["HTML", "RSS", "Markdown", "JSON"] -page = ["HTML", "Markdown", "JSON"] \ No newline at end of file +page = ["HTML", "Markdown", "JSON"] + +# Content negotiation headers (hugo serve only). +# For production nginx config see AI_AGENT_DEVELOPER_GUIDE.md. +# Must be at end of file — TOML [table] sections apply to all following keys. +[server] + [[server.headers]] + for = "/**" + [server.headers.values] + Vary = "Accept" + + [[server.headers]] + for = "**.html.md" + [server.headers.values] + Content-Type = "text/markdown; charset=utf-8" + Vary = "Accept" \ No newline at end of file From 951b34e65c88a1b88543665185ae614fdfa1e729 Mon Sep 17 00:00:00 2001 From: mich-elle-luna Date: Thu, 25 Jun 2026 15:42:22 -0700 Subject: [PATCH 2/3] Fix content negotiation config: outputs, nginx, dev server note - Add home to [outputs] so the homepage and taxonomy pages emit .html.md and .json variants (previously fell back to [HTML, RSS]) - Fix nginx Content-Type: use types{} block instead of add_header so .md files get the correct MIME from the types table rather than a header that can't override nginx's own content-type - Add always flag to all Vary: Accept add_header directives so the header is sent on non-2xx responses (404/5xx) for cache correctness - Harden Accept map: exclude q=0 (explicit refusal) to avoid serving markdown to clients that don't want it - Fix rewrite regex: replace [^.]+ (skips dot-paths) with [^.]*[^/] so version directories (7.4/) and dotless paths are handled correctly - Clarify dev server limitation: hugo serve sets headers only and does not perform Accept-based rewriting; full negotiation requires nginx Co-Authored-By: Claude Sonnet 4.6 --- AI_AGENT_DEVELOPER_GUIDE.md | 46 ++++++++++++++++++++++++++----------- config.toml | 3 ++- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/AI_AGENT_DEVELOPER_GUIDE.md b/AI_AGENT_DEVELOPER_GUIDE.md index 731d987a38..466d9323be 100644 --- a/AI_AGENT_DEVELOPER_GUIDE.md +++ b/AI_AGENT_DEVELOPER_GUIDE.md @@ -216,32 +216,49 @@ When working on this project: ### Content Negotiation (text/markdown) -Every page generates a markdown version at `{path}index.html.md` (e.g. `/develop/index.html.md`). -The `` element is already present in every page's ``. +Every page — including the homepage — generates a markdown version at `{path}index.html.md` +(e.g. `/develop/index.html.md`). The `` element is +already present in every page's `` (generated by Hugo's `AlternativeOutputFormats` loop +in `baseof.html`). To enable HTTP-level content negotiation so `Accept: text/markdown` returns the markdown file with `Content-Type: text/markdown`, configure nginx as follows: ```nginx +# Honour q-values: match only when text/markdown appears without q=0. +# This is still a heuristic — full RFC 7231 q-value parsing requires Lua +# or a compiled module. For most clients this is sufficient. map $http_accept $use_markdown { - "~*text/markdown" 1; - default 0; + "~*text/markdown(?!;q=0)" 1; + default 0; } server { - # Serve .html.md files with correct Content-Type + # Declare the .md extension → MIME mapping so the type is set from the + # types table, not via add_header (which can't override Content-Type on + # files nginx serves directly). + types { + text/markdown md; + } + default_type application/octet-stream; + + # .html.md files served directly: MIME comes from the types block above. + # add_header always ensures Vary is sent on error responses too. location ~ \.html\.md$ { - add_header Content-Type "text/markdown; charset=utf-8"; - add_header Vary "Accept"; + add_header Vary "Accept" always; } location / { - add_header Vary "Accept"; + add_header Vary "Accept" always; - # Content negotiation: rewrite to .html.md when Accept: text/markdown + # Content negotiation: rewrite to .html.md when Accept: text/markdown. + # The first rewrite handles trailing-slash paths (/develop/). + # The second handles slash-terminated paths without a dot segment. + # Paths containing a dot (e.g. index.html, version dirs like 7.4/) + # are intentionally left to try_files so they resolve normally. if ($use_markdown) { - rewrite ^(.*/)$ $1index.html.md last; - rewrite ^([^.]+)$ $1/index.html.md last; + rewrite ^(.*/)$ $1index.html.md last; + rewrite ^(/[^.]*[^/])$ $1/index.html.md last; } try_files $uri $uri/ =404; @@ -249,8 +266,11 @@ server { } ``` -The `hugo serve` dev server already sends `Vary: Accept` and `Content-Type: text/markdown` -for `.html.md` files via the `[server]` config in `config.toml`. +**`hugo serve` note:** The `[server]` config in `config.toml` sets `Vary: Accept` and the +correct `Content-Type` for `.html.md` files during local development, but it does **not** +perform Accept-based rewriting. Requesting a page with `Accept: text/markdown` against +`hugo serve` still returns HTML. Full content negotiation requires the nginx config above +(or equivalent CDN rules) in production. --- diff --git a/config.toml b/config.toml index 0227b9e501..0d62ccc8ec 100644 --- a/config.toml +++ b/config.toml @@ -123,8 +123,9 @@ rdi_current_version = "1.18.1" # Comment out if you don't want the "print entire section" link enabled. [outputs] +home = ["HTML", "RSS", "Markdown", "JSON"] section = ["HTML", "RSS", "Markdown", "JSON"] -page = ["HTML", "Markdown", "JSON"] +page = ["HTML", "Markdown", "JSON"] # Content negotiation headers (hugo serve only). # For production nginx config see AI_AGENT_DEVELOPER_GUIDE.md. From d92baf286ddfe126904ac61e854c86cc62f2dcec Mon Sep 17 00:00:00 2001 From: mich-elle-luna Date: Tue, 30 Jun 2026 11:21:40 -0700 Subject: [PATCH 3/3] Add home page output layouts and remove nginx config from AI guide - Add layouts/home.md and layouts/home.json so Hugo generates Markdown and JSON output formats for the homepage (config.toml lists both in home outputs but had no corresponding templates) - Remove nginx content negotiation section from AI_AGENT_DEVELOPER_GUIDE.md; production routing config belongs in ops/infra docs, not the AI agent guide Co-Authored-By: Claude Sonnet 4.6 --- AI_AGENT_DEVELOPER_GUIDE.md | 62 ------------------------------------- layouts/home.json | 36 +++++++++++++++++++++ layouts/home.md | 16 ++++++++++ 3 files changed, 52 insertions(+), 62 deletions(-) create mode 100644 layouts/home.json create mode 100644 layouts/home.md diff --git a/AI_AGENT_DEVELOPER_GUIDE.md b/AI_AGENT_DEVELOPER_GUIDE.md index 466d9323be..661a655d87 100644 --- a/AI_AGENT_DEVELOPER_GUIDE.md +++ b/AI_AGENT_DEVELOPER_GUIDE.md @@ -212,68 +212,6 @@ When working on this project: --- -## Production Configuration for Agent Readiness - -### Content Negotiation (text/markdown) - -Every page — including the homepage — generates a markdown version at `{path}index.html.md` -(e.g. `/develop/index.html.md`). The `` element is -already present in every page's `` (generated by Hugo's `AlternativeOutputFormats` loop -in `baseof.html`). - -To enable HTTP-level content negotiation so `Accept: text/markdown` returns the markdown file -with `Content-Type: text/markdown`, configure nginx as follows: - -```nginx -# Honour q-values: match only when text/markdown appears without q=0. -# This is still a heuristic — full RFC 7231 q-value parsing requires Lua -# or a compiled module. For most clients this is sufficient. -map $http_accept $use_markdown { - "~*text/markdown(?!;q=0)" 1; - default 0; -} - -server { - # Declare the .md extension → MIME mapping so the type is set from the - # types table, not via add_header (which can't override Content-Type on - # files nginx serves directly). - types { - text/markdown md; - } - default_type application/octet-stream; - - # .html.md files served directly: MIME comes from the types block above. - # add_header always ensures Vary is sent on error responses too. - location ~ \.html\.md$ { - add_header Vary "Accept" always; - } - - location / { - add_header Vary "Accept" always; - - # Content negotiation: rewrite to .html.md when Accept: text/markdown. - # The first rewrite handles trailing-slash paths (/develop/). - # The second handles slash-terminated paths without a dot segment. - # Paths containing a dot (e.g. index.html, version dirs like 7.4/) - # are intentionally left to try_files so they resolve normally. - if ($use_markdown) { - rewrite ^(.*/)$ $1index.html.md last; - rewrite ^(/[^.]*[^/])$ $1/index.html.md last; - } - - try_files $uri $uri/ =404; - } -} -``` - -**`hugo serve` note:** The `[server]` config in `config.toml` sets `Vary: Accept` and the -correct `Content-Type` for `.html.md` files during local development, but it does **not** -perform Accept-based rewriting. Requesting a page with `Accept: text/markdown` against -`hugo serve` still returns HTML. Full content negotiation requires the nginx config above -(or equivalent CDN rules) in production. - ---- - **Last Updated**: 2026-01-08 **Purpose**: Help AI agents discover and understand Redis documentation architecture diff --git a/layouts/home.json b/layouts/home.json new file mode 100644 index 0000000000..0740cc4607 --- /dev/null +++ b/layouts/home.json @@ -0,0 +1,36 @@ +{{- /* AI-friendly JSON output for the home page */ -}} + +{{- $content := partial "process-markdown-content.html" (dict "RawContent" .RawContent "Site" .Site "Page" .) -}} + +{{- $summary := (.Params.description | default .Description) | plainify | replaceRE "\\s+" " " | strings.TrimSpace -}} +{{- $tags := .Params.categories | default (slice) -}} +{{- $lastUpdated := .Lastmod.Format "2006-01-02T15:04:05Z07:00" -}} + +{{- $children := slice -}} +{{- range .Pages -}} + {{- $childId := "" -}} + {{- with .File -}} + {{- $baseName := .ContentBaseName -}} + {{- if or (not $baseName) (eq $baseName "_index") -}} + {{- $childId = $.Title | urlize -}} + {{- else -}} + {{- $childId = $baseName | urlize -}} + {{- end -}} + {{- else -}} + {{- $childId = .Title | urlize -}} + {{- end -}} + {{- $childSummary := (.Params.description | default .Description) | plainify | replaceRE "\\s+" " " | strings.TrimSpace -}} + {{- $child := dict "id" $childId "title" .Title "url" .Permalink "summary" $childSummary -}} + {{- $children = $children | append $child -}} +{{- end -}} + +{ + "id": "home", + "title": {{ .Title | jsonify }}, + "url": {{ .Permalink | jsonify }}, + "summary": {{ $summary | jsonify }}, + "content": {{ $content | jsonify }}, + "tags": {{ $tags | jsonify }}, + "last_updated": {{ $lastUpdated | jsonify }}, + "children": {{ $children | jsonify }} +} diff --git a/layouts/home.md b/layouts/home.md new file mode 100644 index 0000000000..5522dc7098 --- /dev/null +++ b/layouts/home.md @@ -0,0 +1,16 @@ +# {{ .Title }} + +```json metadata +{ + "title": {{ .Title | jsonify }}, + "description": {{ (.Params.description | default .Description) | plainify | replaceRE "\\s+" " " | strings.TrimSpace | jsonify }}, + "categories": {{ .Params.categories | jsonify }}, + "tableOfContents": {{ partial "toc-from-markdown.html" . }}, + "codeExamples": {{ partial "code-examples-json.html" . }} +} +``` + +{{- /* Process content with shared partial (shortcode expansion, HTML unescaping, etc.) */ -}} +{{- $content := partial "process-markdown-content.html" (dict "RawContent" .RawContent "Site" .Site "Page" .) -}} + +{{ $content }}