# 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 */}