Short Links
A link you paste into a newsletter, a tweet and a printed flyer is three links as far as your analytics are concerned. Or one link whose clicks you cannot attribute to any of them. Reaching for a hosted shortener solves the attribution and hands the click data, visitor by visitor, to somebody else's database.
The links array declares short links that your own app serves and your own database counts:
links:
- slug: spring-promo
to: https://example.com/pricing
title: Spring promo — pricing page
tags:
- q2-launch
utm:
source: newsletter
medium: email
campaign: spring
lifecycle:
validUntil: '2026-06-30T23:59:59Z'
maxClicks: 5000Every link resolves at /l/{slug}, for example https://your-app.com/l/spring-promo. The /l prefix is fixed and not configurable, so the address is predictable on every Sovrium instance.
Link Properties
| Property | Description |
|---|---|
slug |
The address. Lowercase letters, digits, hyphens and underscores. Must be unique across the app. |
to |
Where the visitor lands: an absolute http(s):// URL, or a root-relative path within your own app. |
targets |
Several destinations to split traffic between. Mutually exclusive with to. See Splitting traffic. |
title |
A human label for the Admin console. Never shown to visitors. |
tags |
Labels for grouping links in the console: a campaign, a channel, a client. |
notes |
Free text for whoever maintains the link. Never leaves the console. |
utm |
Campaign parameters appended to the destination. See Campaign parameters. |
lifecycle |
When the link is live and how long it lasts. See Lifecycle. |
A short link answers 302 Found with Cache-Control: no-store. That is deliberate and it is the difference from a redirect: a 301 is cached by browsers and intermediaries indefinitely, which would silently defeat both the click count and the expiry date.
Why not a redirect?
Both answer a URL before page resolution, so it is worth being explicit about which to reach for:
redirects |
links |
|
|---|---|---|
| The job | A page moved and its old URL must keep working | A shareable address whose use you want to measure |
| Identity | A path pattern | A named thing with a lifespan |
| Status | 301, which tells search engines the move is permanent |
302, never cached |
| Languages | Inherits the language prefix | Deliberately one address, whatever the visitor's language |
| Counted | No | Yes, that is the point |
Neither replaces the other. Use redirects for a URL migration and links for anything you are going to share and want to read the numbers on.
Campaign parameters
utm appends the standard campaign parameters to the destination, so you write them once in config instead of pasting a hand-built query string into every channel:
links:
- slug: webinar
to: https://example.com/signup
utm:
source: linkedin
medium: social
campaign: q2-webinar
content: sidebar-banner
term: automationA visitor following /l/webinar lands on https://example.com/signup?utm_source=linkedin&utm_medium=social&utm_campaign=q2-webinar&utm_content=sidebar-banner&utm_term=automation.
Parameters already present on the destination are kept. utm never overwrites a value you wrote yourself, and any query string the visitor arrives with is carried through too.
Lifecycle
A campaign link should stop working when the campaign ends. lifecycle says when:
| Property | Description |
|---|---|
enabled |
false retires the link without deleting it. Defaults to true. |
validFrom |
ISO 8601 timestamp before which the link is not yet live. |
validUntil |
ISO 8601 timestamp after which it stops resolving. |
maxClicks |
Stop resolving once this many clicks are recorded. |
A link that is not yet live answers 404. A link that has expired, exhausted its maxClicks, or been disabled answers 410 Gone: an explicit "this existed and is finished", rather than pretending it never existed.
maxClicks carries two limits, and both are worth stating rather than leaving you to discover. The count is read from your analytics data, so only clicks inside your retention window are counted; a capped link that outlives your analytics.retentionDays starts again on a fresh budget. The cap itself is enforced per request rather than under a lock, so a simultaneous burst can overshoot it slightly. Both are the price of counting clicks in exactly one place, instead of keeping a second tally that could disagree with the first.
Splitting traffic
Give a link several targets to split traffic between them, for an A/B test or to compare two landing pages:
links:
- slug: pricing-test
title: Pricing page A/B
targets:
- to: https://example.com/pricing-a
weight: 3
- to: https://example.com/pricing-b
weight: 1weight is relative, so this sends three visitors to A for every one to B. A target with no weight counts as 1. Every recorded click carries the target it resolved to, so the Admin console reports each variant separately.
QR codes
Every link has a QR code at /l/{slug}.svg, generated by the app with no external service and no third-party request:
https://your-app.com/l/spring-promo.svgThe SVG is ready to print. Drop it into a flyer, a poster, a business card, or an image component on one of your own pages. Fetching the SVG is not a click; only scanning it is.
A scan is counted separately from a paste of the same link, so "did the flyer work, or was it the tweet?" is a question with an answer. The Admin console shows both.
Tracking
Clicks land in the same analytics store as your page views and honour the same settings: respectDoNotTrack, excludedPaths and retentionDays all apply, and no click is recorded at all unless analytics is enabled. Visitors are counted by the same cookieless hash used everywhere else; no click tracking writes a cookie.
Read the numbers in the Admin console at /_admin/links: clicks over time, referrers, devices, campaigns and the raw click log, for all links together or one link at a time.
Creating links outside config
Links declared in config belong to the app. They ship with it and are versioned with it, and links can also be created in the Admin console for campaigns that come and go faster than a deploy.
Config always wins. A console link cannot shadow a config link, and a config-declared link is read-only in the console: you can enable or disable it there, but editing or deleting it is refused. Configuration stays the source of truth for everything configuration declares.
Validation
A config that would make a link unreachable is rejected at boot rather than at the moment a visitor finds the dead address:
- No
pages,formsorredirectspath may sit under/l, which would shadow every short link beneath it slugmust be unique across the app- A link must have exactly one of
toortargets
Related Pages
- URL Redirects — retire a URL without breaking inbound links
- Analytics — the click store, its settings and its retention
- Admin Dashboard — where the link reports live
Last updated August 28, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.