markdownintercept

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: MIT Imports: 16 Imported by: 0

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:

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/.

# 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:

{
    order markdown_intercept before file_server
}

example.com {
    markdown_intercept
    file_server
}
Full Options
{
    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

{
  "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:

# 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 unit:

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:

# 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:

# 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:

# 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-<end>/<total> — byte offsets of the frontmatter block within the full file

Development

# Run tests
go test -v -race ./...

# Build Caddy locally with the module (requires xcaddy)
xcaddy build --with github.com/avvertix/caddy-content-negotiation=.

Documentation

Overview

Package markdownintercept provides a Caddy HTTP middleware that intercepts requests with an Accept header requesting markdown (text/markdown) and serves a precomputed .md file located alongside the originally requested resource.

For example:

  • GET /docs/page.html with Accept: text/markdown → serves /docs/page.md
  • GET /docs/ with Accept: text/markdown → serves /docs/index.md
  • GET /about with Accept: text/markdown → serves /about.md

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type MarkdownIntercept

type MarkdownIntercept struct {
	// Root is the filesystem path from which to look for .md files.
	// Defaults to the current working directory or Caddy's configured root.
	Root string `json:"root,omitempty"`

	// IndexNames is the list of default index filenames to try when the
	// request path ends with "/". Defaults to ["index.html", "index.htm", "index.php"].
	IndexNames []string `json:"index_names,omitempty"`

	// Extensions is the list of file extensions to consider when rewriting
	// to .md. Defaults to [".html", ".htm", ".php", ".txt"].
	Extensions []string `json:"extensions,omitempty"`

	// ExperimentalRangeRequests enables support for the non-standard
	// Range: x-frontmatter request unit, which returns only the YAML/TOML
	// frontmatter block of a markdown file (206 Partial Content).
	// Disabled by default.
	ExperimentalRangeRequests bool `json:"experimental_range_requests,omitempty"`

	// StrictMode, when enabled, enforces strict content-type negotiation:
	//   - Requests whose Accept header contains no type compatible with
	//     text/* or */* are rejected with 406 Not Acceptable.
	//   - Requests that explicitly ask for text/markdown but have no
	//     corresponding .md file are also rejected with 406.
	// Disabled by default.
	StrictMode bool `json:"strict_mode,omitempty"`
	// contains filtered or unexported fields
}

MarkdownIntercept is a Caddy middleware that checks if the client accepts text/markdown. If so, it looks for a .md file corresponding to the requested path and serves it instead of delegating to the next handler.

func (MarkdownIntercept) CaddyModule

func (MarkdownIntercept) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*MarkdownIntercept) Provision

func (m *MarkdownIntercept) Provision(ctx caddy.Context) error

Provision sets up the module.

func (MarkdownIntercept) ServeHTTP

ServeHTTP implements caddyhttp.MiddlewareHandler.

func (*MarkdownIntercept) UnmarshalCaddyfile

func (m *MarkdownIntercept) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile sets up the module from Caddyfile tokens.

Syntax:

markdown_intercept {
    root <path>
    index_names <name1> <name2> ...
    extensions <.ext1> <.ext2> ...
    experimental_range_requests
    strict_mode
}

func (*MarkdownIntercept) Validate

func (m *MarkdownIntercept) Validate() error

Validate ensures the module configuration is valid.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL