18 KiB
PHASE 12: Next.js 15 App Router Pages Migration
Status: ⬜ Not Started
Estimated Time: 45-55 hours
Priority: CRITICAL
Depends On: All previous phases (1-11)
Blocks: Phase 13 (Next.js Optimization)
🎯 Goal
Convert ALL React SPA pages to Next.js 15 App Router pages while replacing Supabase calls with Django services. This is a DUAL migration:
- React → Next.js App Router: Convert pages and routing
- Supabase → Django: Replace all data fetching
Critical Requirements:
- Preserve ALL existing URLs
- Maintain exact same UI/UX
- Use Server Components by default
- Mark Client Components with 'use client'
- Sacred Pipeline remains intact
📋 Next.js App Router Structure
New Directory Structure
app/
├── layout.tsx # Root layout
├── page.tsx # Homepage
├── loading.tsx # Global loading
├── error.tsx # Global error
├── not-found.tsx # 404 page
│
├── parks/
│ ├── page.tsx # /parks (listing)
│ ├── loading.tsx # Loading state
│ ├── [parkSlug]/
│ │ ├── page.tsx # /parks/[parkSlug]
│ │ ├── loading.tsx
│ │ └── rides/
│ │ └── page.tsx # /parks/[parkSlug]/rides
│ └── owners/
│ └── [ownerSlug]/
│ └── page.tsx # /owners/[ownerSlug]/parks
│
├── rides/
│ ├── page.tsx # /rides (listing)
│ ├── [rideSlug]/
│ │ ├── page.tsx # /rides/[rideSlug]
│ │ └── reviews/
│ │ └── page.tsx # /rides/[rideSlug]/reviews
│ └── models/
│ └── [modelSlug]/
│ ├── page.tsx # /ride-models/[modelSlug]
│ └── rides/
│ └── page.tsx # /ride-models/[modelSlug]/rides
│
├── manufacturers/
│ ├── page.tsx # /manufacturers (listing)
│ └── [manufacturerSlug]/
│ ├── page.tsx # /manufacturers/[manufacturerSlug]
│ └── rides/
│ └── page.tsx # /manufacturers/[manufacturerSlug]/rides
│
├── owners/
│ ├── page.tsx # /owners (listing)
│ └── [ownerSlug]/
│ └── page.tsx # /owners/[ownerSlug]
│
├── designers/
│ ├── page.tsx # /designers (listing)
│ └── [designerSlug]/
│ └── page.tsx # /designers/[designerSlug]
│
├── auth/
│ ├── login/
│ │ └── page.tsx
│ ├── register/
│ │ └── page.tsx
│ └── callback/
│ └── page.tsx
│
├── profile/
│ ├── page.tsx # /profile
│ ├── settings/
│ │ └── page.tsx
│ └── lists/
│ └── page.tsx
│
├── admin/
│ ├── page.tsx # /admin
│ └── moderation/
│ └── page.tsx # /admin/moderation
│
├── search/
│ └── page.tsx # /search
│
└── contact/
└── page.tsx # /contact
📋 Tasks
Task 12.1: Root Layout & Homepage (6 hours)
Create Root Layout
app/layout.tsx (Server Component):
import { Inter } from 'next/font/google';
import './globals.css';
import { AuthProvider } from '@/components/providers/AuthProvider';
import { Navigation } from '@/components/layout/Navigation';
import { Footer } from '@/components/layout/Footer';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'ThrillWiki - Theme Park & Ride Database',
description: 'Comprehensive database of theme parks and rides',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<AuthProvider>
<Navigation />
<main className="min-h-screen">{children}</main>
<Footer />
</AuthProvider>
</body>
</html>
);
}
Convert Homepage
app/page.tsx (Server Component):
import { env } from '@/lib/env';
import { FeaturedParks } from '@/components/home/FeaturedParks';
import { RecentReviews } from '@/components/home/RecentReviews';
import { Stats } from '@/components/home/Stats';
export default async function HomePage() {
// Fetch data server-side
const [parks, stats] = await Promise.all([
fetch(`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/?featured=true`).then(r => r.json()),
fetch(`${env.NEXT_PUBLIC_DJANGO_API_URL}/stats/`).then(r => r.json()),
]);
return (
<div>
<Hero />
<Stats data={stats} />
<FeaturedParks parks={parks} />
<RecentReviews />
</div>
);
}
Checklist
- Create
app/layout.tsxwith root layout - Create
app/page.tsxfor homepage - Create
app/loading.tsxfor loading state - Create
app/error.tsxfor error boundary - Create
app/not-found.tsxfor 404 - Move CSS to
app/globals.css - Test homepage loads
- Verify navigation works
- Check loading states
- Test error boundaries
Task 12.2: Park Pages (8 hours)
Park Listing Page
app/parks/page.tsx (Server Component):
import { env } from '@/lib/env';
import { ParksList } from '@/components/parks/ParksList';
import { ParkFilters } from '@/components/parks/ParkFilters';
export const metadata = {
title: 'Theme Parks - ThrillWiki',
description: 'Browse theme parks from around the world',
};
export default async function ParksPage({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) {
const params = new URLSearchParams(searchParams as any);
const parks = await fetch(
`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/?${params}`,
{ next: { revalidate: 300 } } // Cache for 5 minutes
).then(r => r.json());
return (
<div>
<h1>Theme Parks</h1>
<ParkFilters /> {/* Client Component */}
<ParksList parks={parks} /> {/* Can be Server Component */}
</div>
);
}
Park Detail Page
app/parks/[parkSlug]/page.tsx (Server Component):
import { env } from '@/lib/env';
import { ParkDetail } from '@/components/parks/ParkDetail';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
// Pre-render top 100 parks
const parks = await fetch(`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/?page_size=100`)
.then(r => r.json());
return parks.results.map((park: any) => ({
parkSlug: park.slug,
}));
}
export async function generateMetadata({ params }: { params: { parkSlug: string } }) {
const park = await fetch(`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/${params.parkSlug}/`)
.then(r => r.json());
return {
title: `${park.name} - ThrillWiki`,
description: park.description,
};
}
export default async function ParkDetailPage({
params
}: {
params: { parkSlug: string }
}) {
const park = await fetch(
`${env.NEXT_PUBLIC_DJANGO_API_URL}/parks/${params.parkSlug}/`,
{ next: { revalidate: 3600 } } // Cache for 1 hour
).then(r => r.json())
.catch(() => notFound());
return <ParkDetail park={park} />;
}
Checklist
- Create
app/parks/page.tsx - Create
app/parks/[parkSlug]/page.tsx - Create
app/parks/[parkSlug]/rides/page.tsx - Create
app/owners/[ownerSlug]/parks/page.tsx - Create loading.tsx for each route
- Implement generateStaticParams for ISR
- Implement generateMetadata for SEO
- Test all park URLs work
- Verify filtering works
- Test pagination
Task 12.3: Ride Pages (8 hours)
Ride Listing Page
app/rides/page.tsx (Server Component with caching)
Ride Detail Page
app/rides/[rideSlug]/page.tsx (Server Component)
Ride Model Pages
app/ride-models/[modelSlug]/page.tsx (Server Component)
Checklist
- Create
app/rides/page.tsx - Create
app/rides/[rideSlug]/page.tsx - Create
app/rides/[rideSlug]/reviews/page.tsx - Create
app/ride-models/[modelSlug]/page.tsx - Create
app/ride-models/[modelSlug]/rides/page.tsx - Create loading states
- Implement metadata
- Test all ride URLs
- Verify reviews work
Task 12.4: Company Pages (8 hours)
Convert manufacturer, owner, and designer pages to App Router.
Checklist
- Create
app/manufacturers/page.tsx - Create
app/manufacturers/[manufacturerSlug]/page.tsx - Create
app/manufacturers/[manufacturerSlug]/rides/page.tsx - Create
app/owners/page.tsx - Create
app/owners/[ownerSlug]/page.tsx - Create
app/designers/page.tsx - Create
app/designers/[designerSlug]/page.tsx - Test all company URLs
- Verify filtering works
Task 12.5: User Pages (6 hours)
These pages need authentication and user-specific data.
Profile Page
app/profile/page.tsx (Server Component with auth check):
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
import { ProfileView } from '@/components/profile/ProfileView';
export default async function ProfilePage() {
const session = await getServerSession();
if (!session) {
redirect('/auth/login');
}
const user = await fetch(
`${env.NEXT_PUBLIC_DJANGO_API_URL}/users/me/`,
{
headers: { Authorization: `Bearer ${session.accessToken}` },
cache: 'no-store' // Don't cache user-specific data
}
).then(r => r.json());
return <ProfileView user={user} />;
}
Checklist
- Create
app/profile/page.tsx - Create
app/profile/settings/page.tsx - Create
app/profile/lists/page.tsx - Implement authentication checks
- Disable caching for user data
- Test profile loads
- Test settings work
- Test list management
Task 12.6: Admin & Moderation Pages (6 hours)
Admin pages require role checks and real-time updates.
Moderation Queue
app/admin/moderation/page.tsx (Server Component):
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
import { ModerationQueue } from '@/components/moderation/ModerationQueue';
export default async function ModerationPage() {
const session = await getServerSession();
if (!session || session.user.role !== 'moderator') {
redirect('/');
}
// Fetch submissions server-side
const submissions = await fetch(
`${env.NEXT_PUBLIC_DJANGO_API_URL}/moderation/queue/`,
{
headers: { Authorization: `Bearer ${session.accessToken}` },
cache: 'no-store'
}
).then(r => r.json());
return <ModerationQueue initialSubmissions={submissions} />;
}
Checklist
- Create
app/admin/page.tsx - Create
app/admin/moderation/page.tsx - Implement role-based access
- Add server-side permission checks
- Test moderator access
- Test admin dashboard
- Verify Sacred Pipeline
Task 12.7: Authentication Pages (4 hours)
Login Page
app/auth/login/page.tsx (Client Component - needs form state):
'use client';
import { LoginForm } from '@/components/auth/LoginForm';
import { redirect } from 'next/navigation';
import { useSession } from 'next-auth/react';
export default function LoginPage() {
const { data: session } = useSession();
if (session) {
redirect('/profile');
}
return (
<div>
<h1>Login</h1>
<LoginForm />
</div>
);
}
Checklist
- Create
app/auth/login/page.tsx - Create
app/auth/register/page.tsx - Create
app/auth/callback/page.tsx - Create
app/auth/reset-password/page.tsx - Test email/password login
- Test OAuth login (Google, GitHub)
- Test registration
- Test password reset
Task 12.8: Search & Contact Pages (3 hours)
Search Page
app/search/page.tsx (Client Component - needs interactive search):
'use client';
import { useSearchParams } from 'next/navigation';
import { SearchResults } from '@/components/search/SearchResults';
import { SearchFilters } from '@/components/search/SearchFilters';
export default function SearchPage() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return (
<div>
<h1>Search Results</h1>
<SearchFilters />
<SearchResults query={query} />
</div>
);
}
Contact Page
app/contact/page.tsx (Server Component):
import { ContactForm } from '@/components/contact/ContactForm';
export const metadata = {
title: 'Contact Us - ThrillWiki',
};
export default function ContactPage() {
return (
<div>
<h1>Contact Us</h1>
<ContactForm />
</div>
);
}
Checklist
- Create
app/search/page.tsx - Create
app/contact/page.tsx - Test search functionality
- Test contact form submission
- Verify form validation
Task 12.9: Component Migration (6 hours)
Convert React components to work with Next.js.
Server Components (Default)
Components that only display data and don't need interactivity:
- Lists (ParksL, RidesList)
- Detail views (ParkDetail, RideDetail)
- Static content
Client Components ('use client')
Components that need interactivity:
- Forms (LoginForm, SubmissionForm)
- Interactive filters
- Modals and dialogs
- Components using useState, useEffect, etc.
Checklist
- Identify all components using Supabase
- Determine Server vs Client Component
- Add 'use client' where needed
- Replace Supabase with service calls
- Test all components render
- Verify interactions work
- Check no hydration errors
Task 12.10: Routing & Navigation (4 hours)
Update all navigation to use Next.js routing.
Replace React Router
// OLD (React Router)
import { Link, useNavigate } from 'react-router-dom';
// NEW (Next.js)
import Link from 'next/link';
import { useRouter } from 'next/navigation';
Update All Links
// OLD
<Link to="/parks">Parks</Link>
// NEW
<Link href="/parks">Parks</Link>
Checklist
- Replace all React Router imports
- Update all components
- Update all navigate() calls to router.push()
- Update all useParams to use params prop
- Update all useSearchParams
- Test navigation works
- Test browser back button
- Test deep linking
🎯 Success Criteria
Zero React Router Usage
- No
react-router-domimports remain - All navigation uses Next.js Link/router
- No old React pages in
src/pages/(move toapp/)
Zero Supabase Usage
grep -r "supabase\." app/ components/ lib/ --include="*.ts" --include="*.tsx"returns 0grep -r "from '@/lib/supabaseClient'"returns 0grep -r "from '@supabase/supabase-js'"returns 0
All URLs Preserved
/parksworks/parks/[parkSlug]works/parks/[parkSlug]/ridesworks/owners/[ownerSlug]/parksworks/ridesworks/rides/[rideSlug]works/ride-models/[modelSlug]works/manufacturers/[manufacturerSlug]works/manufacturers/[manufacturerSlug]/ridesworks- All other URLs work
All Pages Load
- Homepage loads (SSR)
- All park pages load
- All ride pages load
- All company pages load
- Profile page loads (auth check)
- Settings page loads (auth check)
- Admin dashboard loads (role check)
- Moderation queue loads (role check)
- Search page loads
- Contact page loads
- Auth pages load
All Features Work
- Can browse entities
- Can view entity details
- Can filter/sort
- Can submit content (creates submission)
- Can moderate content
- Can write reviews
- Can add ride credits
- Can manage top lists
- Can upload photos
- Can search
- Authentication works
- Server-side rendering works
- Client-side navigation works
Next.js Specific
- Server Components render on server
- Client Components work in browser
- No hydration errors
- Loading states display
- Error boundaries catch errors
- Metadata API generates correct tags
- ISR/SSR configured correctly
- Build succeeds without errors
Sacred Pipeline Intact
- All entity changes go through submissions
- Moderation queue receives submissions
- Approval creates entities/updates
- Rejection saves reason
- No pipeline bypasses
📝 Implementation Strategy
1. Start with Entity Pages (Most Critical)
Focus on parks, rides, companies first since they're core functionality.
2. Then User Pages
Profile and settings are user-facing and important.
3. Then Admin Pages
Moderation queue and admin dashboard.
4. Then Misc Pages
Homepage, search, contact, etc.
5. Component Sweep
Find any remaining components with Supabase calls.
6. Final Search
Run grep commands to find ANY remaining Supabase usage.
🚨 Critical Rules
DO NOT
- ❌ Skip any page
- ❌ Leave any
supabase.calls - ❌ Assume a page works without testing it
- ❌ Move to Phase 13 until grep returns 0 results
MUST DO
- ✅ Test EVERY page you touch
- ✅ Verify EVERY interaction works
- ✅ Check console for errors
- ✅ Verify Sacred Pipeline intact
- ✅ Document any issues found
🔗 Related Documentation
Refer back to previous phases for service usage examples:
- Phase 1: Foundation (BaseService pattern)
- Phase 4: Entity Services (parks, rides, companies)
- Phase 5: Reviews & Social
- Phase 6: Moderation & Admin
- Phase 7: Media & Photos
- Phase 8: Search
- Phase 9: Timeline & History
- Phase 10: Users & Profiles
- Phase 11: Contact & Reports
⏭️ Next Phase
Once this phase is complete, proceed to Phase 13: Next.js Optimization
🔗 Related Documentation
- Next.js 15 Migration Guide
- Environment Variables Guide
- Phase 1: Foundation
- Phase 13: Next.js Optimization
- Next.js App Router: https://nextjs.org/docs/app
- Server Components: https://nextjs.org/docs/app/building-your-application/rendering/server-components
Document Version: 2.0
Last Updated: November 9, 2025
Changes: Converted to Next.js 15 App Router migration