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

Geo SEO Locator

Wordpress plugin to add geo seo variables in URLS

by Axel SAGLIER · github.com/axsag/geo-seo-locator · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/axsag/geo-seo-locator/archive/refs/heads/main.zip

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.