5. Frontend Architecture
Next.js 14 App Router with Route Groups separating public, auth, and protected layouts. State split between Zustand for UI state and TanStack Query for server data.
5.1 Pages
| Route | Layout | Type | Description |
|---|---|---|---|
/ | Public | Server | Homepage — featured products, hero section |
/products | Public | Server | All products — URL param filters |
/products/[slug] | Public | Server | Product detail — preview images, Add to Cart |
/invite/[token] | Auth | Server | Validate token, pre-fill email in registration |
/login | Auth | Server | Login shell — LoginForm is Client Component |
/reset-password/[token] | Auth | Server | Set new password |
/checkout | Protected | Server | Order summary (server) + Stripe Elements (client) |
/checkout/success | Protected | Server | Order confirmed — download links shown |
/checkout/failed | Protected | Server | Payment failed — retry prompt |
/account/orders | Protected | Server | Order history — downloads + physical tracking |
/account/orders/[id] | Protected | Server | Order detail — download/tracking buttons |
/account/settings | Protected | Server | Update profile, change password |
5.2 Folder Structure
- Root layout — fonts, providers, CartDrawer
5.3 State Management
| Tool | Manages | Rationale |
|---|---|---|
| Zustand | Cart drawer open/closed, item count, customer session | Lightweight, no boilerplate, perfect for UI state |
| TanStack Query | Products, orders, downloads from Medusa API | Stale-while-revalidate, isRevalidating for skeleton UI |
| Medusa JS SDK | Cart CRUD, cart ID persistence via cookie | Official SDK handles cart state |
| URL params | Product filters — triggers fresh server refetch | Shareable, bookmarkable |
5.4 Server vs Client Components
Server Components (no JS sent to browser):
├── ProductGrid, ProductCard
├── OrderHistory, OrderSummary
├── Navbar shell
└── All static page content
Client Components (interactive — 'use client'):
├── AddToCartButton → onClick → Medusa SDK → Zustand
├── CartIcon → reads Zustand cart store
├── CartDrawer → always mounted, reads isDrawerOpen
├── PaymentForm → Stripe Elements
├── DownloadButton → onClick → GET /store/downloads/:token
└── FilterBar → onClick → updates URL → server refetchRule: push 'use client' as deep as possible. Only interactive leaf components need it. A ProductCard is a Server Component — only the AddToCartButton inside it is a Client Component.
💡
Cookies vs localStorage: Cart ID and auth tokens use cookies, not localStorage. Cookies are sent automatically with every request and can be read server-side by Next.js middleware and Server Components. localStorage is browser-only.
5.5 Caching Strategy
| Route / Resource | Cache Policy | Invalidation |
|---|---|---|
/products, /products/[slug] | public, max-age=300, stale-while-revalidate=60 | revalidatePath() on product update |
/account/*, /checkout/* | private, no-store | Always fresh — auth required |
| Preview images | public, max-age=31536000, immutable | Content hash in URL |
| Presigned download URLs | no-store | 15 min validity — never cached |
| Static assets | public, max-age=31536000, immutable | Next.js auto cache-busts with hashes |
5.6 Auth Guard (middleware.ts)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('_medusa_jwt')?.value
const isProtected = request.nextUrl.pathname.startsWith('/account') ||
request.nextUrl.pathname.startsWith('/checkout')
const isAuthPage = request.nextUrl.pathname.startsWith('/login')
// No token + protected route → redirect to login
if (!token && isProtected) {
const url = new URL('/login', request.url)
url.searchParams.set('redirect', request.nextUrl.pathname)
return NextResponse.redirect(url)
}
// Has token + auth page → redirect to account
if (token && isAuthPage) {
return NextResponse.redirect(new URL('/account', request.url))
}
}
export const config = {
matcher: ['/account/:path*', '/checkout/:path*', '/login'],
}5.7 SEO
generateMetadata()for dynamic product pages — unique title, description, OG image per productapp/sitemap.ts— auto-generated, includes all product slugsapp/robots.ts— disallow/account/,/checkout/,/api/- JSON-LD structured data on product pages — price, availability, name for Google
- Australian SEO: AUD pricing stated, Australian English, Sydney server region