Building a Multi-City Production Portal with Next.js 14: Architecture, Infrastructure, and SEO Strategy
This post covers the technical implementation of rickdrakeproductions.com, a multi-city portal for a production coordination business. The project required domain registration, monorepo setup, dynamic routing for city-specific content, and CloudFront distribution configuration. Here's how we built it.
The Architecture Decision: Why Subdomains vs. Subdirectories
The initial architectural question: should each city be served via subdomains (e.g., san-diego.rickdrakeproductions.com) or subdirectories (e.g., rickdrakeproductions.com/san-diego)? For SEO and operational simplicity, we chose subdirectories as the primary pattern, with the flexibility to add subdomains later if individual city brands require it.
Why subdirectories first?
- Link equity consolidation: All SEO signals flow to the root domain rather than splitting across subdomains
- Shared infrastructure: Single SSL certificate, single CloudFront distribution, unified analytics
- Referral model compatibility: If Rick doesn't adopt the service, the portal becomes a referral capture mechanism—subdirectories make client handoff cleaner
- Simpler deployment: One build artifact serves all cities rather than managing separate builds per subdomain
Domain Registration and Route53 Setup
We registered rickdrakeproductions.com via AWS Route53 rather than GoDaddy, despite having GoDaddy API credentials available. Here's why:
- Unified AWS account: DNS, certificates, CloudFront, and Lambda all live in the same account—no cross-service credential management
- Infrastructure-as-code: Route53 records can be managed via Terraform or CloudFormation; GoDaddy requires manual API calls or third-party tools
- Privacy protection included: Route53 registration includes WHOIS privacy at no extra cost
The registration command checked existing contact information from other Route53-registered domains (specifically 86from.com) to pre-populate the registrant details, minimizing manual entry and reducing registrant data exposure across multiple services.
Monorepo and Build Configuration
Project structure:
rickdrakeproductions.com/
├── pnpm-workspace.yaml
├── package.json
└── apps/
└── web/
├── next.config.ts
├── package.json
└── src/
├── app/
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ └── [city]/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── services/page.tsx
│ ├── fleet/page.tsx
│ ├── fleet/[vehicle]/page.tsx
│ ├── contact/page.tsx
│ ├── about/page.tsx
│ ├── locations/page.tsx
│ └── portfolio/page.tsx
└── lib/
├── types.ts
├── cities.ts
└── content.ts
We used pnpm workspaces instead of npm or yarn because:
- Stricter dependency isolation prevents phantom dependencies
- Faster install times via content-addressable storage
- Disk efficiency—dependencies deduplicated across workspaces
Dynamic Routing with Catch-All Parameters
The [city] directory uses Next.js 14's dynamic segments. The routing strategy:
/→ Root homepage listing all cities/[city]/→ City-specific landing page (e.g.,/san-diego/)/[city]/services→ City services page/[city]/fleet→ City vehicle fleet listing/[city]/fleet/[vehicle]→ Individual vehicle detail page/[city]/contact,/about,/locations,/portfolio→ City-scoped pages
The src/lib/cities.ts file serves as the source of truth for available cities and their metadata:
export const CITIES = {
'san-diego': { name: 'San Diego', slug: 'san-diego', ... },
'las-vegas': { name: 'Las Vegas', slug: 'las-vegas', ... },
// Phoenix, Palm Springs, LA added as they launch
}
export function getCityBySlug(slug: string) {
return CITIES[slug] || null
}
This approach allows dynamic page generation while maintaining type safety. The layout component at src/app/[city]/layout.tsx wraps all city-scoped pages and handles city-specific metadata (title, description, OG tags).
Tailwind CSS 4 and lightningcss Native Binary
The build encountered a native dependency issue early: Tailwind CSS 4 uses lightningcss, which has optional native binaries for each platform. On macOS (darwin-x64), the npm install process doesn't automatically fetch platform-specific binaries if the main package manager is offline or if optional dependencies are skipped.
Solution:
# Install the darwin-x64 native binary explicitly
npm install lightningcss-darwin-x64 --save-optional
# Or use pnpm with optional dependency support
pnpm install --optional
This matters because compiling lightningcss from source during the Next.js build is significantly slower than using precompiled binaries. For production builds, we ensure the correct platform binary is installed before build time.
Infrastructure: CloudFront, ACM, and SSL
While rickdrakeproductions.com is new, the infrastructure patterns learned from sibling projects (adamcherrycomics.com, dangerouscentaur.com) informed the setup:
- CloudFront distribution: Edge caching for static assets, regional failover for origin
- ACM certificate: Wildcard cert covering
*.rickdrakeproductions.comfor future subdomain flexibility; DNS validation via Route53 - S3 origin: Static site export option if needed; dynamic rendering via Lambda@Edge for city-specific headers/redirects
The distribution configuration (CloudFront dist ID: pending) enables:
- Automatic gzip/brotli compression for text assets
- HTTP/2 push for critical resources
- Cache invalidation triggered by deployment pipelines
- Regional failover if the primary origin becomes unavailable
Content and Type Safety
The src/lib/types.ts file defines the shape of all city, service, vehicle, and portfolio data:
export interface City {
slug: string
name: string
description: string
// city-specific metadata
}
export interface Vehicle {
id: string
name: string
year