Migration Plan
Migration from the current flat routing system to nested routing. Each phase is independently deployable. The existing site continues to work at every intermediate step.
Phase 0: Asset Storage Reorganization
Goal: Move assets from section-based paths (uploads/photos/{section_id}/) to type-based paths (uploads/images/originals/{asset_id}). Must complete before content moves are enabled.
Tasks
- Write migration to scan all files under
uploads/photos/anduploads/video/ - Move each file to the new directory structure (see Asset Storage)
- Rewrite all asset URLs in content collections (stories, projects, pages, photo essays)
- Rewrite srcset attributes to point to new variant directories
- Update the upload handler to write new files to the type-based structure
- Update the image processing pipeline output directories
- Update the video processing pipeline output directories
- Handle both local filesystem (
LOCAL_STORAGE_PATH) and GCS bucket - Update the Next.js static file proxy if it has path assumptions
Tests
- All existing content renders images and videos correctly after migration
- New uploads go to the correct type-based directories
- srcset attributes point to valid, accessible URLs
- Both local dev and GCS production paths work
Phase 1: Data Model Migration
Goal: Update the section model, add the path field, create the redirects collection, update indexes.
Tasks
- Add
pathfield to section model (computed fromparent_idchain) - Write migration to populate
pathon all existing sections (top-level sections getpath = slug) - Remove
content_typefrom section model - Change slug uniqueness index from global
slugto compound(parent_id, slug) - Create
redirectscollection withold_pathunique index - Add
content_typefield to each content collection if not already present (stories get"story", projects get"project", etc.) - Add compound
(section_id, slug)uniqueness index on each content collection
Backward Compatibility
- Existing sections are all top-level (
parent_id: null), sopath = slugfor all of them - Removing
content_typefrom sections means the frontend must stop relying on it — handled in Phase 3 - The new indexes are additive; old queries still work
Tests
- Migration populates
pathcorrectly for all existing sections - New uniqueness constraints don’t conflict with existing data
- Redirects collection is empty and queryable
Phase 2: Path Resolution Endpoint
Goal: Build GET /sections/resolve-path/{full_path} on the backend.
Tasks
- Implement
resolve_pathhandler with the algorithm from the Path Resolution doc - Implement
find_content_in_sectionhelper that searches across all content collections - Implement breadcrumb builder (walk ancestors from resolved section to root)
- Implement redirect lookup as fallback when path doesn’t resolve
- Add auth-aware handling: authenticated requests can resolve unpublished sections/content
Tests
- Full test suite from Testing Strategy: path resolution section
- Benchmark: resolution of a 5-deep path completes in under 50ms
Phase 3: Children Endpoint
Goal: Build GET /sections/{id}/children that returns mixed content types as ListingItem.
Tasks
- Define
ListingItemprojection for each content collection - Implement the children endpoint: query child sections + content items, merge and paginate
- Implement
subtree=truemode: collect descendant section IDs, query content across all of them - Implement
featured_only=truefilter - Ensure computed listing fields (summary from HTML content, image_url from content body) are stored on write, not computed on read
Tests
- Full test suite from Testing Strategy: children endpoint section
- Mixed content types in response conform to ListingItem shape
- Subtree queries return correct scoped results
Phase 4: Move & Redirect Operations
Goal: Build the move endpoints and redirect write logic.
Tasks
- Implement
PUT /content/{content_type}/{content_id}/move— move content between sections - Implement
PUT /sections/{section_id}/move— move section to new parent - Implement cycle detection for section moves
- Implement slug collision checking for both content and section moves
- Implement redirect flattening (update existing redirects pointing to old path)
- Implement path cascade on section move/rename (rebuild
pathon all descendants) - Wire redirect resolution into
resolve_pathendpoint (301 response on redirect hit)
Tests
- Full test suite from Testing Strategy: redirect mechanics section
- Bulk move of a section with 10 descendants creates correct redirects for all
Phase 5: Frontend Routing Rewrite
Goal: Replace the current catch-all route logic with the new resolve-path integration.
Tasks
- Update
[...slugPath].tsxgetServerSidePropsto call/sections/resolve-path/{path} - Handle
type: "section"response — render section’s display type with children from/sections/{id}/children - Handle
type: "content"response — render detail view based oncontent_item.content_type - Handle 301 response — return Next.js redirect
- Handle 404 response — return notFound
- Render breadcrumbs from response data
- Remove old per-type API calls from the catch-all route (no more direct
/stories,/projectscalls from SSR) - Update the Next.js API proxy layer to pass through the new endpoints
Tests
- Frontend unit tests from Testing Strategy
- Manual verification of all existing URLs
Phase 6: Display Component Refactor
Goal: Update display components to consume ListingItem[] instead of type-specific arrays.
Tasks
- Create unified
ListingCardcomponent that renders anyListingItem - Update
CardGridDisplayto useListingCardinstead ofProjectCard - Update
FeedDisplayto useListingCardinstead ofStoryCard - Update
GalleryDisplayto useListingCard - Remove per-type list components (
StoryCard,ProjectCardfor list contexts — detail components remain) - Update content registry: remove
listItementries (no longer needed per-type) - Add content type picker to “new content” action (since sections no longer imply a type)
Tests
- Visual regression: existing section pages render the same content with new components
- Mixed content types render correctly in all display types
Phase 7: Seed Data & E2E Tests
Goal: Update test infrastructure for nested routing.
Tasks
- Create seed migration for nested section hierarchy (the tree from testing-strategy.md)
- Seed 2-3 content items per section
- Update e2e mock server (
mock-server.ts) withresolve-pathendpoint - Update e2e test data (
test-data.ts) with nested section hierarchy - Update API mock fixture for client-side calls
- Write all Playwright e2e tests from Testing Strategy
- Update
make dev-localto auto-seed nested hierarchy
Tests
- All e2e tests pass
make testpassesmake test-frontend-unitpassesmake test-frontendpasses
Phase 8: Documentation & Cleanup
Goal: Final documentation updates, remove dead code.
Tasks
- Update
docs-site/pages/architecture/overview.mdxwith nested routing architecture - Write ADR for the migration decision
- Remove old per-type API proxy routes that are no longer used
- Remove
content_typereferences from section-related frontend code - Update CLAUDE.md if any commands or conventions changed
- Clean up any feature flags or backward-compatibility code from the transition
Risk Mitigation
| Risk | Mitigation |
|---|---|
| Existing URLs break | Phase 1 populates path = slug for current sections; Phase 5 resolves them identically to the old system |
| Performance regression on deep paths | Materialized path field with index means resolution is O(1) for section lookup, O(N) only for content search across collections |
| Data migration corrupts existing content | Migration is additive — new fields and indexes only; no existing fields are removed until Phase 8 cleanup |
| Move operation creates orphaned redirects | Redirect flattening prevents chains; expired redirects are cleaned up by TTL or admin action |
| Frontend/backend version skew during deploy | Each phase is independently deployable; the old frontend works against the new backend and vice versa during transition |