WP Manifestindependent plugin directory
manifest / seo / geo-seo-locator

Geo SEO Locator

Wordpress plugin to add geo seo variables in URLS

★ 0stars

Install

Download source zip No release zip yet

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/axsag/geo-seo-locator/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

A WordPress plugin that injects a {zip-city} segment into any URL and exposes it through shortcodes and Yoast SEO variables — letting you serve thousands of geo-targeted page variants without duplicating content.

https://your.site/13100-aix-en-provence/contact/
https://your.site/06000-nice/contact/
https://your.site/75001-paris-1er/contact/

All three URLs serve the same page. The geo segment is captured as a query variable and made available via shortcodes, Yoast %% tokens, and nav menu rewriting.


Features

  • Clean geo URLs — rewrite rules for pages, paginated pages, and category archives
  • 10 shortcodes — city name, ZIP, description, neighbourhood list, location links, conditionals, SEO title builder
  • Yoast SEO variables — %%geo_city%%, %%geo_zip%%, %%geo_zip_city%%, %%geo_description%%
  • Geo-sticky nav menus — menu links automatically carry the geo prefix (PHP only, no JS)
  • Default location — fallback city when no geo segment is present or an unknown slug is visited
  • Per-city content — display name, free-text description, neighbourhood list per location
  • Admin UI — dedicated top-level menu with list view, add/edit form, and shortcode reference

Installation

  1. Download the latest release zip.
  2. In WordPress: Plugins → Add New → Upload Plugin → select the zip → Install → Activate.
  3. Go to Geo SEO → All Locations and add your first city.
  4. Add shortcodes to your page content.

After activation, if geo URLs return 404 go to Settings → Permalinks and click Save Changes to flush rewrite rules.


URL Structure

URL Resolves to
/{zip-city}/ Front page / homepage
/{zip-city}/{page-slug}/ Any static page
/{zip-city}/{page-slug}/page/{n}/ Paginated static page
/{zip-city}/{category-base}/{slug}/ Category archive

The {zip-city} segment must match: 4–5 digits + dash + lowercase slug
e.g. 13100-aix-en-provence, 06000-nice, 75001-paris-1er


Shortcodes

[geo_city]

City name extracted from the URL, title-cased.

Plombier à [geo_city fallback="votre ville"]
→ Plombier à Aix-En-Provence
Attribute Default Description
fallback (empty) Text shown when no geo context
format title title | upper | lower

[geo_zip]

Postal / ZIP code.

Intervention en [geo_zip] et ses alentours
→ Intervention en 13100 et ses alentours
Attribute Default Description
fallback (empty) Text shown when no geo context

[geo_zip_city]

ZIP + separator + City in one shot.

Zone couverte : [geo_zip_city sep=" · "]
→ Zone couverte : 13100 · Aix-En-Provence
Attribute Default Description
sep – Separator between ZIP and city
fallback (empty) Text shown when no geo context
format title City name format: title | upper | lower

[geo_description]

Free-text block entered per city in Geo SEO → Edit Location. Basic HTML allowed.

[geo_description tag="p" class="city-intro"]
→ <p class="city-intro">Nice est une ville sur la Côte d'Azur…</p>
Attribute Default Description
tag div Wrapper element: div | p | span | section | none
class gsl-description CSS class on the wrapper
fallback (empty) Text shown when no description is set

Use tag="none" to output the raw HTML without any wrapper element.


[geo_city_link]

Anchor tag linking to the current geo URL.

[geo_city_link class="btn-zone"]
→ <a href="/13100-aix-en-provence/" class="btn-zone">Aix-En-Provence</a>
Attribute Default Description
text (city name) Link label
class (empty) CSS class on the <a>
fallback (empty) Output when no geo context

[geo_quartiers]

Neighbourhood list configured per city in the admin.

[geo_quartiers tag="ul" class="quartiers-list"]
→ <ul class="quartiers-list"><li>Mazarin</li><li>Sextius</li>…</ul>

[geo_quartiers tag="p" separator=" · "]
→ <p>Mazarin · Sextius · Encagnane</p>

[geo_quartiers tag="div" separator="\n"]
→ <div>Mazarin<br>Sextius<br>Encagnane</div>
Attribute Default Description
tag ul ul | ol | div | p
class gsl-quartiers CSS class on the wrapper
separator (empty) Separator between items. Empty = <li> items for ul/ol, , for div/p. Use \n for <br>.
fallback (empty) Text shown when no quartiers defined

[geo_locations_list]

One link per registered location, pointing to the geo variant of the current (or any named) page. The current city is excluded by default.

[geo_locations_list page="contact" link_class="location-link"]
→ <ul class="gsl-locations-list">
     <li><a href="/06000-nice/contact/" class="location-link">Nice</a></li>
     <li><a href="/06270-villeneuve-loubet/contact/" class="location-link">Villeneuve-Loubet</a></li>
   </ul>

[geo_locations_list include_self="yes"]
→ includes current city with active_class applied
Attribute Default Description
page (current page slug) Target page slug
tag ul ul | ol | div | nav
class gsl-locations-list CSS class on the wrapper
link_class location-link CSS class on every <a>
active_class location-link--active Extra class on the current city's link (only with include_self="yes")
include_self no Set to yes to include the current city in the list
fallback (empty) Text shown when no locations are configured

[geo_if]…[/geo_if]

Renders content only when a geo segment is active.

[geo_if]
  <p>Disponible à [geo_city] — appelez le 04 XX XX XX XX.</p>
[/geo_if]

[geo_if zip_city="13100-aix-en-provence"]
  <p>Nous sommes basés à Aix — intervention en 5 min.</p>
[/geo_if]
Attribute Default Description
zip_city (empty) If set, only renders for this exact slug

[geo_unless]…[/geo_unless]

Opposite of [geo_if] — renders only when no geo segment is active.

[geo_unless]
  <p>Contactez-nous pour connaître nos zones d'intervention.</p>
[/geo_unless]

[geo_meta_title]

Formatted SEO string using {city}, {zip}, {zip_city} as placeholders.

[geo_meta_title template="Plombier à {city} – Devis gratuit {zip}"]
→ Plombier à Aix-En-Provence – Devis gratuit 13100
Attribute Default Description
template {city} ({zip}) Template string with {city}, {zip}, {zip_city}
fallback (empty) Output when no geo context

Yoast SEO Variables

Four %% tokens are available directly in the Yoast SEO Title and Meta Description fields:

Token Output
%%geo_city%% Aix-En-Provence
%%geo_zip%% 13100
%%geo_zip_city%% 13100 – Aix-En-Provence
%%geo_description%% City description, HTML stripped (safe for meta)

They appear in the Yoast autocomplete dropdown under the + button, labelled Geo SEO: ….

Example Yoast setup:

SEO Title:
  Plombier à %%geo_city%% (%%geo_zip%%) - %%sitename%%

Meta Description:
  %%geo_description%% Devis gratuit, intervention rapide à %%geo_city%%.

Note: The Yoast live preview in the editor will show these tokens as empty — because they only resolve at request time when an actual geo URL is served. The rendered <title> and <meta name="description"> in the HTML source will be correct.


Default Location

In Geo SEO → All Locations, use the Default location selector to designate a fallback city. When set:

  • Pages served without a geo segment (plain /contact/) will use the default city's data in all shortcodes and Yoast tokens.
  • Visitors who land on an unrecognised geo slug (e.g. a mis-typed URL) will see the default city's content instead of a 404.

The default location is marked with a ★ star in the locations table.


Geo-Sticky Navigation

When a visitor is on a geo URL, nav menu links are automatically rewritten server-side to carry the geo prefix. No JavaScript is involved.

Original link Rewritten to (on /06000-nice/…)
/contact/ /06000-nice/contact/
/services/urgence/ /06000-nice/services/urgence/
https://other.com unchanged

For links outside the nav menu (breadcrumbs, content buttons, footers), use [geo_city_link] or [geo_locations_list] to generate correctly prefixed URLs in your templates.


SEO Best Practices

Canonical tags

The plugin automatically outputs <link rel="canonical"> pointing to the geo URL itself, signalling to Google that each geo URL is intentional — not a duplicate.

If you use Yoast SEO or Rank Math, their canonical output fires after priority 1. The plugin fires at priority 1 to ensure its canonical takes precedence. Alternatively, disable the SEO plugin's canonical for geo pages.

Geo meta tags

Automatically output on every geo page:

<meta name="geo.placename" content="Aix-En-Provence" />
<meta name="geo.region"    content="FR-13100" />

Sitemap

Geo URLs are not added to the default sitemap automatically. Options:

Yoast SEO:

add_filter( 'wpseo_sitemap_entry', function( $url, $type, $object ) {
    // append your geo URL variants here
    return $url;
}, 10, 3 );

Manual: create a dedicated geo-sitemap.xml and submit it in Google Search Console.

Internal linking

Create a "Zones d'intervention" page that links to every /{zip-city}/ variant — this is the primary signal for Google to discover and crawl them.

Avoiding thin content

Use [geo_if], [geo_description], and [geo_quartiers] to add meaningful, unique content to each geo variant. Pages that are 100% identical except for the URL will not rank well.


FAQ

Do I need a separate page for each city?
No. One page → unlimited geo variants. The geo segment is a query variable; shortcodes render city-specific content dynamically.

What if someone visits an unknown slug?
If a default location is configured, it's used. Otherwise, if you have any locations registered, unknown slugs return 404. If no locations are registered, any well-formed slug is accepted.

Does it work with Elementor / Divi / Bricks?
Yes — any builder that supports WordPress shortcodes can render these.

Does it work with WooCommerce?
The rewrite rules work for standard pages and category archives. WooCommerce shop/product URLs have their own rewrite stack and may need additional custom rules.

Will geo URLs be indexed by Google?
Yes, provided you: (1) have internal links pointing to each geo URL, (2) have submitted a sitemap containing them, and (3) are not blocking them in robots.txt.


Requirements

  • WordPress 5.8+
  • PHP 7.4+
  • Yoast SEO (optional, for %%geo_*%% variables)

Changelog

1.3.0

  • Fixed Yoast SEO variable replacement: added wpseo_replacements filter (layer 2) so %%geo_city%% and %%geo_zip%% actually resolve on the front-end. wpseo_register_extra_replacements now handles UI registration only, using __return_empty_string as a placeholder callback.
  • Fixed version mismatch between plugin header and GSL_VERSION constant.
  • Added GitHub-flavored README.md.

1.2.0

  • New shortcode [geo_description] — per-city free-text block with HTML support.
  • New include_self attribute on [geo_locations_list] (default: excluded).
  • [geo_quartiers] separator now works on all tag types; separator="\n" renders <br>.
  • Yoast SEO %%geo_city%%, %%geo_zip%%, %%geo_zip_city%%, %%geo_description%% variables.
  • Default location setting — fallback city for pages without a geo segment.

1.1.0

  • Dedicated top-level admin menu (Geo SEO) with All Locations, Add/Edit, and Shortcode Reference sub-pages.
  • New shortcode [geo_locations_list].
  • Geo-sticky navigation is PHP-only (wp_nav_menu_objects); JS layer removed.
  • Geo meta tags (geo.placename, geo.region) on all geo pages.
  • Fixed location list showing 0 entries (removed conflicting register_setting sanitize callback).

1.0.0

  • Initial release.
  • Rewrite rules for pages, paginated pages, and category archives.
  • Shortcodes: geo_city, geo_zip, geo_zip_city, geo_city_link, geo_quartiers, geo_if, geo_unless, geo_meta_title.
  • Admin settings page.
  • Canonical tag output.
  • Allow-list validation (404 for unknown slugs when list non-empty).

License

GPL v2 or later — see LICENSE.