mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:31:13 -05:00
548 lines
12 KiB
Markdown
548 lines
12 KiB
Markdown
# PHASE 13: Next.js 15 Optimization
|
|
|
|
**Status:** ⬜ Not Started
|
|
**Estimated Time:** 15-20 hours
|
|
**Priority:** HIGH
|
|
**Depends On:** Phase 12 (Next.js Pages Migration)
|
|
**Blocks:** Phase 14 (Cleanup & Testing)
|
|
|
|
---
|
|
|
|
## 🎯 Phase Goal
|
|
|
|
Optimize the Next.js 15 application for production with Turbopack, implement advanced features, and ensure peak performance.
|
|
|
|
---
|
|
|
|
## 📋 Prerequisites
|
|
|
|
- [ ] Phase 12 complete (all pages migrated to Next.js App Router)
|
|
- [ ] All services converted from Supabase to Django API
|
|
- [ ] Sacred Pipeline working in Next.js
|
|
- [ ] Bun is configured as package manager
|
|
- [ ] Environment variables properly configured
|
|
|
|
---
|
|
|
|
## ✅ Task 13.1: Turbopack Configuration (3 hours)
|
|
|
|
### Configure next.config.js for Turbopack
|
|
|
|
Create/update `next.config.js`:
|
|
|
|
```javascript
|
|
/** @type {import('next').NextConfig} */
|
|
const nextConfig = {
|
|
// Enable Turbopack for development
|
|
experimental: {
|
|
turbo: {
|
|
rules: {
|
|
'*.svg': {
|
|
loaders: ['@svgr/webpack'],
|
|
as: '*.js',
|
|
},
|
|
},
|
|
resolveAlias: {
|
|
'@': './src',
|
|
},
|
|
},
|
|
},
|
|
|
|
// Image optimization
|
|
images: {
|
|
remotePatterns: [
|
|
{
|
|
protocol: 'https',
|
|
hostname: 'imagedelivery.net', // CloudFlare Images
|
|
},
|
|
{
|
|
protocol: 'https',
|
|
hostname: process.env.NEXT_PUBLIC_DJANGO_API_URL?.replace('https://', '') || 'api.thrillwiki.com',
|
|
},
|
|
],
|
|
formats: ['image/avif', 'image/webp'],
|
|
},
|
|
|
|
// Environment variables
|
|
env: {
|
|
NEXT_PUBLIC_DJANGO_API_URL: process.env.NEXT_PUBLIC_DJANGO_API_URL,
|
|
},
|
|
|
|
// Production optimizations
|
|
compress: true,
|
|
poweredByHeader: false,
|
|
reactStrictMode: true,
|
|
|
|
// Output configuration
|
|
output: 'standalone', // For Docker deployment
|
|
};
|
|
|
|
module.exports = nextConfig;
|
|
```
|
|
|
|
### Checklist
|
|
- [ ] `next.config.js` created/updated
|
|
- [ ] Turbopack rules configured
|
|
- [ ] Image optimization configured for CloudFlare
|
|
- [ ] Environment variables properly referenced
|
|
- [ ] Production flags enabled
|
|
- [ ] Development server uses Turbopack
|
|
- [ ] Hot reload works correctly
|
|
|
|
---
|
|
|
|
## ✅ Task 13.2: Server vs Client Component Optimization (4 hours)
|
|
|
|
### Identify Component Types
|
|
|
|
**Server Components (Default):**
|
|
- Data fetching pages
|
|
- Static content
|
|
- SEO-critical pages
|
|
- No user interaction
|
|
|
|
**Client Components (Need 'use client'):**
|
|
- Forms with state
|
|
- Interactive UI elements
|
|
- Hooks (useState, useEffect, etc.)
|
|
- Event handlers
|
|
- Context providers
|
|
|
|
### Optimization Checklist
|
|
|
|
#### Server Components
|
|
- [ ] All data-fetching pages are Server Components
|
|
- [ ] Park listing page is Server Component
|
|
- [ ] Ride listing page is Server Component
|
|
- [ ] Detail pages use Server Components for data
|
|
- [ ] SEO metadata generated server-side
|
|
|
|
#### Client Components
|
|
- [ ] Form components marked with 'use client'
|
|
- [ ] Interactive UI (modals, dropdowns) marked
|
|
- [ ] Authentication components marked
|
|
- [ ] Submission forms marked
|
|
- [ ] No unnecessary 'use client' directives
|
|
|
|
#### Component Structure
|
|
```typescript
|
|
// Good: Server Component with Client Components inside
|
|
// app/parks/page.tsx
|
|
export default async function ParksPage() {
|
|
const parks = await fetch(API_URL).then(r => r.json());
|
|
|
|
return (
|
|
<div>
|
|
<h1>Parks</h1>
|
|
<ParkFilters /> {/* Client Component */}
|
|
<ParksList parks={parks} /> {/* Can be Server Component */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Client Component for interactivity
|
|
// components/ParkFilters.tsx
|
|
'use client';
|
|
export function ParkFilters() {
|
|
const [filters, setFilters] = useState({});
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Task 13.3: Data Fetching Optimization (3 hours)
|
|
|
|
### Implement Caching Strategies
|
|
|
|
```typescript
|
|
// app/parks/[parkSlug]/page.tsx
|
|
export async function generateStaticParams() {
|
|
// Pre-render top 100 parks at build time
|
|
const parks = await fetch(`${API_URL}/parks/?page_size=100`);
|
|
return parks.results.map((park) => ({
|
|
parkSlug: park.slug,
|
|
}));
|
|
}
|
|
|
|
export default async function ParkDetailPage({
|
|
params
|
|
}: {
|
|
params: { parkSlug: string }
|
|
}) {
|
|
// Cache for 1 hour, revalidate in background
|
|
const park = await fetch(`${API_URL}/parks/${params.parkSlug}/`, {
|
|
next: { revalidate: 3600 }
|
|
}).then(r => r.json());
|
|
|
|
return <ParkDetail park={park} />;
|
|
}
|
|
```
|
|
|
|
### Caching Strategy Checklist
|
|
- [ ] Static pages use ISR (Incremental Static Regeneration)
|
|
- [ ] Dynamic pages have appropriate revalidation times
|
|
- [ ] Listing pages cache for 5-10 minutes
|
|
- [ ] Detail pages cache for 1 hour
|
|
- [ ] User-specific pages are not cached
|
|
- [ ] Search results are not cached
|
|
|
|
### Parallel Data Fetching
|
|
```typescript
|
|
// Fetch multiple resources in parallel
|
|
export default async function RideDetailPage({ params }) {
|
|
const [ride, reviews, photos] = await Promise.all([
|
|
fetch(`${API_URL}/rides/${params.rideSlug}/`),
|
|
fetch(`${API_URL}/rides/${params.rideSlug}/reviews/`),
|
|
fetch(`${API_URL}/rides/${params.rideSlug}/photos/`),
|
|
]);
|
|
|
|
return <RideDetail ride={ride} reviews={reviews} photos={photos} />;
|
|
}
|
|
```
|
|
|
|
- [ ] Parallel fetching implemented where possible
|
|
- [ ] No waterfall requests
|
|
- [ ] Loading states for slow requests
|
|
|
|
---
|
|
|
|
## ✅ Task 13.4: Loading States & Suspense (2 hours)
|
|
|
|
### Create Loading Components
|
|
|
|
```typescript
|
|
// app/parks/loading.tsx
|
|
export default function Loading() {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="h-8 bg-gray-200 rounded animate-pulse" />
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{[...Array(6)].map((_, i) => (
|
|
<div key={i} className="h-48 bg-gray-200 rounded animate-pulse" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Implement Streaming
|
|
|
|
```typescript
|
|
// app/parks/[parkSlug]/page.tsx
|
|
import { Suspense } from 'react';
|
|
|
|
export default async function ParkPage({ params }) {
|
|
return (
|
|
<div>
|
|
<Suspense fallback={<ParkHeaderSkeleton />}>
|
|
<ParkHeader slug={params.parkSlug} />
|
|
</Suspense>
|
|
|
|
<Suspense fallback={<RidesListSkeleton />}>
|
|
<RidesList parkSlug={params.parkSlug} />
|
|
</Suspense>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Checklist
|
|
- [ ] Loading.tsx for each major route
|
|
- [ ] Suspense boundaries for slow components
|
|
- [ ] Skeleton screens match final UI
|
|
- [ ] Streaming SSR enabled
|
|
- [ ] No flash of loading state
|
|
|
|
---
|
|
|
|
## ✅ Task 13.5: Metadata API for SEO (2 hours)
|
|
|
|
### Implement Dynamic Metadata
|
|
|
|
```typescript
|
|
// app/parks/[parkSlug]/page.tsx
|
|
import { Metadata } from 'next';
|
|
|
|
export async function generateMetadata({
|
|
params
|
|
}: {
|
|
params: { parkSlug: string }
|
|
}): Promise<Metadata> {
|
|
const park = await fetch(`${API_URL}/parks/${params.parkSlug}/`)
|
|
.then(r => r.json());
|
|
|
|
return {
|
|
title: `${park.name} - ThrillWiki`,
|
|
description: park.description || `Discover ${park.name}, a ${park.park_type} featuring exciting rides and attractions.`,
|
|
openGraph: {
|
|
title: park.name,
|
|
description: park.description,
|
|
images: [
|
|
{
|
|
url: park.banner_image_url || '/og-image.png',
|
|
width: 1200,
|
|
height: 630,
|
|
alt: park.name,
|
|
},
|
|
],
|
|
type: 'website',
|
|
siteName: 'ThrillWiki',
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title: park.name,
|
|
description: park.description,
|
|
images: [park.banner_image_url || '/og-image.png'],
|
|
},
|
|
alternates: {
|
|
canonical: `https://thrillwiki.com/parks/${park.slug}`,
|
|
},
|
|
};
|
|
}
|
|
```
|
|
|
|
### SEO Checklist
|
|
- [ ] All pages have unique titles
|
|
- [ ] All pages have meta descriptions
|
|
- [ ] OpenGraph tags for social sharing
|
|
- [ ] Twitter Card tags
|
|
- [ ] Canonical URLs
|
|
- [ ] Structured data (JSON-LD)
|
|
- [ ] Robots.txt configured
|
|
- [ ] Sitemap.xml generated
|
|
|
|
---
|
|
|
|
## ✅ Task 13.6: Performance Optimization (3 hours)
|
|
|
|
### Bundle Analysis
|
|
|
|
```bash
|
|
# Install bundle analyzer
|
|
bun add @next/bundle-analyzer
|
|
|
|
# Update next.config.js
|
|
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
|
enabled: process.env.ANALYZE === 'true',
|
|
});
|
|
|
|
module.exports = withBundleAnalyzer(nextConfig);
|
|
|
|
# Run analysis
|
|
ANALYZE=true bun run build
|
|
```
|
|
|
|
### Code Splitting
|
|
|
|
```typescript
|
|
// Use dynamic imports for heavy components
|
|
import dynamic from 'next/dynamic';
|
|
|
|
const HeavyChart = dynamic(() => import('./HeavyChart'), {
|
|
loading: () => <p>Loading chart...</p>,
|
|
ssr: false, // Don't render on server
|
|
});
|
|
```
|
|
|
|
### Performance Checklist
|
|
- [ ] Bundle size < 500KB initial load
|
|
- [ ] Code splitting for large dependencies
|
|
- [ ] Images use next/image
|
|
- [ ] Fonts optimized with next/font
|
|
- [ ] CSS modules for component styles
|
|
- [ ] No unused dependencies
|
|
- [ ] Tree shaking working
|
|
|
|
### Core Web Vitals Targets
|
|
- [ ] LCP (Largest Contentful Paint) < 2.5s
|
|
- [ ] FID (First Input Delay) < 100ms
|
|
- [ ] CLS (Cumulative Layout Shift) < 0.1
|
|
- [ ] TTFB (Time to First Byte) < 600ms
|
|
|
|
---
|
|
|
|
## ✅ Task 13.7: Environment Variables & Security (1 hour)
|
|
|
|
### Environment Configuration
|
|
|
|
Create `.env.local`:
|
|
```bash
|
|
# Django API (required for build)
|
|
NEXT_PUBLIC_DJANGO_API_URL=https://api.thrillwiki.com
|
|
|
|
# CloudFlare (public)
|
|
NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID=xxx
|
|
|
|
# Server-only secrets (no NEXT_PUBLIC_ prefix)
|
|
CLOUDFLARE_API_TOKEN=xxx
|
|
DJANGO_SECRET_KEY=xxx
|
|
```
|
|
|
|
### Security Checklist
|
|
- [ ] No secrets in client-side code
|
|
- [ ] Server-only vars don't have NEXT_PUBLIC_ prefix
|
|
- [ ] .env.local in .gitignore
|
|
- [ ] .env.example documented
|
|
- [ ] Environment validation on startup
|
|
- [ ] Sensitive data not logged
|
|
|
|
### Runtime Validation
|
|
|
|
```typescript
|
|
// lib/env.ts
|
|
import { z } from 'zod';
|
|
|
|
const envSchema = z.object({
|
|
NEXT_PUBLIC_DJANGO_API_URL: z.string().url(),
|
|
NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID: z.string().min(1),
|
|
});
|
|
|
|
export const env = envSchema.parse({
|
|
NEXT_PUBLIC_DJANGO_API_URL: process.env.NEXT_PUBLIC_DJANGO_API_URL,
|
|
NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID: process.env.NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID,
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Task 13.8: Error Boundaries & Monitoring (2 hours)
|
|
|
|
### Global Error Handling
|
|
|
|
```typescript
|
|
// app/error.tsx
|
|
'use client';
|
|
|
|
export default function Error({
|
|
error,
|
|
reset,
|
|
}: {
|
|
error: Error & { digest?: string };
|
|
reset: () => void;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<h2>Something went wrong!</h2>
|
|
<button onClick={reset}>Try again</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// app/global-error.tsx (catches errors in root layout)
|
|
'use client';
|
|
|
|
export default function GlobalError({
|
|
error,
|
|
reset,
|
|
}: {
|
|
error: Error & { digest?: string };
|
|
reset: () => void;
|
|
}) {
|
|
return (
|
|
<html>
|
|
<body>
|
|
<h2>Something went wrong!</h2>
|
|
<button onClick={reset}>Try again</button>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Not Found Pages
|
|
|
|
```typescript
|
|
// app/not-found.tsx
|
|
export default function NotFound() {
|
|
return (
|
|
<div>
|
|
<h2>404 - Page Not Found</h2>
|
|
<p>The page you're looking for doesn't exist.</p>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Checklist
|
|
- [ ] Error boundaries for each route segment
|
|
- [ ] Global error handler
|
|
- [ ] 404 page styled
|
|
- [ ] Error logging implemented
|
|
- [ ] User-friendly error messages
|
|
- [ ] Errors don't expose sensitive data
|
|
|
|
---
|
|
|
|
## 🎯 Phase Completion Criteria
|
|
|
|
### Configuration
|
|
- [ ] Turbopack fully configured
|
|
- [ ] Image optimization working
|
|
- [ ] Environment variables validated
|
|
- [ ] next.config.js optimized
|
|
|
|
### Performance
|
|
- [ ] Bundle size optimized
|
|
- [ ] Code splitting implemented
|
|
- [ ] Images optimized
|
|
- [ ] Fonts optimized
|
|
- [ ] Core Web Vitals meet targets
|
|
|
|
### SEO
|
|
- [ ] All pages have metadata
|
|
- [ ] OpenGraph tags present
|
|
- [ ] Sitemap generated
|
|
- [ ] Robots.txt configured
|
|
|
|
### User Experience
|
|
- [ ] Loading states implemented
|
|
- [ ] Error boundaries working
|
|
- [ ] 404 pages styled
|
|
- [ ] No jarring layout shifts
|
|
|
|
### Code Quality
|
|
- [ ] Server/Client components optimized
|
|
- [ ] No unnecessary 'use client' directives
|
|
- [ ] Parallel data fetching where possible
|
|
- [ ] Proper caching strategies
|
|
|
|
---
|
|
|
|
## 📊 Progress Tracking
|
|
|
|
**Started:** [Date]
|
|
**Completed:** [Date]
|
|
**Time Spent:** [Hours]
|
|
|
|
### Tasks Completed
|
|
- [ ] Task 13.1: Turbopack Configuration
|
|
- [ ] Task 13.2: Server vs Client Components
|
|
- [ ] Task 13.3: Data Fetching Optimization
|
|
- [ ] Task 13.4: Loading States & Suspense
|
|
- [ ] Task 13.5: Metadata API for SEO
|
|
- [ ] Task 13.6: Performance Optimization
|
|
- [ ] Task 13.7: Environment Variables
|
|
- [ ] Task 13.8: Error Boundaries
|
|
|
|
---
|
|
|
|
## ⏭️ Next Phase
|
|
|
|
Once this phase is complete, proceed to [Phase 14: Cleanup & Testing](./PHASE_14_CLEANUP_TESTING.md)
|
|
|
|
---
|
|
|
|
## 🔗 Related Documentation
|
|
|
|
- [Next.js 15 Migration Guide](./NEXTJS_15_MIGRATION_GUIDE.md)
|
|
- [Environment Variables Guide](./ENVIRONMENT_VARIABLES.md)
|
|
- [Bun Setup Guide](./BUN_GUIDE.md)
|
|
- Next.js Documentation: https://nextjs.org/docs
|
|
|
|
---
|
|
|
|
**Document Version:** 1.0
|
|
**Last Updated:** November 9, 2025
|