An opinionated, minimalist, static site generator CLI for redirecting vanity Go module URLs. Take control of your Go modules by owning your namespace.
Go is unique. Unlike other languages with centralized package registries (npm, PyPI, Crates.io, etc.), Go embraced a fully distributed module system. Your domain is your namespace. Yet GitHub has become the de facto registry, bringing vendor lock-in with it. Vanity fixes that by giving you control on how users go get and go install your code.
GitHub doesn't own your code; you do.
Owning your Go module namespace makes you sovereign over your code. This sets you free to switch to any git provider behind the scenes with minimal disruption to your users. No more vendor lock-in.
Vanity generates plain-Jane static HTML pages. No servers, no databases, no runtime dependencies, no BS. Point to your domain, push your pages, and you're done.
Vanity makes decisions so you don't have to. With built-in templates, most users won't need to configure anything beyond their domain and list of modules, but you can customize this further if you bring your own templates.
You can install vanity directly with your Go toolchain.
go install go.treyburn.dev/vanity@latestOnce installed, you can bootstrap your local config with the just required fields.
vanity initVanity is driven by a single YAML file and a handful of CLI commands. See CLI Commands for available commands and Configuration for setting up your .vanity.yml.
For most users, it's as simple to get going as running vanity init, filling in a few required fields, then running vanity generate. With that you're ready to ship your static pages.
Most users can use the following commands as a basic quickstart to get their pages generated.
# install Vanity
go install go.treyburn.dev/vanity@latest
# move to the dir you want to store these pages in
cd ~/repo/deployment
# generate your config file
vanity init
# add your domain and module redirect info and save
vi .vanity.yml
# check that everything is configured correct
vanity check
# generate your pages
vanity generate
# run a localhost server with some sample curl calls to validate
vanity serve
# and now you're ready to publish
scp -r ./dist [email protected]If you want to see the full set of commands and flags available you can simply run vanity --help.
# vanity --help
Usage: vanity <command> [flags]
A static site generator CLI for Go vanity URLs.
Flags:
-h, --help Show context-sensitive help.
--log-level=STRING Override log level (debug, info, warn, error) ($VANITY_LOG_LEVEL).
--log-format=STRING Override log format (text, json) ($VANITY_LOG_FORMAT).
Commands:
init [flags]
Generate a minimal .vanity.yml config file (use --verbose for full spec).
generate [flags]
Generate static HTML files from configuration.
check [flags]
Validate the .vanity.yml configuration.
preview [flags]
Generate in-memory and serve via local HTTP.
serve [flags]
Serve already-generated output directory.
clean [flags]
Remove the generated output directory.
version [flags]
Print version information.
Help:
Run "vanity <command> --help" for more information on a command.All configuration is handled via the .vanity.yml YAML file. You can generate a minimal configuration file with just the required fields by running:name: release.
vanity initOr you can generate a full configuration on all possible fields (ore-filled with example values and comments) by running:
vanity init --verboseSee the Configuration Reference for the full set of all available configurations.
The following fields are required to be populated.
# .vanity.yml
# Your vanity URL domain (may include subdomains and url paths)
domain: example.com # REQUIRED
# Module definitions (at least one required)
modules:
- name: my-module # REQUIRED: import path becomes {domain}/{module.name}
repo: https://github.com/example/my-module # REQUIRED: full git repository URLAs you might have guessed, Vanity uses Vanity to provide it's vanity URL redirection.
I've set this up to push to Cloudflare Pages for my go.treyburn.dev domain as an action on release in this repository. It kicks offs an action to update and publish in another repo. You can check out the repository for that here, and I have a small write-up on my blog explaining how it works.
# .vanity.yml
# CLI behavior (overridable via --flags and VANITY_* env vars)
log:
level: info # Options: debug | info | warn | error
format: text # Options: text (human-friendly) | json (structured)
color: true # Colorize text output (no effect on json)
# Output settings
output:
dir: dist # Relative to .vanity.yml location
clean: true # Remove output dir before generating
index: true # Generate root index.html listing all modules
not_found: true # Generate 404.html redirecting to index
robots: true # Generate robots.txt (permissive, links to sitemap)
sitemap: true # Generate sitemap.xml listing all module URLs
# Your vanity URL domain (may include subdomains and url paths)
domain: example.com # REQUIRED
# Default values applied to all modules (overridable per-module)
defaults:
branch: main # Used in go-source meta tag URL templates (defaults to 'main')
go_source: true # Include go-source meta tag for pkg.go.dev source links
redirect_root: https://pkg.go.dev # Redirect = redirect_root/domain/name
# Module definitions (at least one required)
modules:
- name: my-module # REQUIRED: import path becomes {domain}/{module.name}
repo: https://github.com/example/my-module # REQUIRED: full git repository URL
branch: main # Override defaults.branch for this module
go_source: true # Override defaults.go_source for this module
redirect: https://pkg.go.dev/example.com/my-module # Override browser redirect URL for this module
local_path: ./my-module # Local checkout path (default: in-memory clone from repo remote if not specified)
# Subpackage discovery settings (enabled by default in 'auto' mode - set subpackages:mode:off to disable)
subpackages:
mode: auto # Options: off | auto | explicit (defaults to 'auto')
exclude: # Directories to skip in auto mode (defaults to [internal, testdata] in 'auto' mode)
- internal
- testdata
paths: # Allow-list exact subpackage paths (explicit mode only)
- sub/pkg
# Custom templates and static assets (all paths relative to .vanity.yml)
templates:
index: templates/index.html # Custom body partial for the root index page
module: templates/module.html # Custom body partial for module pages
submodule: templates/submodule.html # Custom body partial for submodule pages (falls back to module)
not_found: templates/404.html # Custom body partial for the 404 page
partials: # Reusable template components (referenced via {{template "name" .}})
- templates/header.html
- templates/footer.html
assets: # Static files/dirs copied verbatim into the output directory
- css/
- js/app.jsVanity generates minimally functional redirect pages by default, but you can customize the look and feel by providing your own HTML template partials. Templates are entirely optional.
Vanity owns the <head> section of each page (the go-import and go-source meta tags are critical for go get to work). Your templates can optionally define additional attributes to <head> via {{define "head"}} as well as the <body> via {{define "body"}} blocks that extend the built-in base templates:
<!--example.html-->
{{/* templates/module.html */}}
{{define "head"}}
<link rel="stylesheet" href="/css/style.css">
{{end}}
{{define "body"}}
<div class="module-page">
<h1>{{upper .Name}}</h1>
<p>Redirecting to <a href="{{.Redirect}}">documentation</a>...</p>
</div>
{{end}}The "head" block is injected after Vanity's required meta tags. The "body" block replaces the default body content. Both are optional, and you can omit either one n your template to keep the default behavior.
| Field | Type | Description |
|---|---|---|
.Domain |
string |
The configured domain (e.g. go.treyburn.dev) |
.Name |
string |
Module name / import suffix (e.g. vanity or vanity/pkg/cmd) |
.ImportPath |
string |
Full import path (e.g. go.treyburn.dev/vanity) |
.Repo |
string |
Git repository URL |
.Branch |
string |
Git branch |
.GoSource |
bool |
Whether go-source meta tag is enabled |
.Redirect |
string |
Redirect URL for documentation |
.Subpackages |
[]ModuleData |
Child subpackage data (only on parent module pages) |
| Field | Type | Description |
|---|---|---|
.Domain |
string |
The configured domain |
.Modules |
[]ModuleData |
All modules with their subpackages |
The following functions are available in all templates:
| Function | Signature | Description | Example |
|---|---|---|---|
upper |
string -> string |
Uppercase | {{upper .Name}} |
lower |
string -> string |
Lowercase | {{lower .Domain}} |
title |
string -> string |
Title case (English) | {{title .Name}} |
join |
[]string, string -> string |
Join with separator | {{join .List ", "}} |
sprintf |
string, ...any -> string |
Formatted string | {{sprintf "%s/%s" .Domain .Name}} |
now |
-> time.Time |
Current datetime in UTC | {{now.Format "2006-01-02"}} |
year |
-> int |
Current year | © {{year}} |
contains |
string, string -> bool |
Substring check | {{if contains .Name "api"}} |
hasPrefix |
string, string -> bool |
Prefix check | {{if hasPrefix .Name "pkg"}} |
hasSuffix |
string, string -> bool |
Suffix check | {{if hasSuffix .Repo ".git"}} |
replace |
string, string, string -> string |
Replace all occurrences | {{replace .Name "-" "_"}} |
trimSpace |
string -> string |
Trim whitespace | {{trimSpace .Name}} |
now returns a time.Time value, so you can call any method on it -- .Year, .Month, .Format, etc. Go uses a reference-time-based format system (not YYYY-MM-DD). See the Go time package docs for format strings.
Common patterns:
{{now.Format "2006-01-02"}}-- ISO date (2026-04-18){{now.Format "Jan 2, 2006"}}-- human-friendly (Apr 18, 2026){{now.Format "January 2006"}}-- month and year (April 2026)
You can define reusable components in files listed under templates.partials and reference them via {{template "name" .}}:
{{/* templates/header.html */}}
{{define "header"}}
<nav>
<a href="/">{{.Domain}}</a>
</nav>
{{end}}{{/* templates/module.html */}}
{{define "body"}}
{{template "header" .}}
<main>
<h1>{{.Name}}</h1>
</main>
{{template "footer" .}}
{{end}}Files and directories listed under templates.assets are copied verbatim into the output directory, preserving their relative path structure. No processing or minification is performed.
templates:
assets:
- css/ # css/style.css -> dist/css/style.css
- js/app.js # js/app.js -> dist/js/app.jsThe vanity check command validates custom templates when configured:
- Verifies all referenced template and asset files exist.
- Parses templates to catch syntax errors.
- Detects collisions between assets and generated output files.