postcards: custom memorable URLs via Shlink (e.g. post.msbls.de/sardinien2026) #3
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
m wants to assign a custom, memorable URL to a postcard instead of (or alongside) the unguessable
/c/{token}link — e.g.post.msbls.de/sardinien2026— and set it comfortably while composing. He explicitly named Shlink.Architecture decision (otto-head)
Reuse the EXISTING Shlink instance on mlake (already multi-domain: s.flexsiebels.de, msbls.de, ts.msbls.de, dumusst.com; API at
https://s.flexsiebels.de/rest/v3, key in env). Do not deploy a new Shlink. This mirrors howm/hihlcreuses it.Routing model — the hihlc pattern (fallthrough on the same domain):
post.msbls.de/,/compose,/c/{token},/static/*,/health→ the postcards Go app (high Traefik priority for these specific paths).post.msbls.de/{anything-else}→ falls through to Shlink (lower priority catch-all), which 302-redirects the custom slug to that card'shttps://post.msbls.de/c/{token}.m/hihlcREADME § “Domain-Settings (Shlink)” + itshihlc-shlink.ymlTraefik dynamic config for the exact priority mechanism. post.msbls.de must be added to Shlink's domain list.Compose-flow integration:
/c/{token}), then calls the Shlink API (POST /rest/v3/short-urls,X-Api-Key) withcustomSlug=<slug>,domain=post.msbls.de,longUrl=https://post.msbls.de/c/{token}. Store the slug on the postcard so it can be shown/managed.Constraints
/c/{token}link always exists. A card with no slug behaves exactly like today.compose,c,static,health, empty/root) before calling Shlink, so a slug can never shadow the app.BASE_URL, don't hardcode post.msbls.de in app logic — the domain passed to Shlink should derive fromBASE_URL's host).DB
slugcolumn (unique) topostcards.postcardsvia a new idempotent migration. Schema-ownership reminder: verify against msupabase as the postgres role (via pooler100.99.98.203:6789), NOT supabase_admin — otherwise the prod app can't write and the deploy crash-loops (this hit v1).Sequencing
Depends on #2 (v2 design/multi-image/flip-card) — both touch the compose flow + handlers + templates. This issue is filed now to capture the design but should be picked up AFTER #2 is merged, to avoid two workers editing the same files. otto-head will assign mAi once #2 lands.
Definition of done
post.msbls.de/{slug}resolves (via Shlink) to the card.go build/vet/testgreen; e2e verified against msupabase as postgres; test data + test slug cleaned up.#3 umgesetzt — Custom Memorable URLs via Shlink
Optionaler, merkbarer Slug pro Karte:
post.msbls.de/<slug>→ Shlink →/c/{token}der Karte. Nutzt die bestehende, geteilte Shlink-Instanz (s.flexsiebels.de), kein neuer Shlink — analogm/hihlc.App-Verhalten:
post.msbls.de/), mobile-first, deutsch. Erscheint nur wennSHLINK_API_KEYgesetzt ist — ohne Key ist das Feature aus und jede Karte funktioniert weiter über/c/{token}. Karten ohne Slug sind unverändert.DeleteShortURLzurückgerollt.non-unique-slug, nicht 409 — der Client erkennt beide.compose,c,static,health,uploadswerden (plus Format-Regeln: Kleinbuchstaben/Ziffern/Bindestriche, 2–64, kein führender/abschließender Bindestrich) vor jedem Seiteneffekt geprüft → ein Slug kann nie eine App-Route überschatten.SHLINK_DOMAINleitet sich aus dem Host vonBASE_URLab; nichts hardcodedpost.msbls.de.DB: Idempotente Migration
003_slug_audience.sql— nullable, uniqueslug(Partial-Index nur über Non-NULL) +audience.ALTERbehält den Table-Owner (postgres), additiv, bricht die laufende alte Prod-Instanz nicht (alte Binaries selektieren/inserten die neuen Spalten nicht).Verifikation (e2e gegen msupabase als
postgres, local storage → Prod-Bucket unangetastet):slug+audienceSpalten +postcards_slug_keyIndex vorhanden.post.msbls.de/<slug>).compose→ „reserviert“.slug IS NULL.go build/vet/test/gofmtgrün (inkl. Unit-Tests für Slug-Validierung, Shlink-Client 400/409, Domain-Ableitung; Template-Render-Tests).Deploy-Artefakte für otto-head:
deploy/traefik/postcards-app.yml(App-Pfade Priority 100) +postcards-shlink.yml(Catch-all Priority 1) +deploy/README.md(Env, Container-Name-Platzhalter, Shlink-Domain, „kein Dokploy-UI-Domain für post.msbls.de“-Hinweis).Sequencing-Hinweis: Gemeinsam mit #4 gebaut (siehe dortigen Kommentar) und auf #2 aufbauend, plus enthält den separaten #5-Fix (Commit
39f048a).Commit: https://mgit.msbls.de/m/postcards/commit/2f0c9b6
Branch:
mai/hermes/issue-3-postcards-customNoch offen (otto-head): mergen,
SHLINK_API_KEYin Dokploy setzen, Traefik-Dateien nach mlake kopieren (Container-Namen eintragen), post.msbls.de redeployen.✅ #3 live on https://post.msbls.de
Custom memorable URLs work end-to-end: e.g.
post.msbls.de/sardinien2026→ Shlink → the card's/c/{token}. Verified live (created a slug card, hit the pretty URL, got 302 → 200 on the card), then cleaned up the test slug + card.App code merged in
408b276(was dormant until the key/routing landed). Infra wired by otto-head:SHLINK_API_KEY(+SHLINK_API_URL,SHLINK_DOMAIN=post.msbls.de) set on the Dokploy app; reuses the existing shared Shlink (s.flexsiebels.de), no new instance.post.msbls.dedomain (its Host-only router would swallow slugs) and replaced it with file-based Traefik routing:postcards-app.yml(app paths, priority 100) +postcards-shlink.yml(catch-all →shlink:8080).Gotcha worth recording: the shlink catch-all initially 404'd because a global maintenance router (
HostRegexp(\^.+$`)`, priority 1) tied with my catch-all (also priority 1) and won. Fix: bump the postcards-shlink router to priority 50 (above maintenance's 1, below the app's 100). Slug routing worked immediately after.App paths (
/,/compose,/c/,/static/,/health) still win via the priority-100 router. Not closing — m closes.