Skip to content

feat(auto-redirect): browser-locale runtime + SEO override hreflang#29

Open
benjaminprojas wants to merge 1 commit into
masterfrom
brojas/geo-auto-redirect
Open

feat(auto-redirect): browser-locale runtime + SEO override hreflang#29
benjaminprojas wants to merge 1 commit into
masterfrom
brojas/geo-auto-redirect

Conversation

@benjaminprojas
Copy link
Copy Markdown

@benjaminprojas benjaminprojas commented May 21, 2026

Visitor-side runtime for the auto-redirect feature. Pairs with awesomemotive/universally#70 (dashboard, API, schema, plan gates) and awesomemotive/universally#69 (issue).

Summary

  • New AutoRedirect.phpwp_enqueue_scripts hook that, when the feature is enabled for the site and the current page isn't excluded, inlines a window.universallyAuto boot object and registers /assets/js/auto-redirect.js into <head> (not the footer — must run before paint). Boot config carries currentLang, mappings, enabled languages (for RFC 4647 fallback matching), basePath (current URL path with the language prefix stripped), beaconEndpoint, and the site's pk_ public API key.
  • New auto-redirect.js — browser-only matcher. Decision order: cookie short-circuit → already on a translated URL? → match navigator.languages against explicit mappings, then enabled target languages, with both exact and language-prefix passes. On a match: fires a fetch beacon with keepalive: true + credentials: 'omit', then location.replaces. Includes a BOT_RE skip path when cfg.skipBots is on.
  • language-switcher.js — emits data-prefix on each language anchor and writes the universally_auto_lang cookie (Max-Age=31536000; Path=/; SameSite=Lax, host-only) on click. Empty string = source language. The runtime treats this cookie as authoritative on future loads.
  • helpers.php — adds an override-hreflang block to universally_get_hreflang_tags() that emits an extra <link rel="alternate" hreflang="{sourceLocale}" href="{targetUrl}"> for each mapping with seoHreflang=true. Resolves URLs through the same $urlsByPrefix lookup the main loop builds. Adds the universally_get_auto_redirect_config() helper, backed by the 15-min site-config transient.
  • entry.php — busts the site-config / all-languages transients on visits to the Universally settings page so dashboard edits don't have to wait out the TTL.
  • universally.php — registers the AutoRedirect class on plugin boot.
  • Drive-by: LanguageSwitcher.php:240 — pre-existing preg_replace regex backreference bug ('/$1' produced //foo for paths like /en/foo). Fixed to '/'.

Review Context

  • Type: New Feature (paired with API/dashboard side in awesomemotive/universally#70)
  • Maturity: MVP / Initial — on by default for paid plans on the API side; plugin runtime stays a pure follower of whatever /connect/site-config reports and no-ops on enabled: false.
  • Risk: Low — pure additive plugin code that runs in the visitor's browser; no DB writes from the plugin side, no server-side state changes. The drive-by LanguageSwitcher.php fix is the only behavior change to existing functionality.

Safety properties worth a second look

  • Beacon uses credentials: 'omit' so it never carries cookies (no CSRF surface against the API endpoint).
  • Cookie write is host-only (no Domain attr), SameSite=Lax, not Secure because plugin runs on http+https sites — but cookie value is non-sensitive (URL prefix only).
  • Loop-defense: currentLang derives from both PHP-inlined value AND a path-derived fallback (currentLangFromPath), and the cookie/already-on-translated-URL short-circuits sit before the matcher. Multiple independent stops against /es/es/es/...-style redirect loops.
  • AutoRedirect::loadConfig() strips empty sourceLocale/targetUrlPrefix rows defensively, and lowercases both ends, so a mixed-case dashboard entry can't desync from currentLangFromPath() (which lowercases the path segment).

Test plan

Tested against an http://localhost WordPress install pointed at the staging API.

Manual happy path

  • Activate the plugin against a Universally site that has at least one enabled target language (e.g., pt-BR).
  • In the dashboard PR (universally#70), enable auto-redirect for the site and add an override pt-pt → pt-BR with SEO ON.
  • Open a private browser window with system language set to Portuguese (Portugal). Load the WP site root. Confirm: page redirects to /pt-br/ (or whatever the BR URL prefix is), one POST /connect/redirect-event fires (visible in the network panel before navigation; keepalive: true lets it survive the redirect), and a row appears in /platform/redirects ~1 minute later.
  • In the same browser, click the language switcher to pick a different language. Reload root. Confirm: cookie short-circuits, no redirect.
  • View source on a translated page: confirm <link rel="alternate" hreflang="pt-pt" href="…/pt-br/…"> appears alongside the standard hreflang block.

Edge cases

  • Bot user-agent (e.g., curl -A "Googlebot/2.1") does NOT trigger a redirect when skipBots: true (the runtime should early-return).
  • Visiting an excluded page (one configured in the WP plugin's exclude list) does NOT enqueue the runtime — confirm via "view source": no <script src=".../auto-redirect.js"> tag.
  • Visiting the wp-admin Universally settings page immediately after editing target languages in the dashboard reflects the new state (the entry.php transient-bust fires).
  • Disabling auto-redirect from the dashboard removes the runtime on the next page load (cached transient max 15 min — flush manually for instant repro by visiting the settings page).

Drive-by fix verification

  • On a site with currentLang='en', visiting /en/some-page no longer produces a //some-page artifact in the language switcher's path-strip logic. Old behavior: //some-page. New: /some-page.

Notes

  • Depends on awesomemotive/universally#70 being deployed to whatever environment the test site points at. The runtime no-ops when the API doesn't return enabled: true in /connect/site-config.
  • The runtime is in <head> (not footer) by design — auto-redirect must happen before paint.
  • No new PHP dependencies; no new JS dependencies; no Composer / npm install required.

…wesomemotive/universally#69)

Adds the visitor-side runtime for the auto-redirect feature whose dashboard +
API land in awesomemotive/universally#70. First-time visitors get sent to the
language version matching their browser locale; a switcher-set cookie wins on
subsequent visits.

  - AutoRedirect.php: wp_enqueue_scripts hook that inlines the boot config
    (window.universallyAuto) and registers /assets/js/auto-redirect.js into
    <head>. Skips when the feature is disabled, the page is excluded, or
    there's nothing to match against.
  - auto-redirect.js: browser-only matcher. Reads cookie → checks current
    URL prefix → matches navigator.languages against explicit mappings then
    enabled target languages (RFC 4647 fallback). fetch+keepalive beacon to
    /connect/redirect-event before navigating. credentials: 'omit', SameSite
    cookie writes only via the switcher.
  - language-switcher.js: emits data-prefix on each anchor and writes the
    `universally_auto_lang` cookie (1-year Max-Age, SameSite=Lax, host-only)
    when the user picks a language.
  - helpers.php: emits an extra <link rel="alternate" hreflang="…"> for each
    override mapping where seoHreflang=true. Resolves URLs through the same
    urlPrefix lookup used by the main hreflang block. Adds
    universally_get_auto_redirect_config() helper backed by the 15-min
    site-config transient.
  - entry.php: busts the site-config / all-languages transients on visits to
    the Universally settings page so dashboard edits reflect on the WP side
    without waiting out the TTL.
  - LanguageSwitcher.php: drive-by fix for a pre-existing regex
    backreference issue in the current-path strip ('/$1' → '/'), which was
    producing '//foo' for paths like '/en/foo'.
@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant