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-x64to 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 metadataService: 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