Building a Multi-City Next.js Portal with Dynamic Routing and Monorepo Architecture
This session involved architecting and bootstrapping a multi-city web platform for Drake Productions—a production coordination service operating across San Diego, Las Vegas, with Phoenix, Palm Springs, and LA planned for expansion. The challenge: build a scalable hub-and-spoke model that serves city-specific content while maintaining a unified codebase and SEO presence.
What Was Done
- Registered
rickdrakeproductions.comvia AWS Route53 with privacy protection - Established a pnpm monorepo workspace structure at
/apps/web - Scaffolded a Next.js 14 application with TypeScript, Tailwind CSS 4, and the App Router
- Implemented dynamic city-based routing using Next.js catch-all segments
- Created city-specific pages (services, fleet, locations, portfolio, about, contact) with shared components
- Built content management infrastructure with TypeScript type safety
- Resolved Tailwind CSS 4 native binary dependencies for the build pipeline
Domain and Infrastructure Setup
The domain registration decision prioritized operational simplicity. Rather than using GoDaddy's API (which required managing separate DNS configuration), Route53 registration was chosen because it:
- Keeps infrastructure in a single AWS account
- Eliminates nameserver delegation complexity
- Enables automatic CloudFront integration for HTTPS/TLS via ACM certificates
- Supports Route53 health checks and failover if needed for multi-region deployments
The domain was registered with privacy protection enabled, protecting the registrant's personal information while maintaining technical DNS control through AWS.
Monorepo Architecture and Package Management
The workspace structure uses pnpm workspaces to enable code sharing across future satellite applications:
rickdrakeproductions.com/
├── pnpm-workspace.yaml
├── package.json
└── apps/
└── web/
├── package.json
├── next.config.ts
└── src/
├── app/
├── components/
└── lib/
The pnpm-workspace.yaml declares apps/* as workspaces, allowing shared packages (future: shared components, utilities, types) to be referenced across multiple applications without npm/yarn link complexity. This matters for the roadmap—when city-specific microsites or admin panels are added, they can consume shared business logic from a packages/ directory.
Dependency installation proved non-trivial due to optional native dependencies (specifically lightningcss for Tailwind CSS 4). The solution involved explicitly installing the platform-specific binary:
npm install lightningcss-darwin-x64 --save-optional
This ensures the build pipeline doesn't fail on CI/CD runners or developer machines lacking precompiled binaries.
Dynamic Routing and Content Organization
The application uses Next.js 14's dynamic routes to serve city-specific pages from a single codebase. The routing structure:
src/app/
├── page.tsx # Hub landing page
├── layout.tsx # Root layout with Nav/Footer
├── globals.css
├── [city]/
│ ├── layout.tsx # City-specific layout wrapper
│ ├── page.tsx # City homepage
│ ├── services/page.tsx
│ ├── fleet/page.tsx
│ ├── fleet/[vehicle]/page.tsx # Vehicle detail (nested dynamic)
│ ├── contact/page.tsx
│ ├── about/page.tsx
│ ├── locations/page.tsx
│ └── portfolio/page.tsx
The [city] catch-all segment accepts URL parameters like /san-diego/, /las-vegas/, etc. Each page receives the city slug via Next.js params, enabling:
- SEO isolation: Each city gets its own URL namespace with canonical tags
- Analytics separation: UTM parameters and GA4 can segment by city
- Content personalization: Phone numbers, addresses, and service offerings vary by location
Content and Type Safety
Content management is centralized in src/lib/:
types.ts— TypeScript interfaces for cities, services, vehicles, locationscities.ts— City definitions (name, slug, coordinates, timezone)content.ts— Service descriptions, fleet inventory, contact details
This approach avoids a database dependency during initial launch while maintaining type safety. Migration to a headless CMS (Contentful, Sanity, etc.) is straightforward—only content.ts needs replacement with API calls.
Styling and Tailwind CSS 4
Tailwind CSS 4 uses a native Rust compiler via lightningcss for faster builds than the JavaScript-based v3. The global stylesheet at src/app/globals.css imports Tailwind directives:
@import "tailwindcss";
@layer components {
.btn-primary {
@apply px-4 py-2 bg-blue-600 text-white rounded;
}
}
Custom components are defined using @layer to maintain cascade predictability across pages.
Navigation and Layout Components
Shared UI components live in src/components/layout/:
Nav.tsx— Header with city selector dropdown, linking to/[city]/services,/[city]/fleet, etc.Footer.tsx— Footer with links, copyright, and city-specific contact info
These components receive the current city slug via Next.js useParams() hook (client-side) or via layout props (server-side), enabling context-aware navigation without repetition.
Next Configuration and Build Optimization
The next.config.ts enables:
reactStrictMode: true— Catches common React anti-patterns in developmentswcMinify: true— Uses SWC (Rust-based compiler) for faster minification than Terser- Image optimization via
next/imagecomponent for responsive, lazy-loaded images
The build process compiles all city variants statically (via next build), generating optimized HTML at request time or via ISR (Incremental Static Regeneration) for content updates without full rebuilds.
Key Architectural Decisions
- Subpaths vs. subdomains: Using subpaths (
/san-diego/) instead of subdomains improves SEO by consolidating domain authority and simplifies certificate management (single TLS cert covers all variants). - Monorepo over separate repos: pnpm workspaces enable code re