Caddy snippets for static sites

I moved my website from WordPress to a static site generator in 2014, and over the next few months, I wrote several posts about how I achieved certain dynamic behavior using custom nginx configurations. However, I switched over to Caddy as my web server in 2017, but I never updated how I adapted my server configuration. In basically all cases, I find the Caddy config much simpler and easier to read, though that may be because it’s all I ever use anymore. So here is my long overdue updates to a few old blog posts about adding some custom web server behavior for static sites.

Supporting WebFinger

I July 2014, I wrote Supporting WebFinger with Static Files and Nginx. I still use Webfinger, now primarily for my custom Mastodon server and most recently with OpenID Connect for Tailscale. My old nginx config required lua support to be compiled in, which wasn’t awful, but kind of annoying. My Caddy configuration is mostly equivalent, though I didn’t bother to return the proper 400 and 405 status codes on an incorrect resource parameter or HTTP method. Instead, they just return a 404 which suits me just fine.

I define a named matcher that matches on the webfinger well-known URL, the HTTP methods I want to support, and one of several valid resource values. Then I rewrite the request to a static file like before and set some response headers.

@webfinger {
  path /.well-known/webfinger
  method GET HEAD
  query resource=
  query resource=
rewrite @webfinger /webfinger.json
header @webfinger {
  Content-Type "application/jrd+json"
  Access-Control-Allow-Origin "*"
  X-Robots-Tag "noindex"

Proxying webmentions

In August 2014, I wrote Proxying webmentions with nginx. I still proxy my webmentions to an external service, though I now use The config requires a tiny bit more work because my URL path didn’t match where I needed to send it, but it is still pretty straightforward.

Like before, I use a named matcher to match the relevant requests, then use Caddy’s reverse_proxy directive to send them to

@webmention {
  method POST
  path /api/webmention/
handle @webmention {
  uri replace /api/webmention/ /
  reverse_proxy {
    header_up Host {upstream_hostport}

Fetching go packages

In February 2015, I wrote Fetching Go Sub-Packages on Static Sites. Unsurprisingly, I still use my own domain in the import path of all of my go packages. I currently use Hugo to generate my site, so I have a custom layout for my go package files which reads relevant metadata from the page front matter and populates the necessary meta tags.

To serve the right page on go get requests for sub-packages, the Caddy config is quite minimal. A named matcher is used to match requests for go sub-packages that include the go-get parameter, and then serve the contents of the top-level go package file without the sub-package.

@gopkg {
  path_regexp gopkg (/go/\w+/).+
  query go-get=*
rewrite @gopkg {re.gopkg.1}

Do more with custom Caddy modules

I’ve also done a lot more interesting things with custom Caddy modules like embedding my imageproxy service as well as a Tailscale node directly into the Caddy binary. But that will be a topic for another day.

Comments and responses

Have you written a response to this? Let me know the URL:

Oh, lovely!

I’ve been toying with the idea of switching to Caddy for several weeks, and this writeup will come in very handy if I eventually do.