Building a Multi-City Production Portal: Next.js 14 Monorepo with Dynamic Route Handling and CloudFront Distribution

This post documents the infrastructure and application architecture for Drake Productions' multi-city web platform, designed to serve individual city sites (San Diego, Las Vegas, Phoenix, Palm Springs, LA) from a single codebase with city-specific content and routing.

What Was Done

  • Registered rickdrakeproductions.com domain via Route53 with privacy protection
  • Scaffolded a pnpm monorepo with pnpm-workspace.yaml to support multiple applications
  • Built a Next.js 14 application with App Router in apps/web using TypeScript and Tailwind CSS 4
  • Implemented dynamic city-based routing with catch-all segments and URL parameters
  • Created type-safe city and content management system using TypeScript interfaces
  • Configured CloudFront distribution with ACM certificate for HTTPS delivery
  • Set up DNS architecture to support both hub domain and city-specific subdomains

Technical Architecture

Monorepo Structure & Dependency Management

The project uses pnpm with workspace configuration for scalability:

rickdrakeproductions.com/
├── pnpm-workspace.yaml          # Defines workspace packages
├── package.json                 # Root configuration with shared dependencies
└── apps/
    └── web/                     # Next.js 14 application
        ├── next.config.ts
        ├── src/
        │   ├── app/
        │   │   ├── layout.tsx            # Root layout with navigation
        │   │   ├── page.tsx              # Hub landing page
        │   │   ├── globals.css           # Global Tailwind styles
        │   │   └── [city]/               # Dynamic city segment
        │   │       ├── layout.tsx        # City-specific layout
        │   │       ├── page.tsx          # City home page
        │   │       ├── 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
        └── package.json

The pnpm-workspace.yaml enables efficient dependency installation across packages, reducing duplication and improving build times.

Dynamic Routing with [city] Catch-All Segment

The core routing pattern uses Next.js 14's dynamic segments with the [city] directory:

// apps/web/src/app/[city]/page.tsx
import { notFound } from 'next/navigation'
import { getCityContent } from '@/lib/content'
import { CITIES } from '@/lib/cities'

export async function generateStaticParams() {
  return CITIES.map(city => ({
    city: city.slug
  }))
}

export default function CityPage({ params }: { params: { city: string } }) {
  const cityContent = getCityContent(params.city)
  if (!cityContent) notFound()
  
  return (
    // City-specific content rendered here
  )
}

This pattern allows /san-diego/, /las-vegas/, and future cities to be generated statically at build time via generateStaticParams().

Type-Safe Content Management

Centralized type definitions in apps/web/src/lib/types.ts

export interface City {
  slug: string
  name: string
  state: string
  description: string
}

export interface FleetVehicle {
  id: string
  name: string
  capacity: number
  citySlug: string
}

The apps/web/src/lib/cities.ts file exports the CITIES array, which drives both routing and navigation generation. Content queries are handled in apps/web/src/lib/content.ts, abstracting data fetching from components.

Infrastructure & Deployment

Domain Registration & DNS

The domain was registered via Route53 (AWS) rather than third-party registrars because:

  • Unified Management: DNS records and domain registration are in the same AWS account
  • Automation: No API handoff required between registrar and DNS provider
  • Cost: Consistent pricing and no domain transfer fees
  • Privacy: WHOIS privacy protection enabled at registration

Route53 hosted zone for rickdrakeproductions.com now accepts DNS records for:

  • rickdrakeproductions.com (A record pointing to CloudFront distribution)
  • www.rickdrakeproductions.com (CNAME or alias to main domain)
  • City subdomains: san-diego.rickdrakeproductions.com, etc. (future expansion)

CloudFront & HTTPS Delivery

CloudFront distribution E2Q4UU71SRNTMB was configured with:

  • Origin: S3 bucket hosting Next.js static exports or EC2/container origin for dynamic rendering
  • Certificate: ACM certificate covering rickdrakeproductions.com and www.rickdrakeproductions.com
  • Cache Behaviors: Static assets cached aggressively; dynamic pages with shorter TTLs
  • Distribution Aliases: Added www subdomain to DNS validation and distribution configuration

Namecheap DNS records (from prior domain registrations) were evaluated for migration to Route53, but the primary delivery chain now flows through CloudFront with Route53 as authoritative DNS.

Build & Compilation Challenges

Tailwind CSS 4 & Native Dependencies

Tailwind CSS 4 requires lightningcss, which has optional native bindings for performance. On macOS, the build chain needed lightningcss-darwin-x64:

# Install optional native binary for macOS x64
npm install lightningcss-darwin-x64 --save-optional

# Verify installation
ls apps/web/node_modules/.bin/ | grep lightningcss

If the binary is missing, the build falls back to JavaScript implementation, which is slower but functional. This is relevant when running builds in CI/CD pipelines or containers with different architectures.

pnpm Installation Strategy

Given the large dependency tree (Next.js, React, TypeScript, Tailwind),