Abhijit Hota
Vanity import paths in Go using Caddy
I recently published a small Go tool and wanted to publish it on my own domain like go.abhijithota.com/modfmt
(instead of github.com/abhijit-hota/modfmt
) leveraging “vanity” import paths. Since I already had a small VPS running Caddy, I thought it would be neat to make it work for this.
Vanity import paths
When you download a module via go get
or go install
, if the import URL is not a known VCS provider, it requests the URL over HTTPS/HTTP and expects an HTML response with a specific meta
tag in the head
which looks like:
<meta name="go-import" content="<import-prefix> <vcs> <repo-root>">
So for a package called go.abhijithota.com/modfmt
whose source code is hosted at a GitHub repo (https://github.com/abhijit-hota/modfmt
), the meta
tag would be:
<meta name="go-import" content="go.abhijithota.com/modfmt git https://github.com/abhijit-hota/modfmt">
Using Caddy to make the vanity URL work
The bare minimum thing you need to do is handle a specific path on your domain or subdomain to respond with the relevant HTML with the meta
tag. Your Caddyfile would look something like this:
go.abhijithota.com {
header Content-Type text/html
handle /modfmt {
respond <<HTML
<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="go.abhijithota.com/modfmt git https://github.com/abhijit-hota/modfmt">
</head>
</html>
HTML 200
}
handle {
redir https://abhijithota.com # or wherever you want to
}
}
This gives you a working vanity URL which you can use to install and get modules from.
Distinguishing requests from Go
Go appends a ?go-get=1
query parameter to the HTTPS request so that we can distinguish between normal queries and queries from the Go tools (go get
, etc.). We can use this to distinguish between what to do when someone visits the URL via a browser, etc. For instance, we can choose to redirect to our original GitHub page:
go.abhijithota.com {
# For path /modfmt
handle /modfmt {
# Check if the request has a ?go-get=1 appended by Go
@from_go query go-get=1
# If yes, then send back the required HTML
handle @from_go {
header Content-Type text/html
respond <<HTML
<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="go.abhijithota.com/modfmt git https://github.com/abhijit-hota/modfmt">
</head>
</html>
HTML 200
}
# If not, then redirect to our GitHub repo
handle {
redir https://github.com/abhijit-hota/modfmt # You can also redirect to the pkg.go.dev page for the module
}
}
handle {
redir https://abhijithota.com
}
}
The order of Caddy’s handle
directives act like a fallthrough. If the first one is not matched then the next one is checked and so on. In the above case, we check the request for a ?go-get=1
query parameter using a named matcher. If the match is successful then we send the HTML required. If it isn’t, then the next in chain is used. Here, we have a handle
which basically matches everything. In our case, we redirect to GitHub
We should also probably not leave the HTML page blank. The easiest thing to do here is use a http-equiv="refresh"
to redirect to your GitHub repo:
<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="go.abhijithota.com/modfmt git https://github.com/abhijit-hota/modfmt">
<meta http-equiv="refresh" content="0; url=https://github.com/abhijit-hota/modfmt" />
</head>
</html>
Go to https://go.abhijithota.com/modfmt?go-get=1 to see the redirect in action.
If you don’t want to redirect, you can write some basic HTML and CSS with the repo and docs URL and the installation instructions. At that point you might want to serve an HTML file directly.
Generalizing our setup
We don’t want to repeat the handle /modfmt
block every time we want to publish a package. We can leverage Caddy’s snippets and import
directive to repeat the same pattern for a number of packages.
(gomodhandler) {
handle /{args[0]} {
@from_go query go-get=1
handle @from_go {
header Content-Type text/html
respond <<HTML
<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="go.abhijithota.com/{args[0]} git https://github.com/abhijit-hota/{args[0]}">
<meta http-equiv="refresh" content="0; url=https://github.com/abhijit-hota/{args[0]}" />
</head>
</html>
HTML 200
}
handle {
redir https://github.com/abhijit-hota/{args[0]}
}
}
}
go.abhijithota.com {
import gomodhandler modfmt
import gomodhandler foo
handle {
redir https://abhijithota.com
}
}
The above Caddyfile introduces a snippet called gomodhandler
which is basically the same /modfmt
handler as we saw before but replaced with arguments. Think about it as a way to template the configuration from the configuration itself. When you import
a snippet, that snippet gets inlined with the given arguments.
For comparison, here is the same configuration without snippets:
Caddyfile
go.abhijithota.com { handle /modfmt { @from_go query go-get=1 handle @from_go { header Content-Type text/html respond <<HTML <!DOCTYPE html> <html> <head> <meta name="go-import" content="go.abhijithota.com/modfmt git https://github.com/abhijit-hota/modfmt"> <meta http-equiv="refresh" content="0; url=https://github.com/abhijit-hota/modfmt" /> </head> </html> HTML 200 } handle { redir https://github.com/abhijit-hota/modfmt } } handle /foo { @from_go query go-get=1 handle @from_go { header Content-Type text/html respond <<HTML <!DOCTYPE html> <html> <head> <meta name="go-import" content="go.abhijithota.com/foo git https://github.com/abhijit-hota/foo"> <meta http-equiv="refresh" content="0; url=https://github.com/abhijit-hota/foo" /> </head> </html> HTML 200 } handle { redir https://github.com/abhijit-hota/foo } } handle { redir https://abhijithota.com } }
This setup will have the following behavior for modfmt
and foo
:
- Installing the module
go.abhijithota.com/<pkg>
viago get
orgo install
will install the module from the GitHub repo. - Visiting
go.abhijithota.com/<pkg>
will redirect to the respective GitHub repo. - Visiting
go.abhijithota.com/<pkg>?go-get=1
will return the HTML with themeta
tag and redirect to the respective GitHub repo due to thehttp-equiv="refresh"
tag. - Visiting any path on
go.abhijithota.com
other thanmodfmt
orfoo
will redirect toabhijithota.com
.
We are all set now to release our Go modules on our own domains or subdomains with vanity URLs.
Acknowledgement
This post was inspired by Márk Sági-Kazár’s post on vanity import paths in Go which talks about their use-cases, using them effectively, best practices and much more in detail.