Local SEO That Actually Converts: Pre-Generated City Pages, JSON-LD, and Search Console Workflows

by Marc David, Founder & Lead Developer

The problem we solved

AAA Midea serves all of Slovakia. They needed pages for the cities they actually target, clean structure for search, and a CMS their team could use without calling a developer. Thin or duplicated pages do not convert, and big rewrites stall if editors cannot keep up. Our goal was to: make locality useful for users and credible for search.

The approach in three parts

  1. a typed locality model that powers programmatic pages without doorway patterns
  2. structured data (JSON-LD) that matches visible content and keeps signals consistent
  3. a weekly Search Console workflow that finds issues early and guides improvements

1) A typed locality model (that editors can live with)

We modelled Slovak city names alongside their “in” forms, slugs, and labels, so content reads naturally and URLs stay predictable.

type CityEntry = {
  name: string // "Bratislava"
  in: string // "v Bratislave" for headings and leads
  slugCity: string // "bratislava"
  slugIn: string // "v-bratislave"
}
  • A canonical set of city slugs drives generateStaticParams and the sitemap.
  • A helper maps any slug back to a display label, which prevents copy errors.
  • We only index the cities we actually serve. A nationwide page covers the rest.

Top tip

Maintain one source of truth for localities. Keep both the nominative form and the “in” form so headings read naturally. Expose helpers like getCityBySlug to templates and metadata.

2) Programmatic pages that are not thin

Each city page follows the same template but is never a blank clone. We pair reusable components with proof blocks and media:

  • services offered in that city, with clear CTAs
  • recent projects or case studies as proof
  • consistent headings that use the correct “in” form
  • image variants that are optimised and properly described

Guardrails in the CMS require a lead, at least one proof item, and image alt text. Drafts stay out of the sitemap and out of navigation until they meet quality checks.

3) JSON-LD that search can trust

We inject typed JSON-LD with a small DTS-backed helper so properties stay valid:

  • Service or vertical subtypes for what is offered
  • BreadcrumbList that matches the actual links
  • LocalBusiness details where applicable, and areaServed for the city
  • stable url and canonical rules that reflect the route

Because the JSON-LD is generated from the same data model as the page, what users see and what crawlers read never drift.

Putting it together in Next.js

// generate valid routes only
export function generateStaticParams() {
  return ALL_CITY_SLUGS.map((city) => ({ city }))
}

// consistent, city-aware metadata
export async function generateMetadata({ params }) {
  const { city } = await params
  const entry = getCityBySlug(city) ?? DEFAULT_LOCATION
  return constructServiceLocationMetadata({
    service: 'Klimatizácia, rekuperácia a tepelné čerpadlá',
    city: entry.name,
    canonicalPath: `/lokality/${city}`,
  })
}

And in the page:

<JsonLd id="jsonld-breadcrumbs" data={withContext(breadcrumbJsonLd([...]))} />
<JsonLd id="jsonld-service" data={withContext(serviceJsonLd({
  serviceName: 'Montáž a servis klimatizácie',
  url: pageUrl,
  providerName: 'AAA Midea',
  areaServed: [entry.name],
}))} />

Sitemaps that reflect reality

We generate a base set of routes plus only the canonical city slugs, refreshed on a schedule. This keeps the index clean and predictable.

export const revalidate = 3600 // sitemap freshness
const localitySlugs = new Set([
  ...ALL_CANONICAL_CITY_SLUGS,
  DEFAULT_LOCATION.slugCity,
])

Pages inherit monthly change frequency. Nationwide can carry a slightly higher priority than individual cities to guide crawlers sensibly.

Search Console as a weekly habit

We use a domain property in Search Console and review four areas weekly:

  • Coverage: find unexpected exclusions, soft 404s, and canonical conflicts
  • Enhancements: schema warnings for Service, Breadcrumb, or LocalBusiness
  • Links: confirm internal linking is surfacing the right cities
  • Performance: queries per city page, CTR on locality terms, and device splits

When issues appear, we fix the source of truth first (the locality model or template), then request validation. Editors see a simple dashboard inside the CMS, and owners can still explore raw GSC when they want detail.

Anti-doorway rules

  • index only the cities we can genuinely serve
  • unique leads and proof per city, no boilerplate walls of text
  • strict canonical rules between service pages and city pages
  • drafts never enter the sitemap

These rules keep the value for users high and the site in good standing.

Core Web Vitals and media

City pages load fast because the heavy lifting happens up front. We resize images at upload, serve responsive variants, and cache near users. Video is used sparingly and only where it adds proof. Measured against our budgets, we see steady CWV scores and quick first input.

A-B testing and feature flags

Owners can toggle variants of headlines, proof blocks, and CTAs without a deploy. This is how we test changes safely and schedule rollouts. For the deeper implementation, see From Wix to Custom Without Downtime: Dual-Run Migrations with Feature Flags.

Results so far

Within a few weeks of launching the new locality system, average positions moved from around 60 to top 15 for priority queries. The pipeline is young, so we expect further gains as more proof accrues and internal links mature.

Top tip

Do less, but do it consistently. A clean locality model, honest proof, and a weekly Search Console routine beat giant content dumps every time.

Checklist you can copy

  • One typed source of truth for cities and “in” forms
  • Programmatic pages with required proof and media
  • JSON-LD generated from the same data as the page
  • Clean sitemaps and predictable canonicals
  • Weekly Search Console workflow with clear owners
  • Flags for A-B tests and safe rollouts

More articles

From Wix to Custom Without Downtime: Dual-Run Migrations with Feature Flags

How we moved a fast-growing retailer from Wix to a custom stack without breaking sales, using a dual-run architecture, an ingestion service, and owner-controlled feature flags.

Read more

Tell us about your project