ADR-0013: URL Variable Substitution in Short Links
Context and Problem Statement
Short links are currently static: a slug maps to exactly one URL. Users who want to navigate
to parameterized destinations (e.g. go/github/joestump → https://github.com/joestump) must
create one link per target — there is no way to build a reusable "template" link. Should we add
user-managed parameterized links with $variable substitution in URL templates, and if so, how
should variables be declared, matched, and substituted in the resolver?
Decision Drivers
- Users want reusable go-links that accept path segments as arguments (e.g.
go/jira/PROJ-123) - The feature must be entirely user-facing — no admin configuration or per-host DNS tricks required
- Multiple ordered variables must be supported (
go/mylink/$foo/$bar) - Syntax must be unambiguous and not break existing single-segment, static links
- The resolver already handles a similar problem for keyword host routing (📝 ADR-0011) — the solution should compose cleanly with it
- No breaking changes to the existing
linkstable schema where avoidable
Considered Options
- Option A —
$varnamein URL field, prefix-slug matching in resolver (chosen) - Option B — explicit
variablescolumn listing variable names in order - Option C — positional
{1},{2}placeholder syntax, no variable naming
Decision Outcome
Chosen option: Option A — $varname markers in the URL field, prefix slug matching,
because it requires no schema migration, variable names are self-documenting in the URL template,
and the matching algorithm generalises the pattern already established for keyword routing.
How it works
- A user creates a link with slug
githuband URLhttps://github.com/$username. - When the browser navigates to
go/github/joestump, the resolver receives path/github/joestump. - The resolver tries an exact match on
github/joestump— not found. - It then splits the path on
/and tries progressively shorter prefixes:- slug
github→ found; URL contains$username; remaining segments["joestump"]are substituted in left-to-right order:$username = joestump.
- slug
- The resolved URL
https://github.com/joestumpis returned as a 302 redirect.
Multiple variables use the same positional approach:
slug: my-link URL: https://example.com/search?q=$query&page=$page
path: /my-link/widgets/3
→ https://example.com/search?q=widgets&page=3
Variable names ($varname) MUST match \$[a-z][a-z0-9_]* (lowercase letters, digits,
underscores). The number of path segments beyond the slug MUST equal the number of $var
markers in the URL template; mismatches fall through to a 404.
Router change
The catch-all resolver route changes from /{slug} (single segment) to /{prefix}* (wildcard
suffix), ensuring multi-segment paths reach the resolver. All existing named routes remain
registered before the catch-all and are unaffected.
Consequences
- Good, because no new database table or column is required —
$varsyntax inlinks.urlis entirely backward-compatible; existing static links continue to work unchanged - Good, because variable names are visible in the URL template, making links self-documenting
- Good, because prefix-slug matching is O(path depth) lookups, not a full table scan
- Good, because the feature is entirely in user-space; no admin configuration required
- Bad, because prefix matching adds latency to 404 paths (multiple failed DB lookups before
giving up) — mitigated by trying the exact slug first and only falling back when the URL
starts with
/-delimited segments - Bad, because slug uniqueness constraint now needs consideration:
githubandgithub/joestumpas separate slugs could ambiguously overlap — resolved by always trying the longest exact match first, then shortening - Neutral, because variable count validation happens at resolution time, not at link creation — users won't see errors until they actually navigate with wrong arity
Confirmation
Implementation is confirmed when:
- A link with
$varnamein its URL can be created through the existing link CRUD UI - A request to
/{slug}/{v1}/{v2}correctly substitutes all variables and redirects - A request with wrong variable count returns 404
- Existing single-segment static links are unaffected
- The API endpoint
GET /api/v1/linksincludes the URL template as-is
Pros and Cons of the Options
Option A — $varname in URL field, prefix slug matching
Variables are embedded in the URL template itself using $varname syntax. The resolver detects
$ in the URL to recognise a parameterised link and substitutes path segments positionally.
- Good, because no schema changes; works with the existing
links.urltext column - Good, because variable names are self-documenting (
$usernamevs{1}) - Good, because creation/editing uses the existing link form with no new UI fields required
- Good, because consistent with
{slug}placeholder syntax used in keyword URL templates (📝 ADR-0011) - Bad, because variable arity errors surface at request time, not at save time
- Bad, because prefix-slug matching adds complexity to the resolver
Option B — explicit variables column
Add a nullable variables TEXT column (JSON array of names) to the links table. The resolver
reads variable names from this column and knows exactly how many segments to expect.
- Good, because explicit variable count enables validation at link save time
- Good, because query
WHERE variables IS NOT NULLcan build an efficient index of variable links - Bad, because requires a schema migration
- Bad, because the form UI needs new fields to declare variable names separately from the URL
- Neutral, because the UI complexity is moderate but noticeable
Option C — positional {1}, {2} placeholder syntax
Use {1}, {2} numbered placeholders in the URL, keeping the same resolver approach as Option A
but without variable names.
- Good, because no schema changes
- Good, because arity can be inferred by counting
{N}tokens - Bad, because positional indices are not self-documenting (what is
{1}inhttps://github.com/{1}/{2}?) - Bad, because inconsistent with
{slug}usage in keyword templates (📝 ADR-0011), where{slug}is a named, not numbered, placeholder
Architecture Diagram
More Information
- 📝 ADR-0005 describes the links data model (slug uniqueness, URL field)
- 📝 ADR-0011 describes keyword host routing using
{slug}substitution — a related but admin-only pattern; this ADR extends the concept to user-space with multi-segment path variables - SPEC-0009 will formalise the requirements, scenarios, and acceptance criteria for implementation