Templates #
The templates directive is one of the most unique and powerful features of the Caddy web server. This feature lets Caddy process HTML files (or other text-based formats) served by the file_server directive dynamically on the server side before serving them to client browsers. Using Go’s built-in template engine (text/template and html/template), you can insert simple server-side logic like conditional branching (if/else), looping, including separate components (partial includes), formatting dates, reading client cookies, and even parsing Markdown files into HTML directly at the web proxy level. This feature is ideal for those wanting to build lightweight dynamic websites, landing pages, or interactive error page handling systems without needing to set up a separate backend application infrastructure. We’ll discuss in depth Caddy’s template rendering architecture, learn Go template logic syntax, access client request data, practice custom error page scenarios, and analyze critical security aspects to prevent Template Injection vulnerabilities.
Server-Side Rendering (SSR) Architecture in Caddy #
Traditionally, web servers like Nginx act as rigid static file servers. If you want to insert dynamic data (e.g., automatically displaying the current year in the page footer), you must forward the request to a backend application server (like PHP-FPM, Node.js, or Go) or rely on client-side rendering JavaScript.
Caddy breaks through this limitation by integrating a Server-Side Rendering (SSR) engine directly into its internal processing pipeline. When the templates directive is enabled, Caddy checks the requested text file before sending it to the network. If the file contains Go template syntax — marked by double curly braces {{ ... }} — Caddy parses and executes that logic in real time in RAM, replacing the template syntax with dynamic text content, and sends clean HTML to the client browser.
Because this evaluation is done on the server side, client browsers only receive a plain HTML document without knowing the page was dynamically generated. This is very friendly to search engine optimization (SEO) because all content is fully rendered before crawler robots read it.
Go Template Syntax and Logic in Caddy #
Caddy’s template engine inherits all the capabilities of Go’s proven-safe, fast built-in template library. Here are some basic syntax elements we often use to build web pages:
1. Text Evaluation and Dynamic Variables #
To display variable values or Caddy placeholders, write the variable name inside double curly braces:
<!-- Displaying the current server time -->
<p>The server time is now: {{ now | date "Mon, 02 Jan 2006 15:04:05 MST" }}</p>
2. Conditional Branching Logic (If/Else) #
You can selectively display certain HTML sections based on condition evaluation:
{{ if .Req.Header.Get "User-Agent" | contains "Mobile" }}
<div class="alert">We detect that you're accessing from a mobile device!</div>
{{ else }}
<div class="alert">Welcome, desktop user.</div>
{{ end }}
3. Looping (Range) #
You can iterate over a set of data — for example a list of files in a directory or a custom data array — using the range keyword:
<ul>
<!-- Iterating the file name list in the current folder -->
{{ range .Dir }}
<li><a href="{{ .Name }}">{{ .Name }} ({{ .Size }} bytes)</a></li>
{{ end }}
</ul>
4. Including Partial Files #
To keep your HTML code tidy and organized (not writing the same header and footer code repeatedly in every file), you can split it into separate files and import them back using the include function:
<!-- file: index.html -->
{{ include "partials/header.html" }}
<main>
<h1>Our Main Page</h1>
<p>This is the main content of the page.</p>
</main>
{{ include "partials/footer.html" }}
Accessing Client Request Data #
One of the biggest advantages of Caddy’s templates directive is its ability to read client HTTP request context directly. Caddy wraps this request data into special objects you can easily access inside your HTML body:
.Req: Accesses HTTP request information (like.Req.Method,.Req.URL.Path,.Req.Header)..Cookie: A special function for reading client cookie values (e.g.,{{ cookie "session_id" }})..Env: A function for reading OS environment variables (e.g.,{{ env "APP_ENV" }}).
Let’s look at the request processing flow by the templates directive in the following flowchart:
flowchart TD
A["1. Client HTTP Request Arrives"] --> B["2. Caddy file_server reads the HTML file from disk"]
B --> C{"3. Is the 'templates' directive active?"}
C -- "No" --> D["4. Serve the original static HTML file (200 OK)"]
C -- "Yes" --> E["5. The Caddy Engine parses the {{ ... }} tags"]
E --> F["6. Read runtime data\n(e.g. Cookie, HTTP Header, Client IP)"]
F --> G["7. Evaluate Go Template logic in RAM\n(e.g. render partials, looping)"]
G --> H["8. Generate the clean rendered HTML document"]
H --> I["9. Send the clean HTML to the client browser (200 OK)"]Complete List of Caddy Custom Template Functions #
Besides Go template’s standard built-in functions, Caddy injects dozens of custom functions specifically designed to simplify your web administration tasks. Here’s the detail table of the most important custom functions in Caddy:
| Function Name | Example Usage Syntax | Functionality Description |
|---|---|---|
include | {{ include "sidebar.html" }} | Reads and renders a separate HTML file internally. |
markdown | `{{ “# Hello” | markdown }}` |
cookie | {{ cookie "username" }} | Safely reads a value from the client’s cookies. |
env | {{ env "PORT" }} | Accesses the server OS environment variable values. |
placeholder | {{ placeholder "http.request.uri" }} | Reads all the built-in placeholders provided by the Caddy runtime. |
now | {{ now }} | Gets the current server time data. |
date | `{{ now | date “2006-01-02” }}` |
uuid | {{ uuid }} | Generates a random universal unique identifier (UUID v4) value. |
sha256 | `{{ “secret” | sha256 }}` |
stripHTML | `{{ “Hello” | stripHTML }}` |
Configuration to Enable Templates in the Caddyfile #
To enable the templates feature in the Caddyfile, just declare the templates directive inside your server route block.
# Example: Enabling Templates Globally
example.com {
root * /var/www/html
# ✓ CORRECT: Enable template processing for all text files (.html, .txt)
templates
file_server
}
Selective Configuration Using a Named Matcher #
In cases where you only want to enable template processing on certain HTML pages to save CPU parsing performance, you can limit it using a named matcher:
# Example: Enabling Templates Only for HTML Pages
example.com {
root * /var/www/html
# Create a named matcher to detect .html extension files
@html_only path *.html /
# Only run the template engine for HTML files
templates @html_only
file_server
}
Use Case 1: Dynamic Custom Error Pages #
When your server experiences problems — for example the backend database is down so it returns a 502 Bad Gateway status — it’s unprofessional to serve the server’s rigid, scary built-in error page to visitors.
You can use a combination of the handle_errors and templates directives in Caddy to render interactive custom error pages, complete with unique UUID tracking (Trace IDs) to ease your debugging process:
1. Caddyfile Error Handling Configuration #
# Example: Centralized Error Handling System
example.com {
# 1. Normal routes
handle {
reverse_proxy backend:8080
}
# 2. Handle HTTP errors from the backend / Caddy
handle_errors {
# Internally route the processing to the error.html file
rewrite * /error.html
# Enable templates so error.html can read the error code
templates
# Serve the file from the special error directory
file_server {
root /var/www/errors
}
}
}
2. Dynamic error.html File HTML Code
#
Create the /var/www/errors/error.html file using template variables to display the status code and error details:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Something Went Wrong - {{ .Placeholder "http.error.status_code" }}</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; background: #fafafa; color: #333; }
.card { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto; }
h1 { color: #d9534f; margin-top: 0; }
.trace-id { background: #eee; padding: 10px; font-family: monospace; font-size: 0.9em; border-radius: 4px; }
</style>
</head>
<body>
<div class="card">
<h1>Oops, Something Went Wrong!</h1>
<p>We apologize for the inconvenience. Your request couldn't be processed by our servers.</p>
<!-- Displaying the HTTP status code (e.g. 502, 404, 403) -->
<h3>Error Status: {{ .Placeholder "http.error.status_code" }} - {{ .Placeholder "http.error.status_text" }}</h3>
<p>Use the following Tracking ID if you want to report this issue to our technical team:</p>
<!-- Displaying the Unique Request ID (Trace ID) using the request UUID -->
<div class="trace-id">Trace ID: {{ .Placeholder "http.request.uuid" }}</div>
<p><a href="/">Back to the Home Page</a></p>
</div>
</body>
</html>
Use Case 2: Rendering Markdown to HTML Automatically #
For those managing simple documentation websites, writing files in HTML format can be very tedious. Caddy solves this problem by providing a built-in template function named markdown. This function parses Markdown-formatted text in real time into standard HTML tags.
1. Auto-Markdown Caddyfile Configuration #
# Example: Automatic Markdown Rendering
docs.example.com {
root * /var/www/docs
# Enable templates for all files
templates
# Fallback route: if the client requests a file without an extension,
# internally route to the .md file
try_files {path} {path}.md {path}/index.html
file_server
}
2. docs.md Wrapper HTML File (or index.html)
#
Create a wrapper template file acting as your main layout, which automatically reads and renders the Markdown file contents:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Our Technical Documentation</title>
<link rel="stylesheet" href="/css/docs-style.css">
</head>
<body>
<div class="sidebar">
<h3>Table of Contents</h3>
<ul>
<li><a href="/guide">Getting Started Guide</a></li>
<li><a href="/installation">Installation Process</a></li>
</ul>
</div>
<div class="content">
<!-- Caddy's markdown function reads the markdown file from the requested path -->
<!-- and dynamically renders it inside this div body -->
{{ if .Req.URL.Path | endsWith ".md" }}
{{ include .Req.URL.Path | markdown }}
{{ else }}
<!-- Default if the file doesn't end with .md -->
{{ include (printf "%s.md" .Req.URL.Path) | markdown }}
{{ end }}
</div>
</body>
</html>
Use Case 3: Creating a Dynamic XML Sitemap Serverlessly #
When building a frequently-updated static site, you must provide a sitemap.xml file so search engines can periodically index all your new files. Instead of using an external build script, you can have Caddy render the XML sitemap dynamically using the templates directive:
<!-- file: sitemap.xml -->
{{- /* Turn off Go template's default whitespace with the minus sign (-) */ -}}
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://example.com/</loc>
<lastmod>{{ now | date "2006-01-02" }}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
{{- range .Dir -}}
{{- if and (endsWith ".html" .Name) (ne .Name "error.html") (ne .Name "index.html") -}}
<url>
<loc>https://example.com/{{ .Name }}</loc>
<lastmod>{{ now | date "2006-01-02" }}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
{{- end -}}
{{- end -}}
</urlset>
In the Caddyfile, make sure you enable the Content-Type: application/xml header when the sitemap.xml file is accessed so browsers or search engine bots read it correctly.
Cache Management for Dynamic Templates #
Because HTML template files are evaluated dynamically when the request arrives, you must set up cache policies defensively. By default, the Caddyfile may include aggressive cache headers to speed up static asset loading. However, if client browsers permanently cache pages containing dynamic templates, they’ll never see your real-time data updates.
You must disable caching for your dynamic files:
# Example: Cache Management for Dynamic Templates
example.com {
root * /var/www/html
templates
# Set no-store cache-control specifically for our dynamic files
@dynamic_pages path *.html /
header @dynamic_pages Cache-Control "no-store, no-cache, must-revalidate"
file_server
}
Critical Security Aspects: Preventing Template Injection #
Although the templates directive provides incredible flexibility, if you’re not careful, this feature can open a serious security hole known as Server-Side Template Injection (SSTI).
The Dangers of SSTI and Credential Leakage #
If your HTML template file body writes raw input data from client query parameters directly without strict sanitization, attackers can insert malicious Go template functions to read sensitive information on your server.
<!-- ANTI-PATTERN: DON'T write client input directly -->
<!-- If an attacker sends the query: ?user={{ env "DATABASE_PASSWORD" }} -->
<!-- Caddy evaluates that tag and exposes our database password! -->
<h1>Hello, {{ .Req.URL.Query.Get "user" }}</h1>
Security Mitigation Solutions in Caddy #
- Use Automatic HTML Sanitization: Go templates by default use the safe
html/templateengine, which automatically escapes dangerous HTML characters (like changing<to<). However, for dynamic input data, you must be very vigilant. - Limit
envFunction Usage: Don’t use theenvfunction inside public HTML template files if those files can be edited or uploaded by external users. - Isolate Template Files: Make sure your web root directory doesn’t have sensitive configuration files readable using the
includefunction.
Summary #
- Server-Side Rendering: Caddy evaluates Go template tags
{{ ... }}in real time in memory before sending clean HTML documents to clients.- Go Template Engine: Inherits all the template writing logic capabilities of the Go language (variables, range loops, if/else conditions, partial includes).
- Client Parameter Access: Opens full access to read client HTTP request data, including Cookies, Headers, and IP addresses.
- Caddy Custom Functions: Provides additional functionality like
include,markdown,cookie,placeholder, and encryption/hashing functions.- Dynamic Error Pages: Makes creating interactive custom error handling pages with UUID tracking IDs easy.
- Markdown Parser: Provides the built-in
markdownfunction to convert.mddocument files into HTML layouts automatically at the server level.- Cache Control: Always set the
Cache-Control: no-storeheader on dynamic HTML file responses so client browsers don’t store stale data.- Security Mitigation: Avoid rendering raw client parameter input to prevent Server-Side Template Injection (SSTI) security holes.