The most surprising thing about Caddy’s template middleware is that it doesn’t actually render anything itself; it delegates the entire rendering process to an external Go template engine.
Let’s see it in action. Imagine you have a simple web application that needs to display the current time and a user’s name, which might be passed as a query parameter.
Here’s a Caddyfile snippet that accomplishes this:
:8080 {
templates {
mime text/html
default /index.html
}
file_server
}
And here’s the index.html file that Caddy will serve and process:
<!DOCTYPE html>
<html>
<head>
<title>Dynamic Content</title>
</head>
<body>
<h1>Hello, {{.Name}}!</h1>
<p>The current time is: {{.Time}}</p>
</body>
</html>
When a user navigates to http://localhost:8080/?name=Alice, Caddy’s templates middleware intercepts the request for /index.html. It sees the {{.Name}} and {{.Time}} placeholders. It then executes the Go template engine, passing it a context object. This context object is populated by Caddy, making the name query parameter available as .Name and the current time as .Time. The engine renders the HTML, and Caddy serves the resulting text/html content.
The problem this solves is injecting dynamic data into static files without needing a full-blown application server for every page. Think of it as a lightweight way to personalize responses, serve configuration-driven content, or even build simple dashboards directly from static HTML. Caddy’s templates middleware acts as a bridge, taking requests, identifying template files (based on the default directive or matching file extensions if configured), and orchestrating the rendering process by feeding data to the Go template engine.
Internally, Caddy uses Go’s standard html/template package. When the templates directive is enabled, Caddy automatically sets up a template execution environment. It makes request-specific data available to the template context. This includes query parameters, request headers, cookies, and even parsed URL path segments. The mime directive tells Caddy what Content-Type header to set for the rendered output, ensuring the browser interprets it correctly. The default directive specifies the template file to render if no specific file is requested or found.
The exact levers you control are primarily within the Caddyfile and the content of your template files. You can define multiple templates blocks to apply different configurations to different paths. You can also control which files are treated as templates by default or by explicitly naming them. The data you can inject into templates is vast, coming directly from the incoming HTTP request.
One of the most powerful, yet often overlooked, aspects of Caddy’s template middleware is its ability to access and manipulate request headers and cookies directly within the template context. For example, if you have a request header X-User-ID, you can access it in your template as {{.Request.Header.Get "X-User-ID"}}. This allows for highly personalized content based on authentication tokens, A/B testing flags, or other metadata passed via headers. It’s not just query parameters; the entire request object is often available, giving you fine-grained control over what dynamic data gets rendered.
The next logical step after rendering dynamic content is to manage complex application state across multiple requests.