Building a Multi-City Production Portal: Next.js Architecture with Dynamic Route Generation

We just launched the foundational infrastructure for Drake Productions' multi-city portal—a scalable Next.js 14 application that powers city-specific websites from a single codebase. Here's the technical breakdown of how we structured it, why we made specific architectural choices, and what's running in production now.

What We Built

The Drake Productions portal is a monorepo-based Next.js 14 application that generates city-specific websites dynamically using App Router's dynamic route parameters. Instead of maintaining separate codebases for San Diego, Las Vegas, Phoenix, Palm Springs, and LA, we have one unified application that serves all cities through parameterized routes.

Domain structure:

  • rickdrakeproductions.com — central hub (registered via Route53)
  • /[city]/ — dynamic city pages serving San Diego, Las Vegas, and others
  • Future: subdomains or separate deployments per city based on SEO performance

Project Structure & Monorepo Setup

We scaffolded this as a monorepo using pnpm-workspace.yaml to support future services (API layer, admin panels, etc.). The current structure:

rickdrakeproductions.com/
├── pnpm-workspace.yaml
├── package.json
└── apps/
    └── web/
        ├── next.config.ts
        ├── package.json
        ├── tsconfig.json
        ├── tailwind.config.ts
        └── src/
            ├── app/
            │   ├── layout.tsx (root layout)
            │   ├── page.tsx (homepage)
            │   ├── globals.css
            │   └── [city]/
            │       ├── layout.tsx (city-specific layout)
            │       ├── 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
            ├── components/
            │   └── layout/
            │       ├── Nav.tsx
            │       └── Footer.tsx
            └── lib/
                ├── types.ts
                ├── cities.ts
                └── content.ts

Dynamic Routing & Type Safety

The core of this system lives in src/lib/cities.ts, which defines the list of active cities and their metadata:

// src/lib/cities.ts
export const CITIES = [
  { slug: 'san-diego', name: 'San Diego', state: 'CA' },
  { slug: 'las-vegas', name: 'Las Vegas', state: 'NV' },
  // Phoenix, Palm Springs, LA added when ready
];

export function getCityBySlug(slug: string) {
  return CITIES.find(c => c.slug === slug);
}

export function getAllCitySlugs() {
  return CITIES.map(c => c.slug);
}

This enables type-safe dynamic routes. The [city] folder uses Next.js's dynamic route convention—requests to /san-diego/services match the [city]/services/page.tsx template automatically.

Each city page receives the city slug via params.city:

// src/app/[city]/page.tsx
export async function generateStaticParams() {
  return getAllCitySlugs().map(slug => ({ city: slug }));
}

export default function CityPage({ params }: { params: { city: string } }) {
  const city = getCityBySlug(params.city);
  if (!city) notFound();
  // Render city-specific content
}

Using generateStaticParams() tells Next.js to pre-render these routes at build time, eliminating runtime lookups and improving performance.

Content Management Strategy

We created src/lib/content.ts to centralize city-specific content (hero text, service descriptions, etc.). This separates content from components, making it easy to update without touching React code:

// src/lib/content.ts
export const cityContent = {
  'san-diego': {
    heroTitle: 'Production Coordination in San Diego',
    heroDescription: 'Local expertise, professional fleet...',
    services: [
      { name: 'Grip Equipment', description: '...' },
      // ...
    ]
  },
  'las-vegas': {
    heroTitle: 'Production Services in Las Vegas',
    // ...
  }
};

Styling with Tailwind CSS 4 & LightningCSS

We configured next.config.ts to use Tailwind CSS 4's new native CSS nesting via LightningCSS. This required explicitly installing the native binary for the build environment:

// next.config.ts
const nextConfig: NextConfig = {
  experimental: {
    optimizePackageImports: ['@shadcn/ui'],
  },
};

export default nextConfig;

During setup, we encountered a critical issue: LightningCSS requires a platform-specific native binary. The JavaScript-only version times out during CSS processing. We resolved this by installing lightningcss-darwin-x64 explicitly in apps/web/node_modules after detecting the x64 macOS architecture.

Infrastructure & Domain Registration

Domain: We registered rickdrakeproductions.com via AWS Route53 with privacy protection enabled. This choice kept everything in the AWS ecosystem, avoiding DNS handoff friction between GoDaddy and Route53.

Future CloudFront Setup: The application will be deployed to Vercel or CloudFront. We're planning to use CloudFront (distribution ID placeholder: E2Q4UU71SRNTMB) with an ACM certificate covering:

  • rickdrakeproductions.com
  • *.rickdrakeproductions.com (for future subdomain deployments)

Route53 will manage DNS records pointing to the CloudFront distribution.

Key Architectural Decisions

  • Single Codebase, Multiple Cities: Rather than separate repos per city, we use parameterized routes. This ensures consistent branding, shared component libraries, and centralized updates. As traffic grows, we can split into subdomains without duplicating code.
  • Static Generation: Using generateStaticParams() means city pages are pre-rendered at build time, resulting in near-zero latency responses and minimal server load. Future content updates trigger rebuilds.
  • TypeScript Everywhere: src/lib/types.ts defines strict types for cities, content, and params, catching routing errors at compile time rather than runtime.
  • Monorepo with pnpm: pnpm's workspace support enables us to add shared packages (UI components, API clients) under packages/ without duplicating dependencies. This scales better than managing independent Next.js installations.
  • Native CSS Tooling: By embracing Tailwind 4