Skip to content

V1-Listing Deprecation Strategy

What happens to https://stringing.wagen.io after V2 launches. The V1 public listing currently exposes client names, prices, and per-row PDF receipts to anyone with the URL — kill that exposure on V2 launch day, and decide what greets returning visitors at the URL afterwards. Owned by Mira. Cross-cuts: v1-baseline § Public site, v2-scope M4, nfr § Auth-on-listing, integration — Caddy ingress, admin-v1-upload (sibling cutover surface), design-tokens.

Source requirement

  • v1-baseline § Public site — V1 is "stringing.wagen.io — single page 'Listing all Orders' with per-row PDF receipts at /receipt_<id>.pdf. Backed by a script that reads the XLSX directly; no separate database."
  • v1-baseline § Gaps — "The public site exposes client names with no auth — fine in a friend-circle context but not intentional."
  • v2-scope M4 — "Auth on the listing — kill public exposure of client names." This deprecation is the operational follow-through on M4.
  • nfr § Auth on the listing — "auth on the listing is mandatory (M4) — eliminates V1's accidental public exposure of client names."
  • integration § Ingress — "Caddy routes racket-book-test.wagen.io and stringing.wagen.io to RBO's containers." The stringing.wagen.io hostname is owned by keystone's Caddy config; deprecation requires a keystone-side ingress edit.

Goal

Three commitments:

  1. Privacy first; SEO second; UX third. The first thing this deprecation does is stop serving client names + prices to unauthenticated visitors. Every option must satisfy that bar on day one of V2 launch, before any UX nicety.
  2. The hostname stays. stringing.wagen.io is a known URL — printed on V1 receipts, bookmarked by clients, indexed by Stefan's friends-and-family circle. Releasing the hostname (returning NXDOMAIN) is the worst UX. Keep the cert, keep the host, change what the host serves.
  3. One cutover edit, not a phased rollout. This is a single decision Stefan makes once at V2 launch. The plan should not require monitoring, follow-up edits, or revisits — pick the option that resolves cleanly the moment Atlas merges the keystone MR.

The three options

Each option is one keystone-side ingress edit (Caddy config) plus optional asset work in racket-book or a sibling repo. None of them require running V1's Python listing script after the cutover — V1's code is mothballed.

Option A — Hard redirect to RBO root

Caddy returns a 301 Moved Permanently for stringing.wagen.io and any path under it, redirecting to the V2 RBO landing page (e.g. https://racket-book.wagen.io/ or whatever the canonical V2 host is at launch).

stringing.wagen.io {
    redir https://racket-book.wagen.io{uri} permanent
}

What returning visitors see: their browser silently follows the 301 to the RBO landing page — which in V2 is an auth-gated app. Unauthenticated visitors land on the sign-in page with no context for why their old bookmark moved.

What search engines see: 301 means "permanent move; transfer link equity to the new URL." Google treats this as the canonical signal. The V1 indexed pages drop from the index over a few weeks.

Per-row receipt URLs (/receipt_<id>.pdf): redirected to RBO root. The old PDFs are no longer served. Anyone with a saved URL (stringing.wagen.io/receipt_142.pdf) gets the V2 sign-in page after the redirect — confusing, but safe (no PII leakage).

Option B — Soft-deprecation banner with sunset countdown

The V1 listing is replaced with a static page that says "We're moving — this listing closes on YYYY-MM-DD" with a countdown. Auth is not added; the listing's data is not rendered. Once the sunset date passes, Caddy switches to Option A's redirect.

# Pre-sunset (T < sunset_date):
stringing.wagen.io {
    root /var/www/v1-sunset-banner
    file_server
    # serves a static index.html with the countdown
}

# Post-sunset (T >= sunset_date) — keystone ops switches to:
stringing.wagen.io {
    redir https://racket-book.wagen.io{uri} permanent
}

What returning visitors see during the banner window: a page with "We're moving to a new home — this listing closes on 2026-MM-DD. Please bookmark [the new URL]." No client data visible.

What returning visitors see after sunset: Option A's silent redirect.

Per-row receipt URLs during the banner window: they 404 (or 410 Gone, more semantically correct) — the static banner only serves /, not /receipt_<id>.pdf. PII is killed instantly even though the banner is up; it's just a courtesy heads-up that the URL is moving.

Option C — Sunset page replaces the listing entirely (no countdown)

The listing is replaced with a static "we've moved" page, forever. No countdown, no automatic redirect; the URL stays a sunset page indefinitely and links visitors to the V2 home.

stringing.wagen.io {
    root /var/www/v1-sunset-page
    file_server
}

What returning visitors see: a friendly "We've moved" page with a CTA linking to V2. No client data, no PDFs.

Per-row receipt URLs: 404 / 410 forever. The static page only serves /.

SEO: the page is indexable; with proper meta tags it can carry the brand and a clear pointer. No automatic 301 link-equity transfer (that's Option A's territory) — but our SEO footprint is essentially nil (Stefan's friends-and-family operation, not a public business), so this is a non-issue in practice.

Evaluation matrix

Concern A — hard redirect B — banner + sunset C — sunset page (forever)
Earliest kill of public PII exposure T0 (cutover day) — / and all receipt PDF URLs return the redirect immediately. ✓ T0 — banner serves nothing but the static notice; PDFs 404 instantly. ✓ T0 — same as B. ✓
SEO — link-equity transfer 301 is the gold standard; Google fully transfers link equity. During banner: 200 OK to a different page (loses some equity); post-sunset: 301 (equity transfer resumes). Stays a 200 OK on a different page; no link-equity transfer to RBO unless the page explicitly links + uses rel=canonical. Our SEO footprint is negligible regardless.
User-surprise — returning visitor with a bookmark High surprise: the page they remember is gone, they land on a sign-in page with no context. Medium surprise during banner: explanation + countdown. Reverts to high surprise post-sunset (silent redirect to sign-in). Low surprise: friendly "we've moved" page with a one-tap link to V2.
Receipt-PDF URL behaviour Redirected to RBO root via the 301. Stefan's clients with old receipt_<id>.pdf URLs see the V2 sign-in screen. 404 / 410 — clean and semantic; no leakage. 404 / 410 forever.
Implementation cost One-line Caddy config edit on keystone. Zero asset work. Static HTML page + countdown logic (JS or server-rendered date) + ops calendar reminder to flip the config on sunset day. Static HTML page (no countdown). One Caddy edit.
Operational ongoing cost None — set-and-forget. Ops reminder to flip config on sunset day; if missed, the banner stays up past its declared date (slightly embarrassing). None — set-and-forget.
Privacy posture clarity Strongest: the URL is permanently part of RBO's domain space; clients learn V1 is gone the first time they hit it. Mediocre: the banner-window phase is a "we know this URL exposes data, we're keeping it sort-of-up for a polite goodbye" stance. Even though no data is served, the optics are weaker. Strong: clear permanent statement that V1 is closed.
Reversibility Easy: revert the Caddy config. Easy: revert the Caddy config. Easy: revert the Caddy config.
Effort to maintain (year 2+) None. None (post-sunset = same as A). None — the static page lives forever; link to V2 stays valid.

Recommendation — Option C: sunset page replaces the listing entirely

Default pick: Option C — a static "we've moved" sunset page that replaces the listing forever. Stefan flips on review if he disagrees.

Rationale

  1. Returning-visitor experience is the dominant UX consideration. V1's audience is Stefan's friends-and-family circle (per the v1-baseline § Public site framing). Those visitors have bookmarks; many of them are casual — not weekly users, but "I'll check my receipt next month" users. Hitting a sign-in page with no context (Option A) creates "did this site die?" anxiety. A friendly "we've moved" page gives them the next click in plain language.

  2. Privacy posture is satisfied at T0 by all three options — so the choice is decided by UX. C wins UX without sacrificing privacy.

  3. SEO is a non-concern. The V1 site is not a public business; it's a hobby tracker visible at a sub-subdomain. Google has it indexed but it ranks for nothing meaningful. The "lose 301 link equity" cost of Option C over Option A is theoretical.

  4. Implementation cost is the same as Option A. A static HTML page is one-time work (a Markdown / HTML write-once); Caddy config is one line. No countdown logic, no calendar reminder.

  5. Set-and-forget durability. Option B (banner + sunset) requires Stefan or Atlas to remember to flip the config on the sunset date. That's the kind of low-priority chore that drifts off the calendar; the embarrassing failure mode is "the banner says 'closes 2026-06-15' and it's now 2026-09-15 and we never flipped it." Option C has no second-step.

  6. The Caddy ingress is a keystone change, not an RBO change. Per integration, stringing.wagen.io is keystone-owned. The deliverable on Stefan's side is a keystone MR (or a keystone-issue cross-linked from racket-book) that lands the new Caddy config + the static asset. The static asset itself can live in keystone's repo or in a tiny wagen-static-pages sibling — either way, racket-book itself is not changed.

Why not Option A

Option A's silent redirect is the right move for public, business-facing sites where SEO link-equity matters more than first-time-visitor disorientation. Stefan's V1 site is neither — it's a hobby tracker with a bookmark-driven audience, where "what is this sign-in page?" is a worse experience than "oh, they moved, here's the new link."

If Stefan ever launches a public business face for racket-book that needs SEO transfer, that's an Option A move at a different URL.

Why not Option B

Option B's banner-with-countdown is the right move for active services where the audience needs warning of impending change. V1 has no warning to give: V2 launches, V1 closes, the audience finds out at the URL. Splitting that into "warning phase + cutover phase" adds operational complexity (the calendar flip) without changing the audience experience materially — they all find out by hitting the URL anyway.

Option B would be the right pick if there were a transition period where V1 should remain readable (e.g. with auth) while V2 stabilises. There isn't — V2 launches with M4 ("Auth on the listing — kill public exposure of client names") satisfied at the V2 listing, and V1's data is migrated in full.

The sunset page — design

A single static HTML page served at stringing.wagen.io/ (and /* — every path returns it; no per-row PDFs).

sm 375 px

┌───────────────────────────────────────┐
│                                       │
│                                       │
│                                       │
│             [racket-book logo]        │  Centered, ~80px
│                                       │
│           We've moved!                │  H1 — text-h1 slate-900
│                                       │
│  Stefan's stringing tracker is now    │  text-body slate-700
│  at a new home — with a real login,   │
│  receipts in your inbox, and the      │
│  same year-on-year history.           │
│                                       │
│  ┌─────────────────────────────────┐  │
│  │   Open the new tracker  →       │  │  Primary CTA — full width
│  └─────────────────────────────────┘  │
│                                       │
│  Bookmarked an old receipt URL?       │  text-small slate-600
│  Sign in to the new tracker — your    │
│  full history is there, including     │
│  every receipt.                       │
│                                       │
│                                       │
│  ─────                                │  Soft divider
│                                       │
│  Questions? Contact Stefan at         │  text-tiny slate-500
│  stefan.wagen@gmail.com.              │
│                                       │
│                                       │
└───────────────────────────────────────┘

md 768 px+

Same content, max-width 560 px, centered. The CTA stays full-width within the column. Vertical breathing room scales: space-12 between blocks instead of space-8.

lg 1280 px+

Identical to md. Don't sprawl wide — the page is a brief courtesy, not a marketing surface.

Component breakdown

Component Notes
Logo Optional — if Stefan has a small SVG mark for racket-book (or just sets the favicon), include it. If not, plain text "racket-book" in text-h2 weight-700 indigo-700 is a clean fallback.
H1 "We've moved!" — text-h1, exclamation kept intentional (warmer than "We have moved.").
Body paragraph Three short clauses: what moved, why it's better, what's preserved.
Primary CTA "Open the new tracker →" — bg-indigo-700 text-white, full-width on sm, max 320 px on wider. Links to https://racket-book.wagen.io/ (or whatever the canonical V2 URL is at launch).
Bookmarked-URL hint One sentence telling visitors with old /receipt_<id>.pdf bookmarks how to recover their receipt.
Contact line Stefan's email for the rare "I can't sign in / my receipt URL doesn't work" case. Keeps the small-operator tone.
Footer divider + contact border-t slate-200 divider; text-tiny slate-500 contact line.

Visual style

  • Inherits design tokens: Inter font, text-body 16/24, slate-900 ink, indigo-700 primary.
  • White background, no patterns or photos. The page is a one-time courtesy; it should load in under 200 ms on a flaky tournament-venue connection.
  • No JS. No HTMX. No analytics. Plain HTML + minimal inline CSS (or a single linked stylesheet — see implementation note below).

Copy — EN (final)

We've moved!

Stefan's stringing tracker is now at a new home — with a real login, receipts in your inbox, and the same year-on-year history.

[Open the new tracker →]

Bookmarked an old receipt URL? Sign in to the new tracker — your full history is there, including every receipt.


Questions? Contact Stefan at stefan.wagen@gmail.com.

Copy — DE (Iris's later pass)

DE strings are flagged for Iris's polish. Mira's first-pass draft for Iris to refine:

Wir sind umgezogen!

Stefans Bespannungs-Tracker hat ein neues Zuhause — mit richtigem Login, Quittungen in deinem Postfach und der gesamten bisherigen Historie.

[Den neuen Tracker öffnen →]

Hast du eine alte Quittungs-URL gespeichert? Melde dich im neuen Tracker an — deine vollständige Historie ist dort, inklusive jeder Quittung.


Fragen? Schreib Stefan an stefan.wagen@gmail.com.

The sunset page can be served in EN-only at launch (the V1 audience is bilingual; Stefan can decide to add a Accept-Language-driven DE variant later if he asks). Alternatively, a two-language stack (EN above, DE below, separated by a divider) is acceptable on a one-shot page like this — flagged as Stefan's call.

Page metadata (HTML head)

<title>racket-book — we've moved</title>
<meta name="description" content="Stefan's stringing tracker has moved. Sign in at the new home for your full history, including every receipt.">
<meta name="robots" content="noindex,follow">
<link rel="canonical" href="https://racket-book.wagen.io/">

noindex,follow tells crawlers: "don't index this page, but follow the link to V2." Combined with rel=canonical, the SEO posture is "the new URL is the canonical home; this page exists only as a forwarding courtesy."

Implementation plan

The change is a keystone MR (Caddy ingress + static asset). Racket-book itself does not ship code for this deprecation. The flow:

  1. Stefan files a keystone issue (source:racket-book label per project memory) titled "RBO V2 launch — V1 listing deprecation: replace stringing.wagen.io with sunset page."
  2. Atlas owns the keystone MR. Deliverables in the keystone MR:
  3. Static asset committed to keystone (static/v1-sunset/index.html + minimal inline CSS, or a tiny standalone repo if keystone prefers).
  4. Caddy config edit:
    stringing.wagen.io {
        root /srv/v1-sunset-page
        file_server
        # All paths fall through to the static index.html
        try_files {path} /index.html
    }
    
  5. The fall-through try_files ensures /receipt_<id>.pdf and any other path renders the same sunset page (cheap; no per-path 404 page maintenance).
  6. Cutover ordering:
  7. V2 deploy lands (RBO container running with M4 auth on its listing).
  8. Stefan runs the V1 → V2 upload via admin-v1-upload.
  9. Once Stefan is signed off (Stage 4 done), Atlas merges the keystone MR; the V1 listing flips to the sunset page within the next deploy cycle.
  10. No window where V1 listing is unauthenticated AND V2 is live: the cutover is sequenced so M4's "kill public exposure" goal is satisfied at the moment V2 launches.
  11. Verification. After the keystone MR lands:
  12. curl -sI https://stringing.wagen.io/ returns 200 with the sunset page HTML.
  13. curl -sI https://stringing.wagen.io/receipt_142.pdf returns 200 with the sunset page HTML (because of try_files fall-through). Crucially, no client name + price PDF is served.
  14. curl -sI https://stringing.wagen.io/listing.html (or whatever V1's old path was) returns 200 sunset.
  15. Post-launch monitoring. None. The sunset page is a static asset; if Stefan ever wants to update copy, it's a one-line static-asset MR on keystone.

Failure modes + mitigations

Failure Mitigation
Old V1 Python listing script accidentally still deployed and reachable on a different port Keystone Caddy config is the only public ingress; the V1 container is decommissioned post-cutover. Atlas's deprecation MR is the source of truth.
TLS cert renewal fails after cutover Caddy auto-renews via Let's Encrypt; the hostname stays the same so renewal continues to work. No special handling.
Search engines keep stringing.wagen.io/receipt_<id>.pdf URLs in their cache for weeks Each cached link returns the sunset page (200 OK with noindex). Crawlers eventually drop the URLs from their index because they all return the same content. PII is not leaked because the V1 PDFs are no longer served.
Stefan's friends-and-family share the V1 URL on WhatsApp / messenger The sunset page renders correctly on every device; the CTA button is the recovery path.
A returning client cannot find their old receipt because they don't have a V2 account yet The bookmarked-URL hint in the sunset copy says "Sign in" — but they may not have credentials. Edge case: the V2 magic-link onboarding (M21) is the recovery path; Stefan provisions a V2 account for any client who asks. The sunset page's contact line ("Questions? Contact Stefan…") is the fallback.
Stefan launches V3 client portal and wants stringing.wagen.io to redirect to client portal instead of the V2 stringer login Trivial: edit the sunset page's CTA target. The hostname structure stays.

Privacy verification checklist

Pre-merge of the keystone MR, verify:

  • [ ] https://stringing.wagen.io/ does NOT render the old V1 listing HTML (check raw HTML body).
  • [ ] https://stringing.wagen.io/receipt_1.pdf does NOT serve a PDF; returns the sunset page HTML.
  • [ ] https://stringing.wagen.io/receipt_329.pdf (last known V1 ID) does NOT serve a PDF.
  • [ ] https://stringing.wagen.io/listing.html (V1 path, if any) does NOT serve client names.
  • [ ] No HTTP response at any path on the host returns headers or body content from the V1 application — Caddy's static handler is the only thing answering.
  • [ ] The V1 application container is stopped + removed from the keystone deploy manifest.

This checklist lives in the keystone MR's description so Atlas can tick it before merging.

Accessibility

Even a one-shot sunset page meets the project's accessibility floor:

  • Single H1, semantic <main>.
  • Color contrastslate-900 on white = 16.1:1; indigo-700 button on white = 8.59:1; both well above WCAG AA.
  • CTA hit target — full-width, 48 px tall on sm.
  • Keyboard — single tab stop (the CTA); Enter activates the link.
  • Screen reader — H1 read first, then body, then CTA labelled "Open the new tracker, link, opens new home". The "→" arrow is decorative; CSS pseudo-element so screen readers don't read it.
  • prefers-reduced-motion — no animations on the page; nothing to suppress.
  • prefers-color-scheme: dark — out of scope for V2 per design-tokens § Dark mode. The sunset page renders in light mode regardless.

i18n affordance

  • EN-only at launch is acceptable. The audience is bilingual; the page is a brief notice.
  • DE pass — Iris's later round, if Stefan asks. The DE draft above is Mira's first-pass; Iris polishes idiom + tone.
  • Locale detection — none in V2. The page is statically served. Adding Accept-Language-based locale switching is a polish round (and arguably out of scope: a static asset with two paragraphs in two languages stacked is the simpler answer if Stefan wants both upfront).

Cross-references

Open questions for Stefan (with proposed defaults)

  1. Pick option (A / B / C). Proposed default: C — sunset page replaces the listing forever. Rationale above. Stefan flips on review.
  2. Logo on the sunset page. Proposed default: omit; use plain racket-book wordmark in indigo-700. No racket-book SVG mark exists yet (Round-0 polish if Stefan wants one). Alternative: use the favicon at 80 px. Mira leans wordmark.
  3. CTA target URL. Proposed default: https://racket-book.wagen.io/ (V2 stringer login) — the public RBO host at launch. Stefan confirms the canonical hostname; the spec should be edited if the host is named differently at cutover (e.g. book.wagen.io).
  4. EN-only vs EN + DE stacked. Proposed default: EN-only at launch, with Mira's draft DE copy ready in this spec for Iris to polish + Stefan to flip on if he wants both. Alternative: ship EN + DE stacked from day one; halves the per-language whitespace but doubles the page length (still tiny). Mira leans EN-only with the DE upgrade as a one-paragraph follow-up.
  5. Per-receipt-URL custom 410 page. Proposed default: no — every path falls through to the same sunset page. Implementing per-/receipt_<id>.pdf "Your receipt has moved — sign in to find it" is more thoughtful but requires path-aware static assets (or server logic). The blanket sunset page covers the case; the bookmarked-URL hint paragraph addresses it in copy. Mira leans the simpler default.
  6. Email Stefan's known clients with the new URL ahead of cutover. Proposed default: yes — a one-time blast to V1 clients with their email on record before V2 launches, telling them V2 is coming and they'll receive a magic-link invite. This is an operations question, not a design question — flagged here so Stefan doesn't lose track. Iris or Stefan owns the actual email content; it's not part of this spec's deliverable.
  7. Robots.txt for stringing.wagen.io. Proposed default: explicit Disallow: / in /robots.txt post-cutover, since there's nothing useful to crawl. Combined with the page-level noindex,follow meta. Alternative: leave robots.txt absent (default-allow) — Google indexes the sunset page once and leaves it. Mira leans explicit Disallow for cleanliness; either works.
  8. Sunset page copy review. Proposed default: Mira's draft above ships as-is. Tone is friendly and short; no marketing-speak. Stefan flips wording if he wants to adjust ("Stefan's stringing tracker" vs "the racket-book", etc.). Iris reviews DE.
  9. Long-term hostname strategy. Proposed default: keep stringing.wagen.io indefinitely as a forwarding sunset page. The cost is zero (static asset on keystone); the upside is bookmark continuity for any returning visitor years later. Alternative: retire the hostname after some time (e.g. 5 years). No reason to retire — keystone owns the cert; the page is unchanging.
  10. Ingress 200 vs 410 response. Proposed default: serve the sunset page with HTTP 200 (positive page; not "this is gone" semantics). Alternative: 410 Gone for /receipt_<id>.pdf paths and 200 for /. More semantically precise but requires path-aware Caddy logic; the all-200-fall-through pattern is simpler and costs nothing in real-world UX. Mira leans 200; Atlas may have a stronger SEO opinion.
  11. Add a "Sign in" link separate from the primary CTA. Proposed default: no — the primary CTA is the sign-in path. A second link clutters the page. The CTA copy "Open the new tracker →" is enough. Alternative: split into "Open new tracker" + "Sign in directly" — only useful if those are separate routes (they aren't; V2 root requires sign-in).