Building a Multi-City Production Portal: Next.js 14 Monorepo with Dynamic City Routes and AWS Infrastructure
What Was Done
We scaffolded a Next.js 14 monorepo architecture for Rick Drake Productions — a multi-city production coordination platform launching with San Diego and Las Vegas, with Phoenix, Palm Springs, and LA on the roadmap. The project uses dynamic route segments to serve city-specific content from a single codebase while maintaining SEO-friendly URLs and independent branding per market.
Key accomplishments in this session:
- Registered
rickdrakeproductions.comvia Route53 with privacy protection enabled - Configured pnpm workspaces across a monorepo structure
- Built dynamic city-based routing with Next.js App Router
- Scaffolded city-specific pages (services, fleet, contact, locations, portfolio)
- Established type-safe content management with TypeScript
- Configured Tailwind CSS 4 with native binary compilation
Technical Details: Monorepo Structure
The project follows a pnpm workspace pattern defined in pnpm-workspace.yaml:
packages:
- 'apps/*'
- 'packages/*'
This allows us to manage multiple applications (currently apps/web) and shared packages under one repository with hoisted node_modules and efficient disk usage.
The package.json at the root defines workspace dependencies and shared dev tools. The apps/web directory contains the Next.js 14 application with TypeScript and Tailwind CSS 4 configured in next.config.ts.
Dynamic City Routing Architecture
The core routing strategy uses Next.js dynamic segments. Directory structure:
apps/web/src/app/
├── page.tsx # Root homepage
├── [city]/
│ ├── layout.tsx # City-specific wrapper
│ ├── page.tsx # City homepage
│ ├── services/page.tsx
│ ├── fleet/page.tsx
│ ├── fleet/[vehicle]/page.tsx
│ ├── contact/page.tsx
│ ├── about/page.tsx
│ ├── locations/page.tsx
│ └── portfolio/page.tsx
The [city] dynamic segment captures the city slug from the URL path. This approach provides several advantages:
- SEO benefits: URLs like
/san-diego/servicescontain semantic keywords for geographic targeting - Code reuse: A single component tree handles all cities without duplication
- Scalability: Adding Phoenix only requires adding city data; no new routes needed
- Shared styling: Global CSS in
globals.cssapplies to all city variants
Data Management: Cities and Content
We created type-safe data structures in apps/web/src/lib/types.ts to define the shape of city and content data. The apps/web/src/lib/cities.ts file exports a cities array:
export const cities = [
{ slug: 'san-diego', name: 'San Diego', state: 'CA' },
{ slug: 'las-vegas', name: 'Las Vegas', state: 'NV' }
];
Content-specific information lives in apps/web/src/lib/content.ts, which can be extended to fetch from a headless CMS, database, or static file system later. This separation allows Rick to eventually swap out the data layer without touching routing logic.
The [city]/layout.tsx validates that the requested city exists:
export async function generateStaticParams() {
return cities.map(city => ({ city: city.slug }));
}
This ensures Next.js pre-renders all city routes at build time and prevents 404s for invalid cities.
Infrastructure: Domain and DNS
We registered rickdrakeproductions.com using AWS Route53 (not GoDaddy or Namecheap) for several reasons:
- Unified infrastructure: DNS management lives in the same AWS account as future compute resources
- Programmatic access: CloudFormation and Terraform can manage DNS records alongside other resources
- No external handoff: Avoids sync issues between domain registrar and DNS provider
- Privacy protection: Enabled at registration to protect Rick's personal details in WHOIS
The Route53 hosted zone was created during domain registration. DNS records will point to CloudFront distributions or application load balancers depending on the final deployment model.
Build Pipeline: Dependency Installation
Installing dependencies proved challenging on the development machine due to network constraints and optional native binaries. The final successful approach:
pnpm install --no-optional
This skipped optional dependencies like the sharp native binary (used for image optimization), reducing installation time. We later explicitly installed lightningcss-darwin-x64 to support Tailwind CSS 4's native CSS parsing on macOS.
The Next.js build completed successfully after resolving the lightningcss native binary. This validates that the project structure, TypeScript configuration, and Tailwind setup are correct.
Frontend Styling: Tailwind CSS 4
We configured Tailwind CSS 4 in apps/web/src/app/globals.css using the new @import syntax instead of the legacy @tailwind directives. Tailwind 4 compiles CSS natively for performance, which requires the lightningcss native binary for the target platform (darwin-x64 for Intel macOS, darwin-arm64 for Apple Silicon).
Navigation and footer components in apps/web/src/components/layout/ will use Tailwind utility classes to ensure consistent styling across all cities while allowing per-city overrides via CSS custom properties or component composition.
Current State and Next Steps
The local development environment is operational at http://localhost:3000/. The San Diego city page renders correctly, confirming that:
- Dynamic routes resolve properly
- Layout composition works across nesting levels
- TypeScript compilation succeeds
- Tailwind CSS applies styles correctly
Remaining work includes:
- CloudFront distribution setup: Create a distribution for rickdrakeproductions.com pointing to the Next.js deployment (AWS Amplify, Vercel, or self-hosted)
- SSL/TLS certificate: Request a wildcard certificate from ACM for rickdrakeproductions.com and *.rickdrakeproductions.com if using subdomain routing
- Content population: Fill in services, fleet vehicles, locations, and portfolio items for each city
- Analytics integration: Add Next.js analytics and site instrumentation
- Old WordPress migration: Plan transition from sandiegoproductions.com's dated Visual Composer setup
The mon