Specification
5. Frontend

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

RouteLayoutTypeDescription
/PublicServerHomepage — featured products, hero section
/productsPublicServerAll products — URL param filters
/products/[slug]PublicServerProduct detail — preview images, Add to Cart
/invite/[token]AuthServerValidate token, pre-fill email in registration
/loginAuthServerLogin shell — LoginForm is Client Component
/reset-password/[token]AuthServerSet new password
/checkoutProtectedServerOrder summary (server) + Stripe Elements (client)
/checkout/successProtectedServerOrder confirmed — download links shown
/checkout/failedProtectedServerPayment failed — retry prompt
/account/ordersProtectedServerOrder history — downloads + physical tracking
/account/orders/[id]ProtectedServerOrder detail — download/tracking buttons
/account/settingsProtectedServerUpdate profile, change password

5.2 Folder Structure

    • Root layout — fonts, providers, CartDrawer
  • Auth guards — redirect to /login
  • 5.3 State Management

    ToolManagesRationale
    ZustandCart drawer open/closed, item count, customer sessionLightweight, no boilerplate, perfect for UI state
    TanStack QueryProducts, orders, downloads from Medusa APIStale-while-revalidate, isRevalidating for skeleton UI
    Medusa JS SDKCart CRUD, cart ID persistence via cookieOfficial SDK handles cart state
    URL paramsProduct filters — triggers fresh server refetchShareable, 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 refetch

    Rule: 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 / ResourceCache PolicyInvalidation
    /products, /products/[slug]public, max-age=300, stale-while-revalidate=60revalidatePath() on product update
    /account/*, /checkout/*private, no-storeAlways fresh — auth required
    Preview imagespublic, max-age=31536000, immutableContent hash in URL
    Presigned download URLsno-store15 min validity — never cached
    Static assetspublic, max-age=31536000, immutableNext.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 product
    • app/sitemap.ts — auto-generated, includes all product slugs
    • app/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