feat: user-configurable /to/ and /from/ short links (extends #3 Shlink) #8

Closed
opened 2026-06-16 15:26:05 +00:00 by mAi · 2 comments
Collaborator

What

Extend the custom memorable-URL feature (#3) so the sender can configure short links under /to/ and /from/ prefixes, e.g.:

  • sendmy.cards/to/oma-erika
  • sendmy.cards/from/matthias

m (verbatim): the /to/ and /from/ forms are each for short links that users can configure. So it's the existing opt-in custom-slug, but namespaced into a to/… or from/… path that reads naturally on the new sendmy.cards domain.

Behaviour

  • In the compose flow's custom-URL field (already exists from #3), let the sender pick a prefix (to / from) plus their slug. Result: <SHLINK_DOMAIN>/to/<slug> or /from/<slug>.
  • Mints a Shlink short URL with the multi-segment slug to/<slug> (or from/<slug>) on SHLINK_DOMAIN (now sendmy.cards), resolving 302 to the card's /c/{token} — same reserve-before-persist + collision + rollback logic as #3.
  • The plain single-segment custom slug from #3 should keep working too (don't break existing). to/from are additive options.

A slug containing a slash (to/oma-erika) requires Shlink's multi-segment slugs to be enabled (MULTI_SEGMENT_SLUGS_ENABLED=true / the equivalent on the shared instance at s.flexsiebels.de). Verify this first — if it's off, the to/<slug> short URL creation will fail. Enabling it is an instance-level config on the shared Shlink (coordinate; don't break hihlc/other domains — multi-segment is backward-compatible with existing single-segment slugs, but confirm).

Routing (already in place — verify, don't re-add)

The Traefik app-router only claims /, /compose, /c/, /static/, /health; /to/… and /from/… already fall through to Shlink (priority-50 catch-all) on both post.msbls.de and sendmy.cards. So no Traefik change should be needed — confirm sendmy.cards/to/<existing-slug> 302s via Shlink once a slug exists.

Reserved paths

Keep the existing reserved-slug guard. to and from as bare first segments now belong to Shlink, so ensure the app doesn't try to claim them and the slug-validation allows the to/ from/ shapes.

Constraints

  • Mobile-first, German UI + Umlaute, renamable (SHLINK_DOMAIN derives from BASE_URL host — now sendmy.cards). Coexists with everything shipped (#3 single-segment slugs, broadcast, layouts, video, lightbox).

DoD

  • Compose lets the sender choose to/from + slug; sendmy.cards/to/<slug> (and /from/<slug>) resolve to the card via Shlink.
  • Multi-segment slugs confirmed working on the shared Shlink (enabled if needed, without breaking other domains).
  • Single-segment slugs from #3 still work. build/vet/test green; e2e verified (create card with a /to/ slug, hit it, lands on card); test data + test slug cleaned. Commit references this issue. otto-head merges + deploys.
## What Extend the custom memorable-URL feature (#3) so the sender can configure short links under **`/to/` and `/from/` prefixes**, e.g.: - `sendmy.cards/to/oma-erika` - `sendmy.cards/from/matthias` m (verbatim): the `/to/` and `/from/` forms are each for **short links that users can configure**. So it's the existing opt-in custom-slug, but namespaced into a `to/…` or `from/…` path that reads naturally on the new `sendmy.cards` domain. ## Behaviour - In the compose flow's custom-URL field (already exists from #3), let the sender pick a **prefix** (`to` / `from`) plus their slug. Result: `<SHLINK_DOMAIN>/to/<slug>` or `/from/<slug>`. - Mints a Shlink short URL with the multi-segment slug `to/<slug>` (or `from/<slug>`) on `SHLINK_DOMAIN` (now `sendmy.cards`), resolving 302 to the card's `/c/{token}` — same reserve-before-persist + collision + rollback logic as #3. - The plain single-segment custom slug from #3 should keep working too (don't break existing). `to`/`from` are additive options. ## Key technical point — multi-segment slugs in Shlink A slug containing a slash (`to/oma-erika`) requires Shlink's **multi-segment slugs** to be enabled (`MULTI_SEGMENT_SLUGS_ENABLED=true` / the equivalent on the shared instance at s.flexsiebels.de). **Verify this first** — if it's off, the `to/<slug>` short URL creation will fail. Enabling it is an instance-level config on the shared Shlink (coordinate; don't break hihlc/other domains — multi-segment is backward-compatible with existing single-segment slugs, but confirm). ## Routing (already in place — verify, don't re-add) The Traefik app-router only claims `/`, `/compose`, `/c/`, `/static/`, `/health`; **`/to/…` and `/from/…` already fall through to Shlink** (priority-50 catch-all) on both post.msbls.de and sendmy.cards. So no Traefik change should be needed — confirm `sendmy.cards/to/<existing-slug>` 302s via Shlink once a slug exists. ## Reserved paths Keep the existing reserved-slug guard. `to` and `from` as bare first segments now belong to Shlink, so ensure the app doesn't try to claim them and the slug-validation allows the `to/` `from/` shapes. ## Constraints - Mobile-first, German UI + Umlaute, renamable (SHLINK_DOMAIN derives from BASE_URL host — now sendmy.cards). Coexists with everything shipped (#3 single-segment slugs, broadcast, layouts, video, lightbox). ## DoD - Compose lets the sender choose `to`/`from` + slug; `sendmy.cards/to/<slug>` (and `/from/<slug>`) resolve to the card via Shlink. - Multi-segment slugs confirmed working on the shared Shlink (enabled if needed, without breaking other domains). - Single-segment slugs from #3 still work. build/vet/test green; e2e verified (create card with a /to/ slug, hit it, lands on card); test data + test slug cleaned. Commit references this issue. otto-head merges + deploys.
mAi self-assigned this 2026-06-16 15:26:05 +00:00
Author
Collaborator

Commit: c9bb3a62ab (branch mai/hermes/issue-8-feat-user)

Multi-segment slugs — verified first, as instructed

They were OFF on the shared Shlink: a to/x slug got silently rewritten to to-x. I enabled them by adding MULTI_SEGMENT_SLUGS_ENABLED=true to the shlink service in its Dokploy compose (raw compose srvcs-shlink-zgb13f on mlake) and redeployed.

  • Backward-compatible — confirmed after enabling: existing single-segment slugs still resolve (post.msbls.de/dania2026 → 302, msbls.de/upcsession → 302). The flag only allows slashes; it changes nothing about existing slugs, so hihlc & other domains are unaffected.
  • The rollback path needs no change either: the app's %2F-encoded DELETE of to/<slug> returns 204 (verified live).
  • (Pre-existing, unrelated: hihlc.de/kicker returns 404 — the slug is intact in the Shlink DB; it's a hihlc-domain routing quirk, not caused by this flag. Flagging for visibility, out of #8 scope.)*

App changes

  • Compose: a –/to/from namespace selector inline in the existing Wunsch-Adresse field. The sender picks a namespace + types a segment; result is <domain>/to/<slug> or /from/<slug>. Input + selector both restore on a validation-error re-render.
  • Validation: ValidateSlug now accepts one to/from prefix plus a clean segment, or a bare segment (#3). The reserved-path guard applies only to bare slugs — to/compose is fine because /to/compose is never an app route.
  • Shlink client: zero changes — the multi-segment slug rides in the JSON customSlug body; reserve-before-persist + collision + rollback are reused as-is.
  • Single-segment slugs (#3) keep working — covered by a regression in the e2e below.

Routing & domain

  • Prod app env is already on sendmy.cards (BASE_URL + SHLINK_DOMAIN). SHLINK_DOMAIN derives from BASE_URL's host — nothing hardcoded.
  • The repo's Traefik files only claimed post.msbls.de, so I updated both routers to claim sendmy.cardspost.msbls.de and documented it. The app router still only claims / /compose /c/ /static/ /health; /to/… and /from/… fall through to the Shlink catch-all (verified live).
  • ⚠️ For otto-head: sendmy.cards does not resolve via DNS yet, so the public sendmy.cards/... URLs can't 302 until DNS points at mlake and a TLS cert is issued. The Traefik routers + README cover it; only DNS/cert remain.

Build / test / e2e — all green

  • go build ./..., go vet ./..., go test ./... (added tests: NormalizeSlugPrefix, BuildSlug, namespaced ValidateSlug cases, compose render/prefill for the selector).
  • e2e on live post.msbls.de (real binary vs prod DB, STORAGE_BACKEND=local so the prod bucket was untouched, real Shlink key):
    • Composed a /to/ card → app minted to/mai-e2e-i8 → DB row slug=to/mai-e2e-i8; rendered card shows the pretty …/to/mai-e2e-i8 URL.
    • Hitting post.msbls.de/to/mai-e2e-i8 → 302 → the card's /c/{token}.
    • Bare single-segment slug (#3) still works.
    • Collision on to/mai-e2e-i8 → 400 „schon vergeben“ with zero orphan cards.
    • All test slugs + DB rows cleaned afterwards (slug now 404, DB 0 rows).

DoD status

  • Compose lets sender choose to/from + slug; resolves to the card via Shlink (verified on post.msbls.de; sendmy.cards pending DNS).
  • Multi-segment confirmed working on the shared Shlink, enabled without breaking other domains.
  • Single-segment slugs (#3) still work; build/vet/test green; e2e verified; test data cleaned; commit references this issue.
  • otto-head: point sendmy.cards DNS at mlake (+ TLS), copy the two deploy/traefik/*.yml to /etc/dokploy/traefik/dynamic/ (fill the app container name), merge + deploy.
## #8 done — `/to/` and `/from/` user-configurable short links **Commit:** https://mgit.msbls.de/m/postcards/commit/c9bb3a62ab13e278c951e5761b0573b042afd51e (branch `mai/hermes/issue-8-feat-user`) ### Multi-segment slugs — verified first, as instructed They were **OFF** on the shared Shlink: a `to/x` slug got silently rewritten to `to-x`. I enabled them by adding `MULTI_SEGMENT_SLUGS_ENABLED=true` to the `shlink` service in its Dokploy compose (raw compose `srvcs-shlink-zgb13f` on mlake) and redeployed. - **Backward-compatible — confirmed after enabling:** existing single-segment slugs still resolve (`post.msbls.de/dania2026` → 302, `msbls.de/upcsession` → 302). The flag only *allows* slashes; it changes nothing about existing slugs, so hihlc & other domains are unaffected. - The rollback path needs no change either: the app's `%2F`-encoded `DELETE` of `to/<slug>` returns 204 (verified live). - *(Pre-existing, unrelated:* `hihlc.de/kicker` returns 404 — the slug is intact in the Shlink DB; it's a hihlc-domain routing quirk, not caused by this flag. Flagging for visibility, out of #8 scope.)* ### App changes - **Compose:** a `–/to/from` namespace selector inline in the existing Wunsch-Adresse field. The sender picks a namespace + types a segment; result is `<domain>/to/<slug>` or `/from/<slug>`. Input + selector both restore on a validation-error re-render. - **Validation:** `ValidateSlug` now accepts one `to`/`from` prefix plus a clean segment, or a bare segment (#3). The reserved-path guard applies only to bare slugs — `to/compose` is fine because `/to/compose` is never an app route. - **Shlink client: zero changes** — the multi-segment slug rides in the JSON `customSlug` body; reserve-before-persist + collision + rollback are reused as-is. - **Single-segment slugs (#3) keep working** — covered by a regression in the e2e below. ### Routing & domain - Prod app env is already on **`sendmy.cards`** (`BASE_URL` + `SHLINK_DOMAIN`). `SHLINK_DOMAIN` derives from `BASE_URL`'s host — nothing hardcoded. - The repo's Traefik files only claimed `post.msbls.de`, so I updated both routers to claim **`sendmy.cards` ‖ `post.msbls.de`** and documented it. The app router still only claims `/ /compose /c/ /static/ /health`; `/to/…` and `/from/…` fall through to the Shlink catch-all (verified live). - ⚠️ **For otto-head:** `sendmy.cards` does **not** resolve via DNS yet, so the public `sendmy.cards/...` URLs can't 302 until DNS points at mlake and a TLS cert is issued. The Traefik routers + README cover it; only DNS/cert remain. ### Build / test / e2e — all green - `go build ./...`, `go vet ./...`, `go test ./...` ✅ (added tests: `NormalizeSlugPrefix`, `BuildSlug`, namespaced `ValidateSlug` cases, compose render/prefill for the selector). - **e2e on live `post.msbls.de`** (real binary vs prod DB, `STORAGE_BACKEND=local` so the prod bucket was untouched, real Shlink key): - Composed a `/to/` card → app minted `to/mai-e2e-i8` → DB row `slug=to/mai-e2e-i8`; rendered card shows the pretty `…/to/mai-e2e-i8` URL. - **Hitting `post.msbls.de/to/mai-e2e-i8` → 302 → the card's `/c/{token}`.** ✅ - Bare single-segment slug (#3) still works. - Collision on `to/mai-e2e-i8` → 400 „schon vergeben“ with **zero orphan cards**. - All test slugs + DB rows cleaned afterwards (slug now 404, DB 0 rows). ### DoD status - [x] Compose lets sender choose `to`/`from` + slug; resolves to the card via Shlink (verified on post.msbls.de; sendmy.cards pending DNS). - [x] Multi-segment confirmed working on the shared Shlink, enabled without breaking other domains. - [x] Single-segment slugs (#3) still work; build/vet/test green; e2e verified; test data cleaned; commit references this issue. - [ ] **otto-head:** point `sendmy.cards` DNS at mlake (+ TLS), copy the two `deploy/traefik/*.yml` to `/etc/dokploy/traefik/dynamic/` (fill the app container name), merge + deploy.
Author
Collaborator

Merged & deployed — live on sendmy.cards

Merged c9bb3a6 (merge e84c3c4), deployed. Verified live e2e on sendmy.cards (via forced resolve, public DNS still propagating): composed a /to/ card → sendmy.cards/to/otto-e2e-i8 302s to the card's /c/{token}. Single-segment slugs (#3) still work. Test data + slug cleaned.

No migration / no Traefik change needed (slug field + both-domain routing already live). The worker enabled MULTI_SEGMENT_SLUGS_ENABLED=true on the shared Shlink (backward-compatible, other domains unaffected — verified). go build/vet/test green.

Not closing — m closes.

## ✅ Merged & deployed — live on sendmy.cards Merged `c9bb3a6` (merge `e84c3c4`), deployed. Verified live e2e on **sendmy.cards** (via forced resolve, public DNS still propagating): composed a `/to/` card → `sendmy.cards/to/otto-e2e-i8` **302s to the card's `/c/{token}`**. Single-segment slugs (#3) still work. Test data + slug cleaned. No migration / no Traefik change needed (slug field + both-domain routing already live). The worker enabled `MULTI_SEGMENT_SLUGS_ENABLED=true` on the shared Shlink (backward-compatible, other domains unaffected — verified). go build/vet/test green. Not closing — m closes.
mAi closed this issue 2026-06-23 15:48:02 +00:00
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/postcards#8
No description provided.