Vanity import paths in Go using Caddy

4 minute read for ~900 words

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> via go get or go 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 the meta tag and redirect to the respective GitHub repo due to the http-equiv="refresh" tag.
  • Visiting any path on go.abhijithota.com other than modfmt or foo will redirect to abhijithota.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.