1429 lines
32 KiB
Markdown
1429 lines
32 KiB
Markdown
# 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 }) => <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`:**
|
||
|
||
```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 (
|
||
<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:**
|
||
|
||
```tsx
|
||
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:**
|
||
|
||
```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 <ArtistGrid artists={artists} />
|
||
}
|
||
```
|
||
|
||
**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 <ArtistGrid artists={artists} />
|
||
}
|
||
```
|
||
|
||
**For interactive parts, create client islands:**
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```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<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:**
|
||
|
||
```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" <<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:**
|
||
```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 (",
|
||
" <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>`:**
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```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 <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:**
|
||
|
||
```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<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:**
|
||
|
||
```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 (
|
||
<html className={`${inter.variable} ${playfair.variable}`}>
|
||
<body className="font-sans">{children}</body>
|
||
</html>
|
||
)
|
||
}
|
||
```
|
||
|
||
**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 <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:**
|
||
|
||
```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 (
|
||
<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:**
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```typescript
|
||
// 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
|
||
-- 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:**
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```typescript
|
||
// 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):**
|
||
|
||
```javascript
|
||
// 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:**
|
||
|
||
```bash
|
||
npm install critical
|
||
```
|
||
|
||
```typescript
|
||
// next.config.mjs
|
||
export default {
|
||
experimental: {
|
||
optimizeCss: true, // Enable CSS optimization
|
||
},
|
||
}
|
||
```
|
||
|
||
**Remove unused ShadCN components:**
|
||
|
||
```bash
|
||
# 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:**
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```markdown
|
||
<!-- 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:**
|
||
|
||
```bash
|
||
npx storybook@latest init
|
||
```
|
||
|
||
```typescript
|
||
// 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:**
|
||
|
||
```bash
|
||
# 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.
|