City Pages That Don't Get You Penalized
Every local service business needs city pages. If you serve Kokomo, Carmel, Fishers, and Indianapolis, you want a page for each. That's just how local SEO works.
The trap: build those pages by copying a template and swapping the city name, and Google will eventually classify them as doorway pages. Doorway pages are pages created primarily to funnel search traffic to a destination, with little unique value for the visitor. Google demotes them. Six thin city pages don't add up to more than zero.
Here's how we built ours to avoid that.
What actually separates a real city page from doorway spam
Two things make a city page legitimate: genuinely differentiated content and unique structured data scope.
Generic city pages fail because the only difference between them is a city name in the <h1>. The body copy, the FAQ, the CTA: identical. Google has seen this pattern thousands of times. It doesn't work.
What works is content that couldn't exist for any other city. That means writing it, not generating it from a template with a find-replace.
How we structured the city data
Each city in our local.ts config has a blurb field and its own faqs array. The blurb requirement: more than 40 characters, genuinely city-specific, can't just be the same sentence with a different city name dropped in.
Kokomo is our home base, so the blurb reflects that:
"Home base. We've produced brand films and built software for Kokomo and Howard County businesses, and we know the local market firsthand."
Carmel has a different operating context:
"We serve Carmel service businesses and professional firms with AI operations builds, web, and high-end video, remote-first with on-site shoots as needed."
These are short but they're real. A real differentiation, not a cosmetic one.
The FAQs follow the same rule. Every city gets questions that target actual local search queries for that city. Not "What services do you offer?" but "Who builds AI workflow automation in Carmel, IN?" That's a query someone actually types. Writing the FAQ question in that form, inside FAQPage schema, makes the page a candidate for that specific result.
Static pages at build time
The /locations/[city] route uses generateStaticParams to produce six distinct static HTML files at build time:
export function generateStaticParams() {
return cities.map((c) => ({ city: c.slug }));
}
Any slug not in the array gets notFound(). No fallback, no dynamic rendering for unknown cities. Six real pages, that's it.
Static generation matters for both speed and SEO. Googlebot receives a complete HTML document on first request, including the server-rendered JSON-LD schema. There's no JavaScript execution required to see the content.
Schema scoped per city
The root layout renders a site-wide ProfessionalService schema with all six cities in areaServed. Each city page additionally renders a second ProfessionalService block scoped to just that city, plus a FAQPage block with the city's own FAQ pairs.
Both use the same @id value: https://twenty1-media.com/#business. That stable identifier tells Google's knowledge graph these are all the same entity, not six separate businesses. The city-scoped page just reinforces that entity's relevance for one specific city.
The CTA names the city
Small detail that adds up: the call-to-action button on each city page reads "Book a free consultation," not just "Get in touch." The city name appears in the heading, the blurb, the FAQ questions, and the CTA. Not keyword stuffing, just honest specificity.
What we didn't do
We didn't generate city pages with AI. We didn't copy one page six times and find-replace the city name. We didn't create pages for cities we don't actually serve. The six cities we built pages for are the six cities we listed as our service area on the Google Business Profile.
City pages work when they're accurate, specific, and actually useful to someone in that city. That's a higher bar than most templates set, but it's the only approach that survives a Google quality review.
If your service business needs city pages built this way, start with a free consultation.