Branded Link QR Service
Purpose
Design and operate a branded short-link + QR service where every printed QR encodes a permanent go.<domain>/<path> URL that 302-redirects to a changeable destination. The output is a source-controlled link registry, a generated redirect config with a CI drift check, deterministic QR artefacts, and a validation report proving each QR encodes the permanent link — not the destination.
When to use
Apply before anything is printed — business cards, flyers, signage, product labels, event collateral — whenever a QR or short URL will outlive the page it currently points at. Also use when:
- Adding per-person QR codes (employee business cards) that must survive staff and URL changes
- A printed QR already points at a destination that has to move
- Introducing a link registry so redirects stop being hand-edited in hosting config
- Hosting the redirect layer on a static host such as Azure Static Web Apps (
staticwebapp.config.jsonroutes) — or on Azure Container Apps when SWA domain binding is broken
Inputs expected
Partial inputs are fine — infer sensible defaults and state them.
- The branded link host (default: a
go.<domain>subdomain) and the static host it runs on - The list of links: slug/path → current destination, plus who owns each
- Whether per-person routes are needed (
/card/<person-slug>) and any existing generic routes to keep as aliases - QR output formats required (SVG for print, PNG for screens) and any styling constraints
- Where the registry and generator should live in the repo (default:
config/branded-links.json,scripts/)
Guiding principles
- Indirection is the whole point — never encode the destination in the QR. The printed QR encodes a permanent
go.<domain>/<path>URL that 302-redirects to a CHANGEABLE destination. Never use 301 — the destination must stay movable without reprinting; a cached 301 makes the printed link permanent in the worst way. In practice printed artwork has stayed final through three failed hosting attempts and a platform switch because only the 302 target and infrastructure changed. - Give the QR path and the human-typed path different routes.
/qr/<campaign>for the code and/<campaign>for what people type, redirecting with?src=qrand?src=urlrespectively, so scans and typed visits are distinguishable downstream. - A source-controlled registry is the single source of truth. Keep
config/branded-links.json(or equivalent) as the only place a link is defined, generate the deployed redirect config from it with a script, and never hand-edit the generated routes. - The generator needs a CI
--checkmode.--checkrebuilds the config in memory and compares it semantically (CRLF-safe — parse, don't diff bytes) against the committed file, so the pipeline fails if the generated config drifts from the registry. - Printed routes are immutable contracts. Use a scalable per-person convention
/card/<person-slug>for employee business-card QRs, and keep any earlier generic/cardas a permanent 302 compatibility alias — once printed, a route is never deleted or repurposed: change its 302 destination, and when terminology changes add the new path as an alias while keeping the old one live (e.g./adviseradded,/consultantkept). - Generate QR codes deterministically from the command line and commit them. Python:
segno.make_qr(url, error='m')— dependency-free and reproducible; itsboost_error(on by default) raises the error-correction level as high as fits WITHOUT increasing the symbol version. Node:npx -y qrcode -t svg -e Q -q 4 -d "<RRGGBB>FF" -l "FFFFFFFF" -o <name>.svg <url>(and-t png -w 1200for proofs), producing brand-colour and pure-black colourways of the identical code. SVG is the print master and PNG the proof; keep the quiet zone, print at least about 2 cm, and test-scan a proof before sign-off. - Always independently decode the generated QR. Decode with
pyzbaror OpenCV and assert it decodes to EXACTLY the branded URL; then assert the redirect destination string appears in neither the SVG nor the PNG — proving the QR encodes the permanent link, not the mutable destination. - Encode bare paths. A static
redirectroute on Azure SWA does not forward the query string, so put any UTM/tracking on the destination side of the registry, never in the printed URL. - Only green-light print when a fresh
curlof the exact encoded URL returns the redirect over a valid certificate (andopenssl s_client -servernameshows the right CN); until then layout work can proceed on the final artwork but nothing goes to the printer. A phone scan showing an "unsecure"/certificate warning right after adding the CNAME isERR_CERT_COMMON_NAME_INVALID: DNS already reaches the edge but the hostname is not yet bound — expected, not a DNS mistake. - Azure Container Apps is a working escape hatch when Static Web Apps domain binding is broken. It validates domains through its own
asuid.<host>TXT plus CNAME, issues free managed certificates, offers Australia East, and scales to zero for near-zero cost. Running the officialcaddyimage with the Caddyfile injected through a Secret-type volume means no custom image, no registry and no build step; a whole rescue took about 90 minutes. Trade-off vs SWA: Caddyfile routing, real logs and region choice, at the cost of a cold start on the first scan after idle, image-version upkeep, and routing config living outsidestaticwebapp.config.json(mitigate by embedding the Caddyfile in the Bicep so routes stay in source control). See ACA escape hatch inreference.md.
Process
- Choose the link host — a
go.<domain>subdomain, deployed as its own static site (on Azure SWA, a subfolder SWA with its own deploy token; see thestatic-website-hostingskill). - Define the route convention —
/card/<person-slug>for people, short nouns for campaigns,/qr/<x>(→?src=qr) alongside/<x>(→?src=url); list any legacy generic routes (/card) to retain as 302 aliases. - Create the registry — one entry per link:
path,destination,status: 302,owner,notes, andaliaseswhere a legacy path must keep working. - Write the generator — reads the registry, emits the host's redirect config (e.g.
staticwebapp.config.jsonrouteswithredirect+statusCode: 302); supports--checkfor CI. - Wire the CI check — run the generator in
--checkmode on every push/PR; fail on drift. - Generate QR artefacts —
segnoornpx qrcodeper link, saved as SVG (print master) and PNG (proof), brand-colour and black colourways, file-named by slug; commit them. - Validate — decode each artefact and assert exact-URL match; grep the SVG/PNG for the destination string and assert absence; record results.
- Deploy and verify live — confirm
302+ correctLocationfor every path, including aliases, over a valid certificate (openssl s_clientCN); test-scan a printed proof. - Operate — to move a destination, edit the registry, regenerate, deploy. Never touch a printed path.
Output format
- Route convention and registry schema — path pattern, alias policy, registry fields
- Registry file — the complete
config/branded-links.json - Generator script — build +
--checkbehaviour, and the CI step that runs it - Generated redirect config — the emitted routes (302, no 301)
- QR artefacts — per link: filename, format, EC level, symbol version
- Validation report — decode result per artefact, destination-absence check, live 302 check
- Operating notes — how to change a destination; what must never change
Quality checklist
- Every printed QR encodes
go.<domain>/<path>, never a destination - All redirects are 302 — no 301 anywhere in the generated config
- Registry is the only source; generated config is never hand-edited
- Generator
--checkruns in CI and compares semantically (CRLF-safe) - Per-person routes follow
/card/<person-slug>; legacy generic routes kept as 302 aliases - QR generated with
segno(error='m',boost_errorleft on) — deterministic and reproducible - Each artefact decoded independently and asserted equal to the exact branded URL
- Destination string absent from every SVG and PNG
- Printed URLs are bare paths (no query strings);
/qr/<x>and/<x>carry?src=qr/?src=urlon the destination side - Live
302+Locationverified over a valid certificate for every path and alias, and a proof test-scanned, before print sign-off - If SWA domain binding is wedged, the host is served from ACA + Caddy with routes embedded in Bicep
Avoid
- Encoding the destination URL directly in a QR — it can never be changed once printed
- Using 301 for branded links — browsers and crawlers cache it, freezing the destination
- Hand-editing the deployed redirect config — the registry must generate it, and CI must prove it did
- Byte-diffing generated config in
--check— CRLF differences cause false failures; parse and compare - Renaming or removing a route that has been printed — add aliases instead
- Trusting the generator without decoding the artefact — verify with
pyzbar/OpenCV every time - Putting UTM/query parameters in the printed URL — a static redirect drops them; attach them to the destination
- Printing before the live redirect returns
302with the rightLocationover a valid certificate — a post-CNAMEERR_CERT_COMMON_NAME_INVALIDmeans the hostname is not yet bound, not that DNS is wrong - Repurposing a printed route when terminology changes — add the new path as an alias and keep the old one live
Example usage
"We're printing business cards for four staff and a flyer for a product page. I want each QR to go to a permanent
go.example.comlink I can repoint later, hosted on our Azure Static Web App, with the links kept in a JSON file and CI checking the redirect config. Generate the QRs and prove they encode the short link, not the destination."
Source: This skill is sourced from the Matrix Skills library. Learn more at the AI Agent Skills Library.