When writing Go code you almost certainly import some kind of third-party package. Commonly, such third-party packages are coming from GitHub and are added with their full URL to Github project, like so:

import (
    "github.com/octocat/phantom"
)

When you are writing your own packages and/ or libraries, it can become both repetitive, long and limiting to be refering to these packages via github.com/<user>/<package>, especially if both your username and package names are long. Additionally, if you choose to move from GitHub to let’s say GitLab or Bitbucket - you have to change the imports in all of your projects.

And what if your projects are used by others? Then you have just broken everything for them - remember this nasty issue with github.com/sirupsen/logrus rename (a simple case change in username)?

Wouldn’t it be so much nicer if we could use a custom domain instead of github.com/<username>? Something that would make our imports look like below. For the sake of example we will be aliasing a made-up github.com/octocat/phantom package to octo.io/phantom (also made up).

import (
    "octo.io/phantom"
    // vs
    "github.com/octocat/phantom"
)

Well, I wouldn’t be writing this post if we couldn’t! Usage of custom domains to shorten the import paths like above is called Vanity URLs and is a very common thing to do (given you have a good short domain). Kubernetes this using this for one specific repo( instead of github.com/kubernetes they simply use k8s.io - 15 characters shorter on every import statement eases readability quite a lot.

Requirements

How any remote import works is explained in Go Docs - Cmd: Remote Import Paths.

For code hosted on other servers, import paths may either be qualified with the version control type, or the go tool can dynamically fetch the import path over https/http and discover where the code resides from a <meta> tag in the HTML.

Basically, To make Go tooling understand vanity urls (which are treated as any other remote import path), server must respond with a special <meta name="go-import" content="import-prefix vcs repo-root"> tag.

Setup

The way we will achieve this is by utilizing permalink feature of Jekyll and a custom layout specifically designed for our packages.

The way it will work is that we will be create a file per package we want to expose. I prefer to put defintion of my repositories in a separate folder, e.g go-imports.

Package definition
Create a new file in for the package go-imports/phantom.md. In the newly created file put the following Front Matter definition:

---
repo: phantom
---

This is all we need to define that our package repository is called phantom. We could also specify branch: v1 for v1 branch, later we will ensure master is the default branch

Site Config
In the site config _config.yml define the following:

domain: octo.io
github_username:  octocat

defaults:
  - scope:
      path: "go-imports"
    values:
      layout: go-imports
      permalink: /:basename
      branch: master

What the defaults above allow us to do is skip repetitiveness of settings in Front Matter for each package.

Layout definition
Create a new layout in _layouts, e.g _layouts/go-imports.html with following contents:

<!DOCTYPE html>{% assign package_name = page.name | remove: ".md" %}
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en-us">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">

  <meta name="go-import" content="{{ site.domain }}/{{ package_name }} git https://github.com/{{ site.github_username }}/{{ page.repo }}">
  <meta name="go-source" content="{{ site.domain }}/{{ package_name }} _ https://github.com/{{ site.github_username }}/{{ page.repo }}/tree/{{ page.branch }}{/dir}
  https://github.com/{{ site.github_username }}/{{ page.repo }}/blob/{{ page.branch }}{/dir}/{file}#L{line}">

  <meta http-equiv="refresh" content="0; https://github.com/{{ site.github_username }}/{{ page.repo }}">

</head>
<body>
</body>
</html> 

A few things are going on here:

  • First, we parse the page name and remove the file extension (.md in our case), and assing result to package_name variable. This is technically not needed if you are fine setting package name in Front Matter - but I wanted to remove the repetitive operations.
  • Now we generate the go-import meta-tag. If you are not on github - it’s easy to change.
  • We also generate a go-source meta-tag that allows a jump to source from godoc and other clients.
  • Last, we add a page-refresh that will lead to the repo on GitHub if this page is opened via browser.

Test it After pushing these changes, you should be able to use curl to validate it: curl https://octo.io/phantom.

This should produce something like this:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en-us">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">

  <meta name="go-import" content="octo.io/phantom git https://github.com/octocat/phantom">
  <meta name="go-source" content="octo.io/phantom _ https://github.com/octocat/phantom/tree/master{/dir}
  https://github.com/octocat/phantom/blob/master{/dir}/{file}#L{line}">

  <meta http-equiv="refresh" content="0; https://github.com/octocat/phantom">

</head>
<body>
</body>
</html>

You can also just try running go get octo.io/phantom.

Limitations

This setup does come with some limitations, and I’ll outline some of them here:

  • We have to manually create a file per package (luckily its very small)
  • As site is static - we cannot add meta tags only when ?go-get=1 is in the query string
  • We are using permalinks, which means you cannot have pages with the same name as your packages.

Conclusion

That’s it! Using GitHub Pages and static site generation with Jekyll is a quick and easy way to get vanity url support for your Go packages.

If you need more dynamic processing or more flexibility, you can easily write one yourself (in Go, naturally!) or use a readily available projects such as https://github.com/GoogleCloudPlatform/govanityurls/. But this option comes with requirement of hosting this app yourself.