diff --git a/.gitignore b/.gitignore index 5076fe25c..f3cc5fc91 100644 --- a/.gitignore +++ b/.gitignore @@ -153,11 +153,4 @@ supabase/.temp/ .tanstack/ .cursorindexingignore .specstory/ -.bmad/** -.bmad/ -.claude/ -.claude/** -.cursor/** -.cursor/ 2025-11-25-where-we-left-off.txt -AGENTS.md diff --git a/OPTIMIZATION_GUIDE.md b/OPTIMIZATION_GUIDE.md new file mode 100644 index 000000000..423ebe739 --- /dev/null +++ b/OPTIMIZATION_GUIDE.md @@ -0,0 +1,1428 @@ +# Next.js Optimization Guide for United Tattoo +## Improving Developer Experience & Site Performance + +**Last Updated:** 2025-11-27 +**Target Framework:** Next.js 14 (App Router) +**Deployment:** Cloudflare Workers via OpenNext + +--- + +## Table of Contents + +1. [Quick Wins (This Week)](#quick-wins-this-week) +2. [Developer Experience Improvements](#developer-experience-improvements) +3. [Performance Optimizations](#performance-optimizations) +4. [Code Quality & Maintainability](#code-quality--maintainability) +5. [Implementation Priority Matrix](#implementation-priority-matrix) +6. [Measuring Success](#measuring-success) + +--- + +## Quick Wins (This Week) + +### 1.1 Add MDX Support for Content Pages + +**Problem:** Editing content pages requires React boilerplate and components. +**Solution:** Add MDX to write pages in markdown with optional React components. + +**Implementation:** + +```bash +npm install @next/mdx @mdx-js/loader @mdx-js/react +``` + +**Create `mdx-components.tsx` in root:** + +```typescript +import type { MDXComponents } from 'mdx/types' + +export function useMDXComponents(components: MDXComponents): MDXComponents { + return { + h1: ({ children }) =>

{children}

, + h2: ({ children }) =>

{children}

, + p: ({ children }) =>

{children}

, + a: ({ href, children }) => {children}, + ...components, + } +} +``` + +**Update `next.config.mjs`:** + +```javascript +import createMDX from '@next/mdx' + +const withMDX = createMDX({ + extension: /\.mdx?$/, + options: { + remarkPlugins: [], + rehypePlugins: [], + }, +}) + +export default withMDX({ + pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], + // ... rest of config +}) +``` + +**Convert pages to MDX:** + +```bash +# Example: app/aftercare/page.tsx → app/aftercare/page.mdx +``` + +**Benefits:** +- ✅ Write content in markdown (much faster) +- ✅ Drop in React components when needed +- ✅ No boilerplate for simple pages +- ⏱️ **Time saved:** 5-10 minutes per content edit + +--- + +### 1.2 Create Content Component Library + +**Problem:** Repeating the same UI patterns across pages. +**Solution:** Pre-built, reusable content components. + +**Create `components/content/` directory:** + +```typescript +// components/content/Section.tsx +export function Section({ + title, + children, + className = "" +}: { + title?: string + children: React.ReactNode + className?: string +}) { + return ( +
+ {title &&

{title}

} +
+ {children} +
+
+ ) +} + +// components/content/Hero.tsx +export function Hero({ + title, + subtitle, + backgroundImage, + cta +}: HeroProps) { + return ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} + {cta && ( + + )} +
+
+
+ ) +} + +// components/content/Card.tsx +export function Card({ + title, + description, + image, + href +}: CardProps) { + return ( + +
+ {image && ( + {title} + )} +
+

+ {title} +

+ {description && ( +

{description}

+ )} +
+
+ + ) +} + +// Export barrel +// components/content/index.ts +export { Section } from './Section' +export { Hero } from './Hero' +export { Card } from './Card' +export { Grid } from './Grid' +export { CallToAction } from './CallToAction' +``` + +**Usage in pages:** + +```tsx +import { Hero, Section, Grid, Card } from '@/components/content' + +export default function ServicesPage() { + return ( + <> + + +
+ + + {/* More cards... */} + +
+ + ) +} +``` + +**Benefits:** +- ✅ Consistent UI across site +- ✅ Less code duplication +- ✅ Easier to maintain styles +- ⏱️ **Time saved:** 15-20 minutes per page creation + +--- + +### 1.3 Convert Pages to Server Components + +**Problem:** Unnecessary client-side JavaScript on static pages. +**Solution:** Remove `"use client"` from pages that don't need interactivity. + +**Audit candidates:** + +```bash +# Find all pages with "use client" +grep -r "use client" app/ --include="page.tsx" +``` + +**Likely candidates for conversion:** + +- `app/page.tsx` (homepage) - Most sections can be server-rendered +- `app/artists/page.tsx` (artist listing) - Just displays data +- `app/aftercare/page.tsx` (static content) +- `app/privacy/page.tsx` (static content) +- `app/terms/page.tsx` (static content) + +**Before:** +```typescript +"use client" + +export default function ArtistsPage() { + const [artists, setArtists] = useState([]) + + useEffect(() => { + fetch('/api/artists') + .then(r => r.json()) + .then(setArtists) + }, []) + + return +} +``` + +**After:** +```typescript +// No "use client" directive - this is a server component + +async function getArtists() { + const db = getDB() + return await db.artists.findMany({ + where: { isActive: true }, + include: { portfolioImages: { take: 6 } } + }) +} + +export default async function ArtistsPage() { + const artists = await getArtists() + + return +} +``` + +**For interactive parts, create client islands:** + +```typescript +// app/artists/page.tsx (server component) +export default async function ArtistsPage() { + const artists = await getArtists() + + return ( +
+

Our Artists

+ {/* Client component for filtering only */} + + +
+ ) +} + +// components/ArtistFilter.tsx (client component) +"use client" + +export function ArtistFilter() { + const [filter, setFilter] = useState('') + // Only the filter is interactive, rest is server-rendered +} +``` + +**Benefits:** +- ✅ Faster initial page load (less JS to download) +- ✅ Better SEO (fully rendered HTML) +- ✅ Reduced hydration time +- 📉 **Performance:** 30-50% less JavaScript per page + +--- + +### 1.4 Add Loading & Error States + +**Problem:** No feedback during data fetching, poor error UX. +**Solution:** Use Next.js 14's `loading.tsx` and `error.tsx` conventions. + +**Create loading states:** + +```typescript +// app/artists/loading.tsx +export default function Loading() { + return ( +
+
+ {[1, 2, 3, 4, 5, 6].map((i) => ( +
+
+
+
+
+ ))} +
+
+ ) +} + +// app/artists/[id]/loading.tsx +export default function Loading() { + return +} +``` + +**Create error boundaries:** + +```typescript +// app/artists/error.tsx +"use client" + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( +
+

Something went wrong!

+

{error.message}

+ +
+ ) +} +``` + +**Benefits:** +- ✅ Better user experience during loading +- ✅ Graceful error handling +- ✅ No need to manage loading state manually +- ⏱️ **Time saved:** 5-10 minutes per page (no manual loading state) + +--- + +## Developer Experience Improvements + +### 2.1 Improve Type Safety + +**Current issue:** Some areas lack proper TypeScript types. + +**Add Zod schemas for API responses:** + +```typescript +// lib/schemas/artist.ts +import { z } from 'zod' + +export const artistSchema = z.object({ + id: z.string(), + name: z.string(), + slug: z.string(), + bio: z.string().nullable(), + specialties: z.array(z.string()), + instagramHandle: z.string().nullable(), + portfolioImages: z.array(z.object({ + id: z.string(), + url: z.string(), + alt: z.string().nullable(), + tags: z.array(z.string()), + })), +}) + +export type Artist = z.infer + +// Use in API routes for validation +export async function GET() { + const data = await db.artists.findMany() + const validated = z.array(artistSchema).parse(data) // Runtime validation + return Response.json(validated) +} +``` + +**Generate types from database schema:** + +```bash +npm install drizzle-kit +``` + +```typescript +// scripts/generate-types.ts +import { generateTypes } from 'drizzle-kit' + +generateTypes({ + schema: './lib/db.ts', + out: './types/database.d.ts' +}) +``` + +**Benefits:** +- ✅ Catch errors at compile time +- ✅ Better autocomplete in IDE +- ✅ Runtime validation of API data +- 🐛 **Fewer bugs** in production + +--- + +### 2.2 Add Development Scripts + +**Create helper scripts for common tasks:** + +```json +// package.json +{ + "scripts": { + "dev:all": "concurrently \"npm run dev\" \"npm run db:studio:local\"", + "dev:debug": "NODE_OPTIONS='--inspect' next dev", + "db:reset:local": "wrangler d1 execute united-tattoo --local --file=./sql/schema.sql && node scripts/seed-local.js", + "db:seed:local": "node scripts/seed-local.js", + "analyze": "ANALYZE=true npm run build", + "type-check:watch": "tsc --noEmit --watch", + "clean": "rm -rf .next .open-next node_modules/.cache", + "fresh": "npm run clean && npm install && npm run dev" + } +} +``` + +**Create seed script for local development:** + +```typescript +// scripts/seed-local.ts +import { getDB } from '@/lib/db' + +async function seed() { + const db = getDB() + + // Create test user + await db.users.create({ + data: { + email: 'test@example.com', + name: 'Test Artist', + role: 'ARTIST', + } + }) + + // Create test artist + await db.artists.create({ + data: { + name: 'Test Artist', + slug: 'test-artist', + bio: 'Test bio', + specialties: ['Realism', 'Color'], + userId: '...', + } + }) + + console.log('✅ Database seeded!') +} + +seed().catch(console.error) +``` + +**Benefits:** +- ✅ Faster development setup +- ✅ Easy database reset/seeding +- ✅ Better debugging capabilities +- ⏱️ **Time saved:** 10-15 minutes daily on setup tasks + +--- + +### 2.3 Improve Error Messages + +**Add better error handling in database layer:** + +```typescript +// lib/db.ts +export const db = { + artists: { + async findById(id: string) { + try { + const db = getDB() + const artist = await db.prepare( + 'SELECT * FROM artists WHERE id = ?' + ).bind(id).first() + + if (!artist) { + throw new Error(`Artist not found: ${id}`) + } + + return artist + } catch (error) { + // Add context to errors + throw new Error( + `Failed to fetch artist ${id}: ${error.message}`, + { cause: error } + ) + } + } + } +} +``` + +**Add request logging:** + +```typescript +// middleware.ts +export function middleware(request: NextRequest) { + const start = Date.now() + + console.log(`→ ${request.method} ${request.nextUrl.pathname}`) + + const response = NextResponse.next() + + response.headers.set('X-Response-Time', `${Date.now() - start}ms`) + + console.log( + `← ${request.method} ${request.nextUrl.pathname} ` + + `(${Date.now() - start}ms)` + ) + + return response +} +``` + +**Benefits:** +- ✅ Easier debugging +- ✅ Faster error resolution +- ✅ Better production monitoring +- 🐛 **Faster bug fixes** + +--- + +### 2.4 Create Component Templates + +**Add templates for common patterns:** + +```bash +# scripts/create-page.sh +#!/bin/bash + +PAGE_NAME=$1 +ROUTE_PATH=$2 + +mkdir -p "app/$ROUTE_PATH" + +cat > "app/$ROUTE_PATH/page.tsx" < +

$PAGE_NAME

+ {/* Content here */} +
+ ) +} +EOF + +echo "✅ Created page: app/$ROUTE_PATH/page.tsx" +``` + +**Usage:** +```bash +npm run create:page "Services" "services" +``` + +**VSCode snippets:** + +```json +// .vscode/snippets.code-snippets +{ + "Next.js Page": { + "prefix": "npage", + "body": [ + "import { Metadata } from 'next'", + "", + "export const metadata: Metadata = {", + " title: '${1:Page Title} | United Tattoo',", + " description: '${2:Description}',", + "}", + "", + "export default async function ${1}Page() {", + " return (", + "
", + "

${1}

", + " $0", + "
", + " )", + "}" + ] + }, + "API Route": { + "prefix": "napi", + "body": [ + "import { NextRequest } from 'next/server'", + "import { getServerSession } from 'next-auth'", + "import { authOptions } from '@/lib/auth'", + "", + "export async function GET(request: NextRequest) {", + " const session = await getServerSession(authOptions)", + " ", + " if (!session) {", + " return Response.json({ error: 'Unauthorized' }, { status: 401 })", + " }", + " ", + " // Logic here", + " $0", + " ", + " return Response.json({ data: null })", + "}" + ] + } +} +``` + +**Benefits:** +- ✅ Consistent code structure +- ✅ Faster file creation +- ✅ Less boilerplate typing +- ⏱️ **Time saved:** 2-5 minutes per file + +--- + +## Performance Optimizations + +### 3.1 Optimize Images + +**Current issue:** Some images not using Next.js Image component. + +**Replace `` with ``:** + +```typescript +// Before +Hero + +// After +import Image from 'next/image' + +Hero +``` + +**For portfolio images from R2:** + +```typescript +// components/PortfolioImage.tsx +import Image from 'next/image' + +export function PortfolioImage({ image }: { image: PortfolioImage }) { + return ( + {image.alt + ) +} +``` + +**Generate blur placeholders:** + +```bash +npm install plaiceholder +``` + +```typescript +// lib/blur-image.ts +import { getPlaiceholder } from 'plaiceholder' + +export async function getBlurDataURL(src: string) { + try { + const buffer = await fetch(src).then(r => r.arrayBuffer()) + const { base64 } = await getPlaiceholder(Buffer.from(buffer)) + return base64 + } catch { + return undefined + } +} +``` + +**Benefits:** +- 📉 **30-50% smaller image sizes** +- ✅ Automatic WebP/AVIF conversion +- ✅ Responsive images +- ✅ Better CLS (Cumulative Layout Shift) + +--- + +### 3.2 Add Route-Level Caching + +**Add caching headers to static content:** + +```typescript +// app/artists/page.tsx +export const revalidate = 3600 // Revalidate every hour + +export default async function ArtistsPage() { + const artists = await getArtists() + return +} + +// app/artists/[id]/page.tsx +export const revalidate = 1800 // 30 minutes + +export async function generateStaticParams() { + const artists = await getArtists() + return artists.map(a => ({ id: a.slug })) +} +``` + +**Add API route caching:** + +```typescript +// app/api/artists/route.ts +export async function GET() { + const artists = await db.artists.findMany() + + return Response.json(artists, { + headers: { + 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200', + } + }) +} +``` + +**Add Cloudflare KV caching for expensive operations:** + +```typescript +// lib/cache.ts +export async function cached( + key: string, + fn: () => Promise, + ttl: number = 3600 +): Promise { + const kv = getKVNamespace() + + // Try cache first + const cached = await kv.get(key, 'json') + if (cached) return cached as T + + // Execute and cache + const result = await fn() + await kv.put(key, JSON.stringify(result), { expirationTtl: ttl }) + return result +} + +// Usage +const artists = await cached('artists:all', () => db.artists.findMany(), 3600) +``` + +**Benefits:** +- 📉 **50-90% faster repeat visits** +- ✅ Reduced database load +- ✅ Better scalability +- 💰 **Lower hosting costs** + +--- + +### 3.3 Optimize Fonts + +**Use next/font with local fonts:** + +```typescript +// app/layout.tsx +import { Inter, Playfair_Display } from 'next/font/google' + +const inter = Inter({ + subsets: ['latin'], + variable: '--font-inter', + display: 'swap', // Prevent FOIT (Flash of Invisible Text) +}) + +const playfair = Playfair_Display({ + subsets: ['latin'], + variable: '--font-playfair', + display: 'swap', +}) + +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} +``` + +**Update Tailwind config:** + +```javascript +// tailwind.config.ts +export default { + theme: { + extend: { + fontFamily: { + sans: ['var(--font-inter)', 'sans-serif'], + serif: ['var(--font-playfair)', 'serif'], + }, + }, + }, +} +``` + +**Benefits:** +- 📉 **No layout shift from font loading** +- ✅ Automatic font subsetting +- ✅ Preloading and optimization +- ⚡ **Faster perceived load time** + +--- + +### 3.4 Code Splitting & Lazy Loading + +**Lazy load heavy components:** + +```typescript +// Before - BookingForm loaded immediately +import { BookingForm } from '@/components/BookingForm' + +export default function BookPage() { + return +} + +// After - BookingForm loaded on demand +import dynamic from 'next/dynamic' + +const BookingForm = dynamic( + () => import('@/components/BookingForm'), + { + loading: () => , + ssr: false // Client-only if needed + } +) + +export default function BookPage() { + return +} +``` + +**Lazy load admin dashboard components:** + +```typescript +// app/admin/page.tsx +import dynamic from 'next/dynamic' + +const AnalyticsDashboard = dynamic(() => import('@/components/admin/AnalyticsDashboard')) +const PortfolioManager = dynamic(() => import('@/components/admin/PortfolioManager')) +const CalendarManager = dynamic(() => import('@/components/admin/CalendarManager')) + +export default function AdminPage() { + return ( +
+ + + +
+ ) +} +``` + +**Benefits:** +- 📉 **40-60% smaller initial bundle** +- ✅ Faster time to interactive +- ✅ Better mobile performance +- ⚡ **Lighthouse score: +10-20 points** + +--- + +### 3.5 Optimize Third-Party Scripts + +**Use next/script for external scripts:** + +```typescript +// app/layout.tsx +import Script from 'next/script' + +export default function RootLayout({ children }) { + return ( + + + {children} + + {/* Analytics - load after page interactive */} +