Vanity, thy name is Go

Setting up Go vanity import URLs with Bunny.net

Go vanity imports let you put a custom domain in front of your import paths. Instead of go get gitea.com/user/repo/pkg (or in my case go get src.tklk.dev/tklk/x/pkg), it's just go get tklk.dev/pkg. Shorter, and decoupled from wherever the code actually lives.

After moving everything into a monorepo, this became trivial to set up. One repo means a single vanity domain covers all packages under it. I got it working with Bunny.net using nothing but a custom 404 page. No servers, no edge compute, no edge rules. Just a static HTML file.

How Go vanity imports work#

When you run go get tklk.dev/somepkg, the Go toolchain makes an HTTP request to https://tklk.dev/somepkg?go-get=1 and parses the HTML response. It looks for a <meta name="go-import"> tag in the <head>, which tells it where the actual source code lives.

The meta tag has three space-separated values in its content attribute: the import prefix, the VCS type, and the repository URL.

<meta name="go-import" content="tklk.dev git https://src.tklk.dev/tklk/x">

For a monorepo, all subpaths under the import prefix resolve to the same repo root. tklk.dev/somepkg and tklk.dev/otherpkg both point at https://src.tklk.dev/tklk/x. The Go toolchain handles the rest, mapping the subpath to a directory within the repository.

The trick: a custom 404 page#

For most cases, the Go toolchain does not care about the HTTP status code. It fetches the URL and parses the HTML looking for the meta tag. A 200 and a 404 work equally well, as long as the response body contains what it needs.

A redirect to Gitea would've been simpler, but the Go toolchain reads the meta tag from the final response after following redirects. Gitea serves its own go-import meta tag with the repo path as the import prefix, so a redirect would make the toolchain see the Gitea hostname instead of the vanity domain. The 404 page sidesteps this because the response comes from the vanity domain itself.

Bunny.net lets you configure a custom error page for a pull zone. Set that 404 page to an HTML file containing the go-import meta tag, and every path under tklk.dev/anything that doesn't match a real file returns the custom error page. The Go toolchain reads the meta tag and resolves the import.

The full 404.html:

<!DOCTYPE html>
<html>
<head>
  <meta name="go-import" content="tklk.dev git https://src.tklk.dev/tklk/x">
</head>
<body>
  <p>Not found. See <a href="https://src.tklk.dev/tklk/x">source</a>.</p>
</body>
</html>

That's the entire thing. The go-import tag tells Go where the repo is.

For this specific pullzone of mine, it also serves a few real files, such as my Docker registry at /v2/, and a few other things that don't conflict with package names, so when the custom 404 fires for paths that don't match anything else it's still fine.

Setting it up#

If you're already using Bunny.net for static hosting, this is a small addition. I covered the basics of creating storage zones and pull zones in a previous post.

To set it up:

  1. Create a storage zone and pull zone for the vanity domain (or reuse an existing one).
  2. Upload the 404.html to the storage zone. Bunny expects custom error pages at bunnycdn_errors/404.html, or if you wanted it somewhere else, you can specify the path in the storage zone settings.
  3. In the pull zone settings, set the storage zone as the origin.
  4. Add a DNS CNAME record pointing tklk.dev to the pull zone hostname.

Testing it#

A quick curl confirms it's working:

curl -s 'https://tklk.dev/somepkg?go-get=1'

You should see the HTML with the meta tag come back, even though the response status is 404. From there, go get tklk.dev/somepkg resolves to the Gitea repo and pulls the code down.

Clean import paths, zero infrastructure to run, and it costs nothing beyond what the CDN already costs. The monorepo is what makes the whole setup so simple. If the domain fronted multiple independent repos instead, each import path would need its own meta tag pointing at a different repository, and you'd need edge compute or a server to route them. I have a separate project, go-vanity-url, that handles that case by generating a set of static HTML files from a TOML config, one per package, each with the correct go-import meta tag. You can host the output on any static file server or CDN. With a monorepo though, none of that is necessary. One meta tag covers everything, so a static 404 page is enough.