Skip to main content
View as Markdown

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:

app.yaml
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: 5000

Every 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.

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:

app.yaml
links:
  - slug: webinar
    to: https://example.com/signup
    utm:
      source: linkedin
      medium: social
      campaign: q2-webinar
      content: sidebar-banner
      term: automation

A 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:

app.yaml
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: 1

weight 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:

code
https://your-app.com/l/spring-promo.svg

The 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.

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, forms or redirects path may sit under /l, which would shadow every short link beneath it
  • slug must be unique across the app
  • A link must have exactly one of to or targets

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.

Built with Sovrium