https://github.com/avvertix/caddy-content-negotiation
Content negotiation module for serving to AI Agents
https://github.com/avvertix/caddy-content-negotiation
Last synced: 3 months ago
JSON representation
Content negotiation module for serving to AI Agents
- Host: GitHub
- URL: https://github.com/avvertix/caddy-content-negotiation
- Owner: avvertix
- License: mit
- Created: 2026-03-17T17:19:16.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-04-24T18:54:58.000Z (3 months ago)
- Last Synced: 2026-04-24T20:33:11.589Z (3 months ago)
- Language: Go
- Size: 76.2 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Content Negotiation module for Caddy
A Caddy middleware module that intercepts HTTP requests containing `Accept: text/markdown` and serves precomputed `.md` files located alongside the originally requested resources.
## How It Works
When a client sends a request with `Accept: text/markdown` (or `text/x-markdown`) in the header, this middleware:
1. Determines which `.md` file corresponds to the requested path
2. Checks if that `.md` file exists on disk
3. If found, serves the markdown content with `Content-Type: text/markdown; charset=utf-8`
4. If not found, passes the request to the next handler as normal (or returns `406` in strict mode)
When `strict_mode` is enabled, the middleware also rejects requests whose `Accept` header contains only types incompatible with text content (e.g. `image/png`) with `406 Not Acceptable`, before any file lookup takes place.
### Path Resolution Examples
| Request Path | Markdown File Checked |
|---|---|
| `/docs/page.html` | `/docs/page.md` |
| `/docs/page.php` | `/docs/page.md` |
| `/docs/` | `/docs/index.md` |
| `/about` | `/about.md` |
| `/` | `/index.md` |
## Installation
### Using xcaddy
Build Caddy with this module using `xcaddy`:
```bash
xcaddy build --with github.com/avvertix/caddy-content-negotiation
```
### Using Docker
A sample Docker setup is included. It builds a custom Caddy image with the
module baked in and serves the demo content in `docker/content/`.
```bash
# Build and start
docker compose up --build
# Test content negotiation
curl -H "Accept: text/markdown" http://localhost/
curl -H "Accept: text/markdown" http://localhost/docs/page.html
curl -H "Accept: text/markdown" http://localhost/about
```
To use your own content, mount a volume over `/srv` in `docker-compose.yml`
or copy files into `docker/content/` before building.
## Caddyfile Configuration
### Minimal
`markdown_intercept` is not a standard ordered directive, so you must register
its position in the global options block:
```caddyfile
{
order markdown_intercept before file_server
}
example.com {
markdown_intercept
file_server
}
```
### Full Options
```caddyfile
{
order markdown_intercept before file_server
}
example.com {
markdown_intercept {
root /var/www/html
index_names index.html index.htm index.php
extensions .html .htm .php .txt
experimental_range_requests
strict_mode
}
file_server
}
```
### Directives
| Directive | Default | Description |
|---|---|---|
| `root` | Site root (`{http.vars.root}`) | Filesystem path to look for `.md` files |
| `index_names` | `index.html index.htm index.php` | Index filenames to try for directory requests |
| `extensions` | `.html .htm .php .txt` | File extensions eligible for `.md` substitution |
| `experimental_range_requests` | disabled | Enable the `x-frontmatter` range unit (see below) |
| `strict_mode` | disabled | Reject unsupported `Accept` types with `406` (see below) |
## JSON Configuration
```json
{
"handler": "markdown_intercept",
"root": "/var/www/html",
"index_names": ["index.html", "index.htm"],
"extensions": [".html", ".htm", ".php"],
"experimental_range_requests": true,
"strict_mode": true
}
```
## Client Usage
Request markdown from any endpoint by setting the `Accept` header:
```bash
# Get the markdown version of a page
curl -H "Accept: text/markdown" https://example.com/docs/page.html
# Normal requests are unaffected
curl https://example.com/docs/page.html
```
### Frontmatter range requests (experimental)
When `experimental_range_requests` is enabled, clients can request only the
frontmatter block of a markdown file using the non-standard `x-frontmatter`
[range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests) unit:
```bash
curl -H "Accept: text/markdown" \
-H "Range: x-frontmatter" \
https://example.com/docs/page.html
```
The server responds with `206 Partial Content` and only the frontmatter section
(the content between the opening and closing `---` delimiters). If the file has
no frontmatter block, the server returns `416 Range Not Satisfiable`.
When the feature is enabled, every markdown response includes
`Accept-Ranges: x-frontmatter` so clients can discover support before issuing a
range request.
### Strict content-type negotiation
When `strict_mode` is enabled the middleware enforces two rules:
**1. Unsupported `Accept` types are rejected with `406 Not Acceptable`**
If the request's `Accept` header contains only types outside the `text/*` family
and no `*/*` wildcard, the middleware returns `406` immediately without
performing any file lookup or calling the next handler. This rejects probes and
requests for content the server cannot produce for text-based resources:
```bash
# Rejected — not a text type
curl -i -H "Accept: image/png" https://example.com/docs/page.html
# → 406 Not Acceptable
curl -i -H "Accept: application/x-content-negotiation-probe" https://example.com/about
# → 406 Not Acceptable
```
Requests that include at least one compatible type are allowed through:
```bash
# Allowed — text/html matches text/*
curl -i -H "Accept: text/html, application/json" https://example.com/docs/page.html
# Allowed — wildcard covers everything
curl -i -H "Accept: */*" https://example.com/docs/page.html
```
**2. Missing markdown files return `406` instead of passing through**
When the client explicitly requests `text/markdown` but no `.md` file exists for
the requested path, the middleware returns `406` rather than forwarding the
request to the next handler:
```bash
# 406 if /docs/page.md does not exist
curl -i -H "Accept: text/markdown" https://example.com/docs/page.html
```
Without `strict_mode` the same request would be forwarded to the next handler
(e.g. a file server that serves the HTML version), and the `X-Content-Md:
requested` header would be added to the forwarded request.
## Response Headers
When a markdown file is served, the response includes:
- `Content-Type: text/markdown; charset=utf-8`
- `Accept-Ranges: x-frontmatter` (only when `experimental_range_requests` is enabled)
A `206 Partial Content` frontmatter response additionally includes:
- `Content-Range: x-frontmatter 0-/` — byte offsets of the frontmatter block within the full file
## Development
```bash
# Run tests
go test -v -race ./...
# Build Caddy locally with the module (requires xcaddy)
xcaddy build --with github.com/avvertix/caddy-content-negotiation=.
```