```html

Building a Multi-City Production Portal with Next.js 14: Domain Registration, Monorepo Setup, and Dynamic Routing

We recently built the foundational infrastructure for rickdrakeproductions.com, a multi-city web platform serving a production coordination company operating across San Diego, Las Vegas, with Phoenix, Palm Springs, and LA planned for expansion. This post details the technical decisions, infrastructure choices, and implementation patterns that enable scalable city-specific content delivery from a single codebase.

What Was Done

  • Registered rickdrakeproductions.com via AWS Route53 with privacy protection
  • Architected a pnpm monorepo workspace with centralized dependency management
  • Built a Next.js 14 application using the App Router with dynamic city-based routing
  • Created reusable city-specific page templates supporting multiple route patterns
  • Configured TypeScript with strict type safety for content and city data structures
  • Implemented a static content layer for city-specific data without database complexity

Domain Registration and DNS Infrastructure

Rather than using traditional registrars, we chose AWS Route53 for domain registration. This decision eliminates friction during deployment—DNS records, zone management, and certificate validation all live in the same AWS ecosystem.

The registration command created a Route53 hosted zone for rickdrakeproductions.com with privacy protection enabled (hiding WHOIS data). Route53 automatically generated nameservers that DNS queries resolve through AWS's global network, providing sub-100ms latency regardless of geographic origin.

Why Route53 instead of GoDaddy or Namecheap? We have existing infrastructure (CloudFront distributions, ACM certificates) tied to AWS accounts. Keeping the domain there means no separate credential management, no registrar API keys to rotate, and DNS changes propagate to CloudFront origin configurations instantly.

Monorepo Architecture with pnpm Workspaces

We structured the project as a pnpm monorepo to support multiple applications under one domain. The workspace configuration lives in /pnpm-workspace.yaml:

packages:
  - "apps/*"
  - "packages/*"

This allows the Next.js application in apps/web to coexist with potential future services (API routes, serverless functions, shared utilities) while maintaining a single lockfile. When dependencies update, all workspaces use the same versions—preventing subtle bugs from version mismatches between packages.

The root package.json defines workspace-level scripts and shared dev dependencies (TypeScript, ESLint, Tailwind CSS 4). Individual applications like apps/web specify only their specific dependencies, keeping node_modules lean.

Next.js 14 App Router with Dynamic City Routing

The application structure uses dynamic route segments to serve city-specific content from a single codebase. Directory layout:

apps/web/src/app/
├── layout.tsx          # Root layout (nav, footer, global styles)
├── page.tsx            # Homepage
├── [city]/             # Dynamic city segment
│   ├── layout.tsx      # City layout wrapper
│   ├── page.tsx        # City homepage (e.g., /san-diego/)
│   ├── services/
│   │   └── page.tsx    # /[city]/services
│   ├── fleet/
│   │   ├── page.tsx    # /[city]/fleet
│   │   └── [vehicle]/
│   │       └── page.tsx # /[city]/fleet/[vehicle]
│   ├── contact/
│   │   └── page.tsx
│   ├── about/
│   │   └── page.tsx
│   ├── locations/
│   │   └── page.tsx
│   └── portfolio/
│       └── page.tsx

The [city] segment accepts URL parameters like "san-diego" and "las-vegas", routing to /apps/web/src/app/[city]/page.tsx. This single component renders different content based on the city parameter without code duplication.

City and Content Data Structures

Rather than querying a database, we defined TypeScript types and static data files:

/apps/web/src/lib/types.ts defines the shape of city and service data:

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

export interface Service {
  id: string;
  city: string;
  title: string;
  description: string;
}

/apps/web/src/lib/cities.ts exports the list of valid cities:

export const cities: City[] = [
  { slug: 'san-diego', name: 'San Diego', state: 'CA', description: '...' },
  { slug: 'las-vegas', name: 'Las Vegas', state: 'NV', description: '...' },
];

/apps/web/src/lib/content.ts provides helper functions to fetch city-specific content. Because we're not querying a database, this layer abstracts the data source—if we migrate to Contentful, Sanity, or a traditional database later, only this file changes.

Why static data instead of a headless CMS? The initial dataset is small (2 cities, ~50 pages). Static data means no runtime database queries, instant page loads, and predictable performance. As the platform grows, migrating to a headless CMS is straightforward—we only modify the content layer, not page components.

TypeScript and Build Configuration

We configured apps/web/next.config.ts with strict TypeScript checking enabled. The build pipeline validates all type definitions before generating optimized JavaScript, catching errors at build time rather than runtime.

Tailwind CSS 4 requires the native lightningcss binary for performance. We installed the platform-specific binary (lightningcss-darwin-x64 for macOS development) to handle CSS compilation during builds and dev server startup.

Global Styles and Layout Structure

/apps/web/src/app/globals.css defines Tailwind directives and custom CSS variables for theme colors, spacing, and typography. This single stylesheet applies to all routes—city-specific styling extends rather than overrides these globals.

The root layout.tsx wraps all pages with:

  • Navigation component (src/components/layout/Nav.tsx): City-aware links showing current city and cities dropdown
  • Footer component (src/components/layout/Footer.tsx): Consistent footer across all routes
  • Metadata export: Sets page title, meta description, and Open Graph tags for SEO

The city layout ([city]/layout.tsx) validates the city parameter against the known cities list, returning a 404 if the city slug is invalid. This prevents crawlers from generating pages for typos or non-existent cities.

Key Architectural Decisions

  • Dynamic segments over subdomain routing: We chose rickdrakeproductions.com/san-diego/ over sandiego.rickdrakeproductions.com for SEO benefits. Google treats single domains more favorably than subdomains for ranking, and internal linking boosts signals across the entire domain authority