united-tattoo/OPTIMIZATION_GUIDE.md
2025-11-27 04:28:40 -07:00

32 KiB

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)
  2. Developer Experience Improvements
  3. Performance Optimizations
  4. Code Quality & Maintainability
  5. Implementation Priority Matrix
  6. 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:

npm install @next/mdx @mdx-js/loader @mdx-js/react

Create mdx-components.tsx in root:

import type { MDXComponents } from 'mdx/types'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h1: ({ children }) => <h1 className="text-4xl font-bold mb-4">{children}</h1>,
    h2: ({ children }) => <h2 className="text-3xl font-semibold mb-3">{children}</h2>,
    p: ({ children }) => <p className="mb-4 leading-relaxed">{children}</p>,
    a: ({ href, children }) => <a href={href} className="text-blue-600 hover:underline">{children}</a>,
    ...components,
  }
}

Update next.config.mjs:

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:

# 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:

// components/content/Section.tsx
export function Section({
  title,
  children,
  className = ""
}: {
  title?: string
  children: React.ReactNode
  className?: string
}) {
  return (
    <section className={`py-12 ${className}`}>
      {title && <h2 className="text-3xl font-bold mb-6">{title}</h2>}
      <div className="prose prose-lg max-w-none">
        {children}
      </div>
    </section>
  )
}

// components/content/Hero.tsx
export function Hero({
  title,
  subtitle,
  backgroundImage,
  cta
}: HeroProps) {
  return (
    <div
      className="relative h-[600px] flex items-center justify-center"
      style={{ backgroundImage: `url(${backgroundImage})` }}
    >
      <div className="text-center text-white z-10">
        <h1 className="text-6xl font-bold mb-4">{title}</h1>
        {subtitle && <p className="text-xl mb-8">{subtitle}</p>}
        {cta && (
          <Button href={cta.href} size="lg">
            {cta.text}
          </Button>
        )}
      </div>
      <div className="absolute inset-0 bg-black/40" />
    </div>
  )
}

// components/content/Card.tsx
export function Card({
  title,
  description,
  image,
  href
}: CardProps) {
  return (
    <Link href={href} className="block group">
      <div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
        {image && (
          <img
            src={image}
            alt={title}
            className="w-full h-48 object-cover"
          />
        )}
        <div className="p-6">
          <h3 className="text-xl font-semibold mb-2 group-hover:text-blue-600">
            {title}
          </h3>
          {description && (
            <p className="text-gray-600">{description}</p>
          )}
        </div>
      </div>
    </Link>
  )
}

// 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:

import { Hero, Section, Grid, Card } from '@/components/content'

export default function ServicesPage() {
  return (
    <>
      <Hero
        title="Our Services"
        subtitle="Professional tattoo artistry"
        backgroundImage="/images/hero-services.jpg"
      />

      <Section title="What We Offer">
        <Grid columns={3}>
          <Card
            title="Custom Tattoos"
            description="Work with our artists to create your dream piece"
            image="/images/custom.jpg"
            href="/book"
          />
          {/* More cards... */}
        </Grid>
      </Section>
    </>
  )
}

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:

# 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:

"use client"

export default function ArtistsPage() {
  const [artists, setArtists] = useState([])

  useEffect(() => {
    fetch('/api/artists')
      .then(r => r.json())
      .then(setArtists)
  }, [])

  return <ArtistGrid artists={artists} />
}

After:

// 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 <ArtistGrid artists={artists} />
}

For interactive parts, create client islands:

// app/artists/page.tsx (server component)
export default async function ArtistsPage() {
  const artists = await getArtists()

  return (
    <div>
      <h1>Our Artists</h1>
      {/* Client component for filtering only */}
      <ArtistFilter />
      <ArtistGrid artists={artists} />
    </div>
  )
}

// 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:

// app/artists/loading.tsx
export default function Loading() {
  return (
    <div className="container py-12">
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {[1, 2, 3, 4, 5, 6].map((i) => (
          <div key={i} className="animate-pulse">
            <div className="bg-gray-200 h-64 rounded-lg mb-4" />
            <div className="bg-gray-200 h-6 w-3/4 rounded mb-2" />
            <div className="bg-gray-200 h-4 w-1/2 rounded" />
          </div>
        ))}
      </div>
    </div>
  )
}

// app/artists/[id]/loading.tsx
export default function Loading() {
  return <PortfolioSkeleton />
}

Create error boundaries:

// app/artists/error.tsx
"use client"

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="container py-12 text-center">
      <h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
      <p className="text-gray-600 mb-6">{error.message}</p>
      <button
        onClick={reset}
        className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
      >
        Try again
      </button>
    </div>
  )
}

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:

// 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<typeof artistSchema>

// 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:

npm install drizzle-kit
// 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:

// 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:

// 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:

// 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:

// 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:

# scripts/create-page.sh
#!/bin/bash

PAGE_NAME=$1
ROUTE_PATH=$2

mkdir -p "app/$ROUTE_PATH"

cat > "app/$ROUTE_PATH/page.tsx" <<EOF
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: '$PAGE_NAME | United Tattoo',
  description: 'Description here',
}

export default async function ${PAGE_NAME}Page() {
  return (
    <div className="container py-12">
      <h1 className="text-4xl font-bold mb-6">$PAGE_NAME</h1>
      {/* Content here */}
    </div>
  )
}
EOF

echo "✅ Created page: app/$ROUTE_PATH/page.tsx"

Usage:

npm run create:page "Services" "services"

VSCode snippets:

// .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 (",
      "    <div className=\"container py-12\">",
      "      <h1 className=\"text-4xl font-bold mb-6\">${1}</h1>",
      "      $0",
      "    </div>",
      "  )",
      "}"
    ]
  },
  "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 <img> with <Image>:

// Before
<img src="/images/hero.jpg" alt="Hero" />

// After
import Image from 'next/image'

<Image
  src="/images/hero.jpg"
  alt="Hero"
  width={1920}
  height={1080}
  priority // For above-the-fold images
  placeholder="blur"
  blurDataURL="data:image/..." // Optional blur-up effect
/>

For portfolio images from R2:

// components/PortfolioImage.tsx
import Image from 'next/image'

export function PortfolioImage({ image }: { image: PortfolioImage }) {
  return (
    <Image
      src={image.url}
      alt={image.alt || image.title}
      width={800}
      height={600}
      className="rounded-lg shadow-md"
      loading="lazy" // Lazy load below fold
      quality={85} // Reduce quality slightly for smaller files
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
  )
}

Generate blur placeholders:

npm install plaiceholder
// 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:

// app/artists/page.tsx
export const revalidate = 3600 // Revalidate every hour

export default async function ArtistsPage() {
  const artists = await getArtists()
  return <ArtistGrid artists={artists} />
}

// 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:

// 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:

// lib/cache.ts
export async function cached<T>(
  key: string,
  fn: () => Promise<T>,
  ttl: number = 3600
): Promise<T> {
  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:

// 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 (
    <html className={`${inter.variable} ${playfair.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  )
}

Update Tailwind config:

// 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:

// Before - BookingForm loaded immediately
import { BookingForm } from '@/components/BookingForm'

export default function BookPage() {
  return <BookingForm />
}

// After - BookingForm loaded on demand
import dynamic from 'next/dynamic'

const BookingForm = dynamic(
  () => import('@/components/BookingForm'),
  {
    loading: () => <LoadingSpinner />,
    ssr: false // Client-only if needed
  }
)

export default function BookPage() {
  return <BookingForm />
}

Lazy load admin dashboard components:

// 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 (
    <div>
      <AnalyticsDashboard />
      <PortfolioManager />
      <CalendarManager />
    </div>
  )
}

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:

// app/layout.tsx
import Script from 'next/script'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}

        {/* Analytics - load after page interactive */}
        <Script
          src="https://analytics.example.com/script.js"
          strategy="afterInteractive"
        />

        {/* Non-critical - load lazily */}
        <Script
          src="https://widget.example.com/chat.js"
          strategy="lazyOnload"
        />
      </body>
    </html>
  )
}

Defer non-essential features:

// components/ChatWidget.tsx
"use client"

import { useEffect, useState } from 'react'

export function ChatWidget() {
  const [loaded, setLoaded] = useState(false)

  useEffect(() => {
    // Load chat widget after 5 seconds or user interaction
    const timer = setTimeout(() => setLoaded(true), 5000)

    const loadOnInteraction = () => {
      setLoaded(true)
      clearTimeout(timer)
    }

    window.addEventListener('scroll', loadOnInteraction, { once: true })
    window.addEventListener('click', loadOnInteraction, { once: true })

    return () => clearTimeout(timer)
  }, [])

  if (!loaded) return null

  return <div id="chat-widget">{/* Widget here */}</div>
}

Benefits:

  • 📉 Faster initial page load
  • Better Core Web Vitals
  • Non-blocking script loading
  • Improved FCP and TTI

3.6 Database Query Optimization

Add indexes to frequently queried columns:

-- sql/migrations/YYYYMMDD_0001_add_indexes.sql

-- Speed up artist lookups by slug
CREATE INDEX IF NOT EXISTS idx_artists_slug ON artists(slug);

-- Speed up portfolio queries
CREATE INDEX IF NOT EXISTS idx_portfolio_artist_id ON portfolio_images(artist_id);
CREATE INDEX IF NOT EXISTS idx_portfolio_sort ON portfolio_images(artist_id, sort_order);

-- Speed up appointment queries
CREATE INDEX IF NOT EXISTS idx_appointments_artist_date ON appointments(artist_id, appointment_date);
CREATE INDEX IF NOT EXISTS idx_appointments_status ON appointments(status);

-- Speed up user lookups
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

Optimize queries with JOINs:

// Before - Multiple queries (N+1 problem)
const artists = await db.artists.findMany()
for (const artist of artists) {
  artist.images = await db.portfolioImages.findMany({
    where: { artistId: artist.id }
  })
}

// After - Single query with JOIN
const artists = await db.prepare(`
  SELECT
    a.*,
    json_group_array(
      json_object(
        'id', pi.id,
        'url', pi.url,
        'alt', pi.alt
      )
    ) as images
  FROM artists a
  LEFT JOIN portfolio_images pi ON pi.artist_id = a.id
  GROUP BY a.id
`).all()

Add prepared statement caching:

// lib/db.ts
const statementCache = new Map()

export function getCachedStatement(sql: string) {
  if (!statementCache.has(sql)) {
    const db = getDB()
    statementCache.set(sql, db.prepare(sql))
  }
  return statementCache.get(sql)
}

// Usage
const stmt = getCachedStatement('SELECT * FROM artists WHERE slug = ?')
const artist = await stmt.bind(slug).first()

Benefits:

  • 📉 50-80% faster queries
  • Reduced database load
  • Better scalability
  • Faster API responses

3.7 Optimize CSS

Use Tailwind's JIT mode (already enabled but optimize):

// tailwind.config.ts
export default {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    // Don't include node_modules unless needed
  ],
  theme: {
    extend: {
      // Only include custom values you actually use
    },
  },
  plugins: [],
}

Extract critical CSS for above-the-fold:

npm install critical
// next.config.mjs
export default {
  experimental: {
    optimizeCss: true, // Enable CSS optimization
  },
}

Remove unused ShadCN components:

# Audit which components are actually used
grep -r "from '@/components/ui'" app/ components/ --no-filename | \
  sed 's/.*from//' | sort | uniq

Benefits:

  • 📉 20-40% smaller CSS bundle
  • Faster first paint
  • Better mobile performance
  • Improved FCP

Code Quality & Maintainability

4.1 Consistent File Structure

Establish clear conventions:

app/
├── (marketing)/          # Public pages
│   ├── page.tsx         # Homepage
│   ├── artists/         # Artist pages
│   ├── book/           # Booking
│   └── [slug]/         # Dynamic content pages
├── (dashboard)/         # Protected pages
│   ├── admin/          # Admin dashboard
│   └── artist-dashboard/ # Artist dashboard
└── api/                # API routes
    ├── artists/
    ├── appointments/
    └── admin/

components/
├── ui/                 # ShadCN components
├── content/            # Content components (NEW)
├── admin/             # Admin-specific
└── shared/            # Reusable components

lib/
├── db.ts              # Database layer
├── auth.ts            # Authentication
├── utils/             # Utilities
│   ├── date.ts
│   ├── format.ts
│   └── validation.ts
└── hooks/             # Custom React hooks

Benefits:

  • Easier to find files
  • Clear separation of concerns
  • Onboarding new developers faster

4.2 Add Documentation

Create component documentation:

// components/content/Hero.tsx

/**
 * Hero section component for top of pages
 *
 * @example
 * ```tsx
 * <Hero
 *   title="Welcome"
 *   subtitle="Your journey starts here"
 *   backgroundImage="/images/hero.jpg"
 *   cta={{ text: "Get Started", href: "/book" }}
 * />
 * ```
 */
export function Hero({
  title,
  subtitle,
  backgroundImage,
  cta
}: HeroProps) {
  // Implementation
}

Add README files:

<!-- components/content/README.md -->
# Content Components

Pre-built components for quickly creating marketing pages.

## Available Components

### Hero
Full-width hero section with background image and CTA.

**Props:**
- `title` (string) - Main heading
- `subtitle` (string, optional) - Subheading
- `backgroundImage` (string) - Image URL
- `cta` (object, optional) - Call to action button

**Example:**
...

Benefits:

  • Faster onboarding
  • Less time answering questions
  • Consistent component usage

4.3 Add Storybook (Optional)

For visual component development:

npx storybook@latest init
// components/content/Hero.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Hero } from './Hero'

const meta: Meta<typeof Hero> = {
  title: 'Content/Hero',
  component: Hero,
}

export default meta
type Story = StoryObj<typeof Hero>

export const Default: Story = {
  args: {
    title: 'Welcome to United Tattoo',
    subtitle: 'Professional artistry in Fountain, CO',
    backgroundImage: '/images/hero.jpg',
    cta: {
      text: 'Book Now',
      href: '/book',
    },
  },
}

export const WithoutSubtitle: Story = {
  args: {
    title: 'Our Services',
    backgroundImage: '/images/services.jpg',
  },
}

Benefits:

  • Visual component library
  • Test components in isolation
  • Faster iteration on UI
  • 🎨 Better design system

Implementation Priority Matrix

Priority 1: Quick Wins (Week 1)

High Impact, Low Effort

  • Add MDX support (1-2 hours)
  • Create content component library (3-4 hours)
  • Convert pages to server components (2-3 hours)
  • Add loading/error states (1-2 hours)
  • Optimize images (2-3 hours)

Total: ~12-16 hours Impact: Major DX improvement + 20-30% performance boost


Priority 2: Performance (Week 2-3)

High Impact, Medium Effort

  • Add route-level caching (3-4 hours)
  • Database indexes (1-2 hours)
  • Code splitting & lazy loading (4-5 hours)
  • Optimize fonts (1-2 hours)
  • Optimize third-party scripts (2-3 hours)

Total: ~12-16 hours Impact: 40-60% performance improvement


Priority 3: Code Quality (Week 3-4)

Medium Impact, Low-Medium Effort

  • Improve type safety (4-5 hours)
  • Add development scripts (2-3 hours)
  • Better error messages (3-4 hours)
  • Component templates (2-3 hours)
  • File structure cleanup (3-4 hours)

Total: ~14-19 hours Impact: Better maintainability, fewer bugs


Priority 4: Documentation (Ongoing)

Lower Impact, Low Effort

  • Component documentation (1-2 hours/week)
  • Add README files (1-2 hours)
  • Storybook setup (4-6 hours, optional)

Total: ~6-10 hours Impact: Easier onboarding, consistent usage


Measuring Success

Developer Experience Metrics

Before:

  • ⏱️ Time to create new page: 30-45 minutes
  • ⏱️ Time to add content section: 15-20 minutes
  • 🐛 Time to debug error: 20-30 minutes
  • 📚 Documentation: Minimal

After (Target):

  • ⏱️ Time to create new page: 10-15 minutes (50-60% faster)
  • ⏱️ Time to add content section: 5-10 minutes (50% faster)
  • 🐛 Time to debug error: 10-15 minutes (40% faster)
  • 📚 Documentation: Comprehensive

Performance Metrics

Measure with Lighthouse:

# Install Lighthouse CLI
npm install -g lighthouse

# Run audit
lighthouse https://united-tattoos.com --view

Target improvements:

Metric Current Target Improvement
Performance Score ~75 90+ +15-20
FCP (First Contentful Paint) ~2.5s <1.5s 40% faster
LCP (Largest Contentful Paint) ~4.0s <2.5s 38% faster
TTI (Time to Interactive) ~5.0s <3.0s 40% faster
CLS (Cumulative Layout Shift) ~0.15 <0.1 33% better
Bundle Size (JS) ~800KB <500KB 38% smaller

User Experience Metrics

Track with analytics:

  • ⏱️ Bounce rate: Target <40% (currently ~50%)
  • 📱 Mobile conversion rate: Target +20%
  • ⏱️ Average session duration: Target +30%
  • 🔄 Repeat visitor rate: Target +25%

Next Steps

Week 1: Quick Wins

  1. Monday: Add MDX support + convert 3 static pages
  2. Tuesday: Create content component library
  3. Wednesday: Convert 5 pages to server components
  4. Thursday: Add loading/error states across app
  5. Friday: Optimize all images

Week 2: Performance

  1. Monday: Add route-level caching
  2. Tuesday: Database indexes + query optimization
  3. Wednesday: Code splitting for admin dashboard
  4. Thursday: Lazy load heavy components
  5. Friday: Optimize fonts + third-party scripts

Week 3: Code Quality

  1. Monday: Improve type safety with Zod
  2. Tuesday: Add development scripts
  3. Wednesday: Better error messages + logging
  4. Thursday: Create component templates
  5. Friday: File structure cleanup

Week 4: Documentation & Polish

  1. Monday-Wednesday: Add documentation
  2. Thursday: Storybook setup (optional)
  3. Friday: Measure results, create performance report

Conclusion

By implementing these optimizations, you'll achieve:

Developer Experience:

  • 50-60% faster page creation
  • Much easier content editing with MDX
  • Less boilerplate and complexity
  • Better debugging and error messages

Performance:

  • 📉 40-60% smaller JavaScript bundles
  • 30-40% faster page loads
  • Better Core Web Vitals
  • 💰 Lower hosting costs

Maintainability:

  • Better code organization
  • Comprehensive documentation
  • Fewer bugs in production
  • Easier onboarding

Total Investment: ~40-60 hours over 4 weeks Long-term Savings: ~5-10 hours/week in development time

This positions you well for the future - whether you eventually migrate to Astro or stay with Next.js, you'll have a cleaner, faster, more maintainable codebase.