ADR-0011: Root Forward Keywords (Admin-Configurable Hostname Templates)
Context and Problem Statement
joe-links currently operates as a single-hostname go-links service: go/slug resolves a slug
to an exact URL. There is demand for parameterized shortcuts — additional hostnames whose
path segment is substituted into a URL template rather than looked up as an exact slug.
Examples: wtf/sssd → Google search for "sssd", gh/joe-links → GitHub repository page,
jira/PROJ-42 → Jira ticket. How should the server model, store, and route these
admin-defined hostname→template mappings without breaking the existing slug system?
Decision Drivers
- Admins need live control — changing keywords should not require a server restart
- The existing link model only supports exact URL mappings; templates are a fundamentally
different abstraction and MUST NOT pollute the
linkstable - The server is already hostname-agnostic; it must learn to inspect the
Hostheader - Each new keyword hostname requires a matching
/etc/hostsentry on the client (or equivalent DNS config); this is an operator concern, not an app concern - The browser extension (see companion work) should be able to resolve keyword hosts the same
way it resolves
go/slug— by knowing the full set of registered keywords
Considered Options
- Option A — Database-backed
keywordstable with admin CRUD UI (chosen) - Option B — Config-file keyword map in
joe-links.yaml - Option C — Extend the existing
linkstable with ais_templateflag
Decision Outcome
Chosen option: Option A, because it gives admins live control through the existing admin dashboard pattern without requiring a server restart, and keeps templates cleanly separated from exact-match links.
Consequences
- Good, because admins can add/edit/delete keyword templates without touching config files or restarting the server
- Good, because the
linkstable remains a pure exact-match store; no conflation of concepts - Good, because the resolver change is minimal — check
Host, look up keyword, fall through to slug if no match — preserving the existing/{slug}catch-all - Bad, because it adds a new DB table, store, and admin handler (scope increase)
- Bad, because each keyword hostname still requires a client-side
/etc/hosts(or DNS) entry; the app cannot automate that step
Confirmation
Implementation is confirmed when:
GET wtf/sssd(withwtfregistered ashttps://google.com/search?q={slug}) returns a302tohttps://google.com/search?q=sssdGET go/some-slugis unaffected — resolver falls through to normal slug lookup- Admin UI at
/admin/keywordsallows create/edit/delete of keyword templates
Pros and Cons of the Options
Option A — Database-backed keywords table
New table: keywords(id UUID, keyword TEXT UNIQUE, url_template TEXT, description TEXT, created_at TIMESTAMP). Template variable: \{slug\} is replaced with the URL path segment.
New KeywordStore in internal/store/. Resolver checks strings.SplitN(r.Host, ":", 2)[0]
against keywords.keyword; on match, substitutes {slug} and redirects. Admin CRUD at
/admin/keywords using the existing admin handler pattern.
- Good, because live updates without restart
- Good, because clean separation from the
linkstable - Good, because fits naturally into the existing admin dashboard and store pattern
- Neutral, because one new migration, one new store, one new handler — small but real scope
Option B — Config-file keyword map
Keywords defined in joe-links.yaml under a keywords: block, loaded at startup via viper.
- Good, because no DB schema change
- Bad, because requires a server restart to add or change a keyword — defeats the admin UX goal
- Bad, because keywords live in a config file that is gitignored in dev, creating drift risk
Option C — Extend links with is_template flag
Add is_template BOOL and keyword_host TEXT columns to links. A "template link" whose slug
matches the Host header applies the url field as a template with the path substituted.
- Good, because reuses existing CRUD and store code
- Bad, because conflates two different abstractions (exact redirect vs. parameterized template) in a single table and model
- Bad, because slug validation rules (uniqueness, reserved prefixes) don't apply to keywords, creating confusing edge cases
Architecture Diagram
More Information
- The
{slug}placeholder inurl_templateis the path segment from the request URL (i.e.,chi.URLParam(r, "slug")). No other substitutions are defined in this ADR. - Template URLs MUST be valid URL prefixes; validation at save time should reject templates that
produce invalid URLs when
{slug}is substituted with a representative value. - Keyword strings follow the same character rules as hostnames; they are lowercase alphanumeric
and hyphens only (no dots, matching the
/etc/hostssingle-word format). - The browser extension can fetch
/api/v1/keywords(a future endpoint) to discover registered keyword hosts and apply them client-side, enablingwtf/footo navigate correctly without needing thehttp://prefix. - Related: 📝 ADR-0007 (routing), 📝 ADR-0005 (data model), 📝 ADR-0008 (REST API layer).