You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: plans/slug-history-redirect.md
+20-10Lines changed: 20 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,9 +1,10 @@
1
1
---
2
-
status: in-progress
2
+
status: done
3
3
depends: []
4
4
specs:
5
5
- specs/behaviors/slug-handles.md
6
6
issues: [80]
7
+
pr: 92
7
8
---
8
9
9
10
# Plan: SlugHistory 90-day redirect handler
@@ -117,13 +118,13 @@ The plugin uses `fastify.addHook('onRequest', ...)` so it fires for every reques
117
118
118
119
## Validation
119
120
120
-
-[]`InMemoryState.slugHistory` populated at boot from non-expired records; expired entries skipped.
121
-
-[] All three write services call `stateApply.upsertSlugHistory` after the gitsheets upsert.
122
-
-[] Fastify plugin registered after `services`, before `static-web`.
123
-
-[ ] All 9 test cases above pass.
124
-
-[ ] Existing 244 API tests still pass.
125
-
-[]`npm run type-check && npm run lint` clean.
126
-
-[] Spec compliance: GET `/<entity>/<old-slug>` with a non-expired SlugHistory → 301; live wins; multi-hop chain follows; expired → no redirect.
121
+
-[x]`InMemoryState.slugHistory` populated at boot from non-expired records; expired entries skipped via `indexSlugHistory`'s `expiresAt < now` guard.
122
+
-[x] All three write services call `stateApply.upsertSlugHistory` after the gitsheets upsert (`project.write.ts`, `person.write.ts`, `account-claim.ts`'s `MergeApply.replay`).
123
+
-[x] Fastify`slug-redirect` plugin registered after `services`, before `static-web`.
124
+
-[x] 11 test cases pass — single-hop project + person renames, sub-route preservation, query-string preservation, multi-hop A→B→C, live-wins, expired-skip, reserved-segment passthrough, tag rename, `/api/*` never intercepted, key-format determinism.
125
+
-[x] All 255 API tests pass (244 pre-existing + 11 new).
126
+
-[x]`npm run type-check && npm run lint` clean.
127
+
-[x] Spec compliance: GET `/<entity>/<old-slug>` with a non-expired SlugHistory → 301; live wins; multi-hop chain follows; expired → no redirect.
127
128
128
129
## Risks / unknowns
129
130
@@ -135,8 +136,17 @@ The plugin uses `fastify.addHook('onRequest', ...)` so it fires for every reques
135
136
136
137
## Notes
137
138
138
-
*(filled at done time)*
139
+
Shipped over five commits — plan opening + three implementation steps (in-memory state, write-service wiring, Fastify plugin) + tests.
140
+
141
+
Surprises:
142
+
143
+
-**Account-claim's MergeApply needed slug-history threading.** Project + person renames live in their own write services where the StateApply is directly accessible, so wiring `upsertSlugHistory` was a one-line addition. Account-claim uses a `MergeApply` wrapper that batches all the post-onboarding rewrites for later replay onto the route-level StateApply — slug-history needed to ride along through that wrapper, which meant adding it to `MergeApplyInput` + `MergeApply.replay`.
144
+
-**Buzz live-index is intentionally fake.** Buzz slugs are keyed by `${projectId}:${buzzSlug}` in the live index (`buzzByProjectAndSlug`); we don't have the projectId cheaply at the URL-pattern level (only the project's slug). For now the buzz pattern always returns `false` from `liveIndex` — which means a slug-history hit always wins for buzz, regardless of whether the buzz slug is live under a different project. No writer currently creates buzz slug-history records, so this is hypothetical.
145
+
-**Tag live-check scans `tagIdByHandle.keys()`.** Tags are uniquely keyed by `(namespace, slug)` but slug-history's key is `tag:<slug>` (no namespace). The live-check walks the handle map looking for any namespace that owns the slug. Tag slug-history has no live writer today either; the conservative live-wins behavior matches the spec.
146
+
-**The 5-min `Cache-Control` is a deliberate undersizing.** 301s are normally aggressively cached by browsers. Because our redirects can expire when the 90-day SlugHistory TTL hits, we keep cache headers short so stale-redirect windows after expiry are bounded to ~5 minutes.
139
147
140
148
## Follow-ups
141
149
142
-
*(filled at done time)*
150
+
-**Periodic sweeper to purge expired SlugHistory records from the sheet.** The read path is already defensive — expired records are filtered at index time — so this is purely about keeping the on-disk sheet from growing forever. *Tracked as*: file a new issue when sheet bloat becomes measurable; today's volume is negligible.
151
+
-**Buzz live-index** — when buzz renames become real, add a `buzzByGlobalSlug` map (or accept the conservative "always redirect on slug-history hit"). *Deferred* until buzz rename writers exist.
152
+
-**CDN/edge cache awareness** — if/when we put the site behind a CDN, 301s would benefit from explicit cache-key handling so the redirect doesn't outlive its TTL at the edge. *Deferred* until cutover plans introduce a CDN.
0 commit comments