Programmatic SEO — Multi-Location Sites
For contractor and home-services clients operating across multiple cities, use Astro’s static route generation to build a dedicated page per city × service combination.
File Structure
Section titled “File Structure”src/├── data/│ ├── cities.json # [{slug, name, region}, ...]│ └── services.json # [{slug, name, description}, ...]└── pages/ └── [city]/ └── [service].astro # getStaticPaths() = cartesian productURL Structure
Section titled “URL Structure”/harare/plumbing//harare/roofing//bulawayo/plumbing//bulawayo/roofing/Static URLs only — dynamic URLs crawl worse and rank lower.
getStaticPaths Pattern
Section titled “getStaticPaths Pattern”---import { getCollection } from 'astro:content';import cities from '../../data/cities.json';import services from '../../data/services.json';
export async function getStaticPaths() { return cities.flatMap(city => services.map(service => ({ params: { city: city.slug, service: service.slug }, props: { city, service }, })) );}
const { city, service } = Astro.props;---Astro v5 generates 10,000 static pages in under 60 seconds. Zero performance penalty at scale.
Content Requirements
Section titled “Content Requirements”Each location page must include:
- Local landmarks and neighborhoods
- Local team member names (if applicable)
- Community involvement (local sponsorships, events)
- Region-specific service details (local regulations, climate considerations)
- Unique testimonials from local customers
Documented impact: 107% ranking lift with hyperlocal vs generic content (AgencyAnalytics, 2025). Real case: contractor expanded to 10+ locations → 500% organic visibility increase, 400%+ phone calls YoY.
Blog Strategy for Multi-Location Clients
Section titled “Blog Strategy for Multi-Location Clients”- Hybrid model: centralized brand content + location-specific blog posts
- Generic blog republished across all locations = weak Google signal
- Use
mos-seo-contentparameterized with city/service slots to generate per-location content - Add
LocalBusinessschema withareaServedon each[city]/[service]page - Separate Google Business Profile per location (not one shared profile)
Structured Data
Section titled “Structured Data”{ "@type": "LocalBusiness", "name": "Acme Plumbing Harare", "areaServed": { "@type": "City", "name": "Harare" }, "address": { "@type": "PostalAddress", "addressLocality": "Harare", "addressCountry": "ZW" }}Pricing This Service
Section titled “Pricing This Service”Package as a distinct offering — separate from the standard site build:
| Tier | Scope | Price range |
|---|---|---|
| Starter | Up to 3 cities × 5 services = 15 pages | Add $500–$800 to base build |
| Growth | Up to 10 cities × 10 services = 100 pages | Add $1,500–$2,500 |
| Scale | 50+ cities, AI-generated per-location content | Custom, $5,000+ |
Related
Section titled “Related”- Tech Stack Standards
- CMS Decision Tree — Storyblok/Sanity for multi-location content management