Building a Multi-City Production Portal with Next.js 14, pnpm Workspaces, and AWS Route53

This session involved scaffolding and deploying rickdrakeproductions.com as a central hub for a multi-city production coordination platform. The architecture supports city-specific pages (San Diego, Las Vegas, with Phoenix, Palm Springs, and LA planned) while maintaining a unified codebase using Next.js 14, TypeScript, Tailwind CSS 4, and pnpm workspaces.

Domain Registration & Infrastructure Setup

The first decision was where to register rickdrakeproductions.com. Rather than using GoDaddy (which required API credential handoffs), we registered directly via AWS Route53:

  • Domain registered with privacy protection enabled to shield the registrant's personal information from WHOIS lookups
  • Route53 was chosen to eliminate cross-provider DNS management — DNS records, domain registration, and CloudFront distribution all live in the same AWS account
  • Existing contact information from other Route53-registered domains (86from.com) was reused to streamline registration
  • The operation completed successfully, with domain propagation monitored via Route53 operation status checks

This approach reduced operational friction: no GoDaddy API tokens to manage, no separate DNS provider to configure, and no nameserver delegation delays.

Project Structure: pnpm Workspace with Monorepo Pattern

The repository structure mirrors a modern Node.js monorepo using pnpm workspaces:

rickdrakeproductions.com/
├── pnpm-workspace.yaml
├── package.json
└── apps/
    └── web/
        ├── next.config.ts
        ├── package.json
        └── src/
            ├── app/
            │   ├── layout.tsx
            │   ├── page.tsx
            │   ├── globals.css
            │   ├── [city]/
            │   │   ├── layout.tsx
            │   │   ├── page.tsx
            │   │   ├── services/page.tsx
            │   │   ├── fleet/page.tsx
            │   │   ├── fleet/[vehicle]/page.tsx
            │   │   ├── contact/page.tsx
            │   │   ├── about/page.tsx
            │   │   ├── locations/page.tsx
            │   │   └── portfolio/page.tsx
            └── components/
                └── layout/
                    ├── Nav.tsx
                    └── Footer.tsx
            └── lib/
                ├── types.ts
                ├── cities.ts
                └── content.ts

Why pnpm? pnpm's strict dependency isolation and disk-efficient deduplication reduce node_modules bloat — critical when working with heavy dependencies like Tailwind CSS 4 (which uses the LightningCSS native binary).

Dynamic Routing with City Parameters

The architecture uses Next.js 14's App Router with dynamic route segments for city-specific pages. The [city] directory creates routes like:

  • /san-diego/
  • /san-diego/services
  • /san-diego/fleet
  • /san-diego/fleet/[vehicle] (for individual vehicle detail pages)
  • /las-vegas/

The cities.ts utility file centralizes city configuration:

// src/lib/cities.ts
export const CITIES = {
  'san-diego': { name: 'San Diego', state: 'CA' },
  'las-vegas': { name: 'Las Vegas', state: 'NV' },
  // Phoenix, Palm Springs, LA to follow
};

This approach allows a single codebase to generate all city pages at build time via static site generation (SSG), which then get cached by CloudFront. The [city] parameter is validated against the CITIES object to prevent arbitrary routes.

Tailwind CSS 4 & LightningCSS Native Binary Challenge

Tailwind CSS 4 introduced a native CSS parser (LightningCSS) for performance. During the initial build, the setup required explicit installation of the platform-specific binary:

  • The LightningCSS package is a Node.js wrapper that loads a platform-specific native binary
  • On macOS, this requires lightningcss-darwin-x64 to be installed
  • pnpm's optional peer dependencies sometimes miss these binaries in CI/offline environments

To resolve, we explicitly installed the native binary:

pnpm add --save-optional lightningcss-darwin-x64

Then configured next.config.ts to ensure the native binary loads correctly. This is a common gotcha when using Tailwind CSS 4 in headless/container environments where the build machine's OS differs from development.

Content Management via Utility Files

Rather than hardcoding content in React components, we created a centralized content.ts file:

// src/lib/content.ts
export const getCityContent = (city: string) => {
  // Returns city-specific content: services, fleet details, contact info
};

This separation enables Rick (or a future content editor) to update city details, service offerings, and vehicle inventory without modifying React components. Down the road, this can evolve into a headless CMS integration (Sanity, Contentful, etc.) without changing the component layer.

Type Safety Across the Stack

A types.ts file defines shared TypeScript interfaces:

  • City: Validates city slug and metadata
  • Service: Defines production services (lighting, grip, coordination, etc.)
  • Vehicle: Represents fleet inventory with specifications and availability

These types are imported into both page components and the content utility, ensuring compile-time validation. If a city detail doesn't match the expected interface, TypeScript catches it before deployment.

Layout Hierarchy & Shared Components

The root layout (app/layout.tsx) wraps all pages with:

  • Global CSS imports (globals.css) for Tailwind reset and custom utilities
  • <Nav /> component for site-wide navigation
  • <Footer /> component with company info and quick links
  • Metadata configuration for SEO (title templates, OG tags, etc.)

The city-level layout (app/[city]/layout.tsx) adds city-specific breadcrumbs and context.

Build Pipeline & Verification

We validated the entire build chain by running:

pnpm --filter=web build

This command:

  • Compiles TypeScript to JavaScript
  • Processes Tailwind CSS classes through LightningCSS
  • Generates static HTML for