Why Programmatic SEO for Local Pages?
If you're a plumber in Lyon, a locksmith in Marseille, or an IT consultant in Algiers, you need a dedicated page for every city you serve. The problem: writing 50 unique pages by hand takes weeks.
Programmatic SEO for local pages solves this by combining a structured database, dynamic templates, and generative AI to produce unique content at scale.
At Otomy, we've deployed this method for B2B clients in France and Algeria. Result: +340% organic local traffic in 3 months on an IT services website.
The Technical Architecture in 5 Layers
Before diving in, here's the complete system architecture:
- Database → Supabase (managed PostgreSQL)
- Content generation → Claude AI via API
- Orchestration → n8n (self-hosted) or Make
- Dynamic frontend → Next.js deployed on Vercel
- SEO monitoring → Google Search Console + Screaming Frog
Step 1: Prepare the City Database
Create a cities table in Supabase with the following columns:
CREATE TABLE cities (
id SERIAL PRIMARY KEY,
city_name TEXT NOT NULL,
department TEXT,
region TEXT,
population INTEGER,
slug TEXT UNIQUE NOT NULL,
meta_title TEXT,
meta_description TEXT,
content TEXT,
status TEXT DEFAULT 'draft'
);
Insert your 50 target cities. For France, use the INSEE open data file of communes. For Algeria, the list of wilayas is usually sufficient.
Otomy tip: Add a local_signals column (TEXT) to store contextual data — main neighborhood names, dominant economic activity, population figures. Claude AI will use these signals to personalize each page.
Step 2: Generate Content with Claude AI
Build an n8n workflow with the following nodes:
- Trigger: Cron schedule or manual trigger
- Supabase Node: Fetch cities where
status = 'draft' - HTTP Request: Call the Claude API (Anthropic) with a structured prompt
- Supabase Node: Update the row with generated content
Here's the prompt template we use (adapted to your business):
You are an expert SEO writer. Write a local service page for a {service_type} in {city_name} ({department}).
Local context: {local_signals}
Required structure:
- H1 including the city and service
- 80-word introduction mentioning the city naturally
- "Our services in {city_name}" section with 4-5 bullet points
- "Why choose [company] in {city_name}?" section with 3 arguments
- 100-word paragraph about geographic coverage
- Final CTA mentioning the city
Constraints:
- 500-700 words
- Professional but approachable tone
- No generic content that's interchangeable between cities
- Primary keyword: "{service} {city_name}"
Critical point: The local_signals field makes all the difference between spun content and genuinely useful pages. Mention the local market, neighborhoods, major roads. Google detects interchangeable content.
Step 3: Build Dynamic Pages with Next.js
In your Next.js project (App Router), create a dynamic route:
app/services/[slug]/page.tsx
import { createClient } from '@supabase/supabase-js'
export async function generateStaticParams() {
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!)
const { data: cities } = await supabase.from('cities').select('slug').eq('status', 'published')
return cities?.map((city) => ({ slug: city.slug })) || []
}
export default async function LocalPage({ params }: { params: { slug: string } }) {
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!)
const { data: city } = await supabase.from('cities').select('*').eq('slug', params.slug).single()
return (
<article>
<h1>{city.meta_title}</h1>
<div dangerouslySetInnerHTML={{ __html: city.content }} />
</article>
)
}
Deploy on Vercel with ISR (Incremental Static Regeneration) so each new city page is immediately indexable.
Step 4: Automated Internal Linking
Programmatic local SEO only works if pages are interconnected. Automatically add:
- A local footer linking to the 5 nearest cities
- A hub page at
/services/listing all cities with optimized anchor text - A dynamic sitemap generated by Next.js (
app/sitemap.ts)
// app/sitemap.ts
export default async function sitemap() {
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!)
const { data: cities } = await supabase.from('cities').select('slug, updated_at').eq('status', 'published')
return cities?.map((city) => ({
url: `https://yoursite.com/services/${city.slug}`,
lastModified: city.updated_at,
})) || []
}
Step 5: Quality Control Before Publishing
Never publish without review. Here's our checklist at Otomy:
- Each page mentions the city at least 5 times naturally
- The
meta_titleis under 60 characters and includes city + service - The
meta_descriptionis under 155 characters - No page is a copy-paste of another (use Screaming Frog to detect near-duplicates)
-
LocalBusinessstructured data is present (schema.org) - Pages load in under 2 seconds (Vercel + ISR handles this)
Run all 50 pages through Screaming Frog in list mode to check tags, duplications, and technical errors in 10 minutes.
Realistic Timeline: 1 Week
| Day | Task |
|---|---|
| Monday | Local keyword research + Supabase database setup |
| Tuesday | n8n workflow configuration + Claude AI prompt engineering |
| Wednesday | Generate 50 content pieces + first review pass |
| Thursday | Next.js template development + internal linking |
| Friday | Screaming Frog QA + schema.org + Vercel deployment |
Mistakes That Kill Your Local Pages
- Interchangeable content: If you can swap the city name without changing the meaning, Google will notice too.
- No local signals: Mention neighborhoods, business districts, regional specifics.
- Forgetting mobile: 70% of local searches happen on mobile. Test every template.
- Ignoring speed: A local page that takes 4 seconds to load won't rank. The Next.js + Vercel + ISR combo solves this.
Conclusion
Programmatic SEO for local pages is no longer reserved for enterprises. With Supabase, Claude AI, n8n, and Next.js, an SMB can deploy 50 optimized local pages in one week — at a fraction of the cost of manual writing.
At Otomy, we help SMBs in France and Algeria make this transformation. If you want to dominate local search results in your industry, contact us for a free audit.