Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/aohl1986-ohlcenter/wp-local-seo-pages/archive/refs/heads/main.zipA WordPress plugin for local businesses that run city landing pages (/cities/springfield/, /cities/shelbyville/, …). It adds Schema.org JSON-LD markup, automatic internal linking between city pages, and keyword linking — fully configurable, with no business data hardcoded.
Extracted and generalized from a production plugin that powers 170+ city landing pages for a German moving company.
Features
- Schema.org JSON-LD on every city page: a business node with configurable
@type(LocalBusiness,MovingCompany,Plumber,Dentist, … or any custom type via filter), address, geo coordinates, opening hours, served area (city + postal codes), optional aggregate rating - Breadcrumb JSON-LD (
BreadcrumbList) for city pages - FAQ JSON-LD (
FAQPage) — content supplied entirely through a filter, nothing hardcoded - Automatic city interlinking: plain city names in list items (
<li>Shelbyville</li>) become links to the matching city page; existing links are never touched, and pages never link to themselves - Regional links block: "More cities in {region}" cross-links between city pages of the same region
- Keyword linking: link the first occurrence of configured keywords to target pages (internal link equity funneling)
- Shortcodes:
[lscp_city_links],[lscp_regional_links],[lscp_schema] - Secure cache clearing via
admin_postaction with nonce and capability check - Clean uninstall (removes its single option)
Requirements
- WordPress 6.0+
- PHP 7.4+
- City pages organized as child pages of one parent page (e.g. a page with slug
citiesand one child page per city)
Installation
- Copy the plugin folder to
wp-content/plugins/local-seo-city-pages/(or install the ZIP via Plugins → Add New → Upload). - Activate Local SEO City Pages.
- Configure it under Settings → Local SEO City Pages.
Configuration
1. Business data
Fill in your business name, Schema.org type, contact data, address, and geo coordinates. Only non-empty fields are emitted in the JSON-LD.
Opening hours use one rule per line:
Mo,Tu,We,Th,Fr|08:00|18:00
Sa|09:00|14:00
Aggregate rating: the rating fields are optional. Only use values that reflect real, verifiable customer reviews — fabricated ratings violate Google's structured-data guidelines and can lead to manual actions.
2. Cities
Set the parent page slug (default cities) and list your cities, one per line:
Springfield | springfield | Illinois | 62701,62702
Shelbyville | | Illinois
Capital City
Format: Name | slug | Region | postal codes — slug (auto-generated from the name), region and postal codes are optional. Lines starting with # are ignored.
Alternatively, provide cities from code:
add_filter( 'lscp_cities', function ( $cities ) {
$cities['springfield'] = array(
'name' => 'Springfield',
'slug' => 'springfield',
'region' => 'Illinois',
'postal_codes' => array( '62701', '62702' ),
);
return $cities;
} );
3. Keyword links
One per line: Keyword | /target-url/. Each keyword is linked at most once per page, existing links are skipped, and self-links are prevented. Also filterable via lscp_keyword_links.
4. FAQ markup
Supply FAQ items per city through the lscp_faq_items filter:
add_filter( 'lscp_faq_items', function ( $items, $city ) {
$items[] = array(
'question' => sprintf( 'Do you serve %s?', $city['name'] ),
'answer' => sprintf( 'Yes, %s is part of our regular service area.', $city['name'] ),
);
return $items;
}, 10, 2 );
Shortcodes
| Shortcode | Purpose |
|---|---|
[lscp_city_links cities="Springfield,Shelbyville"] |
Linked city list (all registered cities when the attribute is omitted) |
[lscp_regional_links] / [lscp_regional_links city="Springfield"] |
"More cities in this region" block |
[lscp_schema city="Springfield" postal_codes="62701"] |
Business JSON-LD anywhere in content |
Example JSON-LD output
For a city page at /cities/springfield/ with a plumbing business configured:
{
"@context": "https://schema.org",
"@type": "Plumber",
"@id": "https://example.com/cities/springfield/#business",
"name": "Acme Plumbing Springfield",
"url": "https://example.com/cities/springfield/",
"description": "Licensed plumbing service in Springfield — repairs, installations and 24/7 emergency service.",
"telephone": "+15551234567",
"email": "info@example.com",
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "1 Main Street",
"addressLocality": "Capital City",
"postalCode": "62700",
"addressRegion": "Illinois",
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "39.7817",
"longitude": "-89.6501"
},
"areaServed": [
{ "@type": "City", "name": "Springfield" },
{ "@type": "PostalAddress", "postalCode": "62701", "addressCountry": "US" },
{ "@type": "PostalAddress", "postalCode": "62702", "addressCountry": "US" }
],
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "08:00",
"closes": "18:00"
}
]
}
Validate your output with the Rich Results Test or the Schema Markup Validator.
Hooks reference
| Hook | Type | Purpose |
|---|---|---|
lscp_settings |
filter | Override any setting from code |
lscp_cities |
filter | Provide/modify the city registry |
lscp_schema_types |
filter | Extend the @type dropdown |
lscp_business_schema |
filter | Modify the business JSON-LD node |
lscp_breadcrumb_schema |
filter | Modify the breadcrumb JSON-LD node |
lscp_faq_items |
filter | Supply FAQ items per city |
lscp_keyword_links |
filter | Provide/modify keyword links |
lscp_regional_links_max |
filter | Cap the regional links count (default 12) |
lscp_regional_links_html |
filter | Modify the rendered regional block |
lscp_cleared_cache |
action | Hook additional cache backends into "Clear caches" |
License
GPL-2.0-or-later — see LICENSE.
Author
Alexander Ohl / Pragma Code