Building a Multi-City Production Portal: Next.js 14 Architecture with Dynamic City Routes and Monorepo Structure
What Was Done
I scaffolded a multi-city web platform for Rick Drake Productions using Next.js 14 with TypeScript, Tailwind CSS 4, and a monorepo structure via pnpm workspaces. The architecture supports a hub domain (rickdrakeproductions.com) with city-specific pages using dynamic routing, enabling SEO-friendly URLs while maintaining a single codebase. The domain was registered via Route53 with privacy protection, and the infrastructure is primed for CloudFront distribution and SSL certification.
Technical Details: Architecture and File Structure
The project uses a pnpm monorepo workspace defined in /Users/cb/Documents/repos/sites/rickdrakeproductions.com/pnpm-workspace.yaml. This allows multiple applications to share dependencies while maintaining isolation. The primary application lives in apps/web/, scaffolded with Next.js 14's App Router (not the legacy Pages Router).
Dynamic City Routing: The application uses Next.js dynamic segments via the [city] directory pattern:
apps/web/src/app/page.tsx— Root homepage (rickdrakeproductions.com)apps/web/src/app/[city]/page.tsx— City-specific landing pages (e.g., /san-diego/)apps/web/src/app/[city]/services/page.tsx— City services listingapps/web/src/app/[city]/fleet/page.tsx— Equipment catalog per cityapps/web/src/app/[city]/fleet/[vehicle]/page.tsx— Individual vehicle detailsapps/web/src/app/[city]/contact/page.tsx— City-specific contact formapps/web/src/app/[city]/about/page.tsx— Company info (localized)apps/web/src/app/[city]/locations/page.tsx— Physical locations per cityapps/web/src/app/[city]/portfolio/page.tsx— Project showcase per city
This structure allows each city URL to render localized content while avoiding subdomain complexity. From an SEO perspective, this approach supports:
- Unified domain authority on rickdrakeproductions.com
- Cleaner URL structure vs. subdomains (san-diego.rickdrakeproductions.com)
- Consolidated GA4 tracking across all cities
- Single SSL certificate covering all routes
Data Layer: City and content data are centralized in utility modules:
apps/web/src/lib/cities.ts— Array of city objects with slugs, names, and metadataapps/web/src/lib/content.ts— Content lookup functions (services, equipment, portfolio data)apps/web/src/lib/types.ts— TypeScript interfaces for City, Vehicle, Service, etc.
This abstraction means adding Phoenix, Palm Springs, or LA is as simple as extending the cities array—no new route files needed.
Infrastructure: Domain, DNS, and Next.js Configuration
Domain Registration: rickdrakeproductions.com was registered via AWS Route53 (not GoDaddy) to eliminate handoff friction. Route53 provides native integration with CloudFront distributions and ACM certificates. Privacy protection was enabled to shield WHOIS records.
Next.js Config: The apps/web/next.config.ts file includes:
- Tailwind CSS 4 integration (requires the
lightningcssnative binary for optimal performance) - Image optimization via Next.js Image component
- Static generation (SSG) for city pages—each city route is pre-rendered at build time
Styling: Tailwind CSS 4 with the lightningcss parser. This required explicit installation of the platform-specific native binary (lightningcss-darwin-x64 for macOS development). The globals stylesheet is defined in apps/web/src/app/globals.css with Tailwind directives.
Dependency Management: The package.json in the workspace root specifies pnpm as the package manager. Dependencies like next, react, and tailwindcss are hoisted to the workspace root, reducing duplication. DevDependencies include typescript and ESLint for code quality.
Key Decisions and Their Rationale
1. Monorepo over Separate Repos: A single monorepo allows shared utilities, consistent versioning, and easier maintenance. If Rick's business expands to internal tools (admin dashboard, booking system), they can coexist in apps/ without repo fragmentation.
2. Next.js App Router with SSG: The App Router (introduced in Next.js 13) offers cleaner layouts, parallel routes, and intercepting routes—useful for modal-based interactions. Static generation means every city page is pre-rendered at build time, with revalidate intervals for periodic updates without full rebuilds.
3. Route53 over GoDaddy: While GoDaddy offers cheaper domain registration, Route53 integration eliminates DNS configuration friction and allows DNS records to be managed via Infrastructure-as-Code (Terraform/CloudFormation). This is crucial for automating certificate validation and scaling.
4. Single Domain with Path-Based Routing vs. Subdomains: Path-based routing (/san-diego/, /las-vegas/) consolidates SEO authority, simplifies analytics, and reduces SSL certificate complexity. Subdomains would require wildcard certificates and separate GA4 properties—overhead we avoid here.
Build and Deployment Pipeline
The build process invokes next build from apps/web/, which:
- Runs TypeScript type-checking
- Generates static pages for all city routes (based on the cities array)
- Outputs optimized HTML, CSS, and JS to
.next/ - Produces a build manifest for CloudFront cache invalidation
The output is deployed to an S3 bucket (e.g., rickdrakeproductions-web-prod) fronted by a CloudFront distribution. Cache headers are set per file type: static assets (JS, CSS) with 1-year max-age, HTML with short TTLs for fresh content.
What's Next
- SSL Certificate: Request an ACM certificate for rickdrakeproductions.com and www.rickdrakeproductions.com, then attach it to the CloudFront distribution.
- CloudFront Distribution: Create a distribution (or update existing) with the S3 bucket as origin, enable compression, and configure cache behaviors.
- DNS Records: Add A and AAAA records in Route53 pointing to the CloudF