WP Manifestindependent plugin directory
manifest / performance / localeyes-wp-plugin

LocalEyes Plugin

LocalEyes WordPress Plugin, with all versions.

by LocalEyes · github.com/luccas-localeyes/localeyes-wp-plugin

★ 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/luccas-localeyes/localeyes-wp-plugin/archive/refs/heads/main.zip

LocalEyes Plugin (WordPress)

LocalEyes is a WordPress control panel for site speed and structure. It scans PageSpeed Insights / Core Web Vitals, gives a dedicated TTFB analysis, builds native replacements for other plugins (copying their data and proving parity against the live page, then recommending you remove them yourself), provides a full SEO suite, surfaces GSC index hygiene, and keeps snapshot backups of everything it changes. It also keeps the original NAP find-and-replace engine as the Search & Replace tool.

It never deactivates or deletes any plugin. It copies functionality and recommends; you remove.

  • Plugin name: LocalEyes Plugin
  • Main file: localeyes-nap.php (loader) + includes/ modules
  • Version: 2.5.0-dev
  • Author: LocalEyes
  • Requires: WordPress 5.6+, PHP 7.0+
  • License: GPL-2.0-or-later
  • REST namespace: localeyes/v1 (v2 routes need manage_options; the NAP routes need edit_pages)
  • Full build plan: C:\Users\Capitao\.claude\plans\eager-yawning-tiger.md

Build status (v2.5.0-dev)

This is an in-progress rebuild delivered in phases. What is implemented and installable now:

Area Status
Modular loader + custom DB tables (wp_localeyes_*) Done
Real-time job engine (pause / continue / cancel, checkpointed) Done
PSI / Core Web Vitals client (mobile + desktop, full audits, no truncation) Done
Configure tab: PSI + GSC keys + admin App Password (show/hide, Save, Test, Refresh), Refresh Info, toggles Done
Overview: field + lab scores with the "LocalEyes can do this / partial / manual / theme" badges Done
TTFB: dedicated field-vs-lab analysis + prioritized actions Done
Speed tab: PSI scan (homepage / whole site) with live progress Done
Speed auto-fixes (hero, fonts, viewport, preconnect) with preview + Deploy + kill switch Built (untested)
Per-URL optimization exclusions (path prefix, per module) Built (untested)
Plugin replacement engine + duplication detector (never deactivates/deletes) Built (untested)
SEO suite (meta, canonical, robots, redirects, sitemap, robots.txt, llms.txt, schema) + Rank Math import Built (untested)
GSC index hygiene + builder page inventory (manual import or API token) Built (untested)
Thrive redirect helpers REST only (POST /thrive/redirects); no screen yet
LocalEyes-changes snapshot backups (REST only, no screen) Built (untested)
Site Scan: read-only crawler (sitemap + DB diff, redirect chains, noindex, canonicals, schema, builder/theme) with a live feed Built (untested)

"Built (untested)" means the module and its dashboard exist and are wired to the REST API, but have not yet been run on a live WordPress. All of it must be verified on staging before production.

As of v2.5.0-dev the static gates and the logic harness both run here: `.\tools\audit.ps1 -Php

` is clean (php -l, PHPStan level 5, duplicate declarations, route/DOM wiring, option read/write) and `.\tools\harness\run.ps1 -Php ` passes fourteen suites. That proves structure and the logic those suites cover; it still does not prove any feature behaves correctly against a real WordPress.

Linting is now a gate. Every PHP file must pass php -l before a change is considered done, and the pure helpers (font MIME/audit, @font-face patching, preconnect head rewriting, config parsing) have executable tests that run against real Lighthouse data. Structural checks alone are not enough: they cannot tell a compiling file from a correct one.

Phase 7 additions (built, untested)

Feature Where
Throttled background queue (Action Scheduler; WP-Cron fallback) includes/queue.php
API rate-limit status (waits, backs off, reports responding/not) includes/queue.php. The backoff applies to every queued API call; /api-status reports it and the Speed tab now prints it under the scan buttons
GSC OAuth 2.0 (Connect + auto token refresh) includes/seo/oauth.php, GSC tab
OAuth setup preflight (asks Google whether the redirect URI is registered, before you connect) includes/seo/oauth.php, /gsc/oauth/preflight, "Check setup" on the GSC tab
Snapshot backups (list/create/restore/download/delete) includes/backups.php, /snapshots/* REST routes
Real plugin reproductions: header/footer code (+analytics), SVG, Classic Editor, smooth scroll, post-types order, author box includes/replace/features/reproductions.php
Post-removal verification (baseline → recheck → restore + reinstall advice) includes/replace/engine.php, Plugins tab
Duplication vs theme/Rank Math/any plugin (exact tag) includes/duplication.php. The Plugins tab uses /duplication; the per-tag breakdown at /duplication/tags has no screen yet
Canonicals: redirected-pages finder + audit + bulk deploy includes/seo/canonical.php, GSC tab
Breadcrumbs (visible trail; BreadcrumbList JSON-LD is offered as a reviewable suggestion, never auto-output) includes/seo/breadcrumbs.php
Table of contents: heading levels, four insert positions, per-post overrides, working anchor jumps includes/seo/toc.php, includes/seo/toc-meta.php
Sitemap index + per-type; robots/llms/sitemap conflict detection, with one-click fixes for the safe ones (move a shadowing physical file aside, switch the WP core sitemap off through core's own wp_sitemaps_enabled filter). Both are reversible with Undo. Another plugin's sitemap is only ever reported, never switched off includes/seo/sitemap.php
Schema builders: FAQ + Article (plus Organization/LocalBusiness). Builders only propose a block for you to review; they never store or emit one includes/seo/schema.php
Pre-uninstall + on-deactivation snapshot; opt-in full purge on uninstall localeyes-nap.php

Phase 8 additions (built, untested)

Feature Where
Per-page SEO + Schema editor in any editor (Gutenberg sidebar, Classic meta box, front-end + Elementor/Thrive/WPBakery drawer) includes/seo/panel.php, assets/seo-panel.js, assets/seo-panel.css
Per-page inspector: identifies current SEO + schema from LocalEyes, Rank Math, Yoast, AIOSEO, SEOPress, plus an optional live-page fetch includes/seo/inspect.php (/seo/page-inspect)
Shared read-only per-post getters behind both the importers and the inspector le_read_rankmath_meta (meta.php), le_read_yoast/aioseo/seopress_meta + le_all_detected_meta (importers.php), le_read_rankmath_schema (schema.php)
Full per-page field parity: twitter description/image, additional keywords, breadcrumb title, max-snippet meta store + analysis.php + breadcrumbs.php

The panel writes to the existing LocalEyes SEO store and only renders on the live page once the SEO replacement is Deployed (Plugins tab), so it never double-emits with Rank Math. In page builders (Elementor/Thrive/WPBakery) it appears as a floating "LocalEyes SEO" launcher and drawer, since those editors do not show a WP meta box or Gutenberg sidebar.

Core Web Vitals

The plugin cannot promise your site passes. Hosting, theme, content and third-party scripts all move these numbers and none of them are LocalEyes's to control. What it does is measure honestly, fix specific defects, and make each fix prove itself.

The verdict (includes/cwv.php, Overview and Speed tabs) judges LCP, CLS and INP against the thresholds in le_cwv_thresholds() and states pass or fail per metric and overall. It is field first: real CrUX visitor data for the URL, falling back to origin-level field data, and only then to a lab run. The source is shown next to every number, because a lab pass is not a Core Web Vitals pass, and INP cannot be measured in a lab at all.

Fixes have to prove themselves. Every scan compares itself against the previous run for the same URL and strategy and reports the before/after delta for the three metrics and the performance score. If a fix changed nothing, it says so. POST /speed/verify returns that same comparison for a single fix and remains available, but no screen calls it: the Speed tab reads the delta that POST /psi/run already computes, so it costs no extra PageSpeed call.

One scan per refresh. The Speed tab has a single Refresh everything button. It runs PageSpeed once for mobile and desktop and repaints scores, Core Web Vitals, fonts and every issue row from that one scan, with the Live progress card showing each step and offering pause / continue / cancel. There are deliberately no per-row re-scan buttons: each one used to run a full scan of the whole page, so a list of 22 issues offered 22 identical 60-second scans that each spent a PageSpeed quota unit to re-measure the same page.

PageSpeed failures. The timeout is LE_PSI_TIMEOUT (120s, overridable by defining it earlier). Transient failures - a run that outlasts the timeout, Lighthouse's own "Something went wrong", a Google 5xx - get exactly one retry. A rate limit and a 403 refusal get none: the first spends quota that is already exhausted, and the second cannot succeed twice. Errors that survive the retry are rewritten to name the page and what to check, rather than quoting a cURL socket error at the user.

Fonts and the critical path

Fonts are the usual reason a WordPress page has a long critical request chain: without font-display, the browser blocks text rendering until the font arrives.

What Where
font-display on every @font-face (not just Google's) - patches local stylesheets that declare fonts without one. This is what takes fonts off the critical path includes/perf/font-display.php
Font audit - every font file with origin, size and format; flags families split across two origins and legacy .ttf/.otf with the woff2 saving. Report only includes/perf/font-audit.php
Capped font preload - preloads the first fonts the page requests, bounded (default 2). Preloading every font competes with the LCP image and makes the chain worse includes/autofix.php (fonts_preload)
Preconnect with the right crossorigin - a preconnect to a font origin without crossorigin opens the wrong kind of connection and the browser discards it. Also fixes tags hard-coded in the theme header, which wp_resource_hints cannot reach includes/autofix.php (preconnect)

LocalEyes cannot convert a .ttf to woff2 - there is no pure-PHP woff2 encoder. It flags the files and the saving; self-hosting from Google returns woff2, or your theme author can supply it. Reducing the number of font families is a design decision, so the audit reports and does not act.

Site Scan

A read-only crawler on its own tab. It reports; it never changes anything. Fixes stay on the Redirects, Canonicals, and per-page SEO screens, where each change gets reviewed individually.

Discovery reads the sitemap (auto-detecting LocalEyes', WP core's, or a plugin's, and following a sitemap index into its children) and enumerates published posts, then diffs the two. The diff is the point: URLs the sitemap advertises that no longer exist (orphan in sitemap), and published pages the sitemap never mentions (missing from sitemap). Known redirect sources are folded in too, so every redirect can be proven to still land somewhere sensible.

Per URL it records the full redirect chain with every hop and status code (with loop detection), the final HTTP status, the emitted canonical, effective noindex from all three sources (robots meta tag, X-Robots-Tag response header, and stored intent), every JSON-LD block actually on the page, the SEO fields, and which builder and theme produced it.

Live feed. Four workers crawl in parallel and each row appears the moment its own fetch returns, not batched at the end. Pause, continue, and cancel work mid-run, and a browser reload offers to resume from where it stopped rather than starting over.

Findings live in wp_localeyes_scan, grouped by scan_id so runs are comparable; the newest three are kept. Filter chips narrow to any single finding, and the whole run exports as CSV.

The crawler identifies itself with a LocalEyes-Scan user agent, and the 404 log and redirect hit counters ignore that traffic, so scanning never shows up in your reports as visits the site did not actually receive.

How schema (structured data) works

Nothing is applied automatically. LocalEyes never generates, adds, or publishes structured data on its own. There is no per-post-type default, no rule that attaches schema to pages you have not opened, and no background write. A page with nothing confirmed on it outputs no JSON-LD at all.

The flow is always the same:

  1. It pulls what is there. Open a page and the panel lists every schema block it can find: what is already confirmed on the page, what Rank Math holds for it, what the live HTML actually contains, and what LocalEyes could build for you. All of it read-only.
  2. You review and edit. Each block is separate, with its own JSON editor. Edit any of them, delete any of them, turn any of them off, or add new ones. Inserting from any source only opens the block in an editor; it stores nothing.
  3. You press Confirm. The block is validated and kept in the editor. It is still not saved.
  4. You press WordPress's Update button. That is what saves the page.

Per-page schema is stored in the _localeyes_schema post meta key, which is why WordPress's own Update button owns it (includes/seo/schema-meta.php). Site-wide and per-term blocks live in wp_localeyes_schema and are edited from the SEO to Schema tab; those screens have no Update button, so Confirm saves immediately there and the UI says so. The same applies inside Elementor, Thrive, and WPBakery, which do not save through WordPress's post form.

Staging Rank Math's schema in bulk (SEO to Import) copies each post's blocks onto that post turned off, so nothing is output until you open the page and confirm each one. Re-running it skips blocks that are already present.

Phase 13 additions (AEO personalization, built, untested)

Phase 12 shipped the AEO features with most of their behavior hardcoded. This makes all of it the site owner's to control, through one settings store (localeyes_aeo, deep-merged over le_aeo_defaults() in includes/aeo/settings.php, with ordered lists replaced rather than merged so deleting the third of three sections actually deletes it).

What is now editable Where
llms.txt build: manual or template mode; %pages% %posts% %cpts% %cpt:slug% %contact% %hours% %entity% %faq% %custom% %sitemap% on top of the existing le_seo_replace_vars() engine; per-section post type, heading, count, ordering and descriptions; description source order; custom Markdown sections; URL exclusions; per-page exclude and pin includes/aeo/llms.php
llms-full.txt: the same pages with their text inline, registered through the new le_files_registry filter so it is served and inspected like every other root file includes/aeo/llms.php, includes/seo/files.php
Accessibility rules: per-rule on/off, per-rule "may a fixer repair this", severity; URL and selector exclusions applied to BOTH the audit and the fixers; the denied-roles and embed-provider maps as editable line lists includes/aeo/a11y.php, includes/aeo/settings.php
Per-element overrides: pin the exact aria-label, title or alt for an element by selector, site-wide or per URL, applied before anything the fixers derive and reported back so the two never disagree le_aeo_overrides_for(), le_aeo_fix_override()
Fixer config: all six modules now expose real fields (icon-class naming, external-link prefix, placeholder vs name, fallback frame title, per-module selector exclusions) instead of four of them having none includes/aeo/fixes.php
WebMCP tools: rename and re-describe the six built-ins and pick which fields they return; the front-end script is descriptor-driven, so adding a tool never touches JavaScript includes/aeo/tools.php, assets/webmcp.js
Custom WebMCP tools: read-only tools backed by a post type, a page, a post meta or option, static text, or a shortcode, with a "Try it" preview and a public POST /aeo/tool resolver that only answers for configured tool ids includes/aeo/tools.php
Contact and hours override: field by field over Local SEO, so a site without Local SEO can still answer le_aeo_contact(), le_aeo_hours()
AI crawler rules: per-bot allow/deny for 15 crawlers with allow_all / answer_engines / block_all presets, written through the existing robots_txt filter includes/aeo/crawlers.php
Entity facts (the About block of llms.txt) and FAQ (written once, reused in llms.txt and the get_faq tool) includes/aeo/settings.php, includes/aeo/llms.php
Audit: max URLs, post types, URL exclusions, daily/weekly schedule via WP-Cron and the throttled queue, regression email, trend across runs, CSV export includes/aeo/audit.php
Per-page AEO in the existing Performance panel: agent summary (emitted as a meta tag and returned by get_page_content), exclude from llms.txt, pin, skip the audit, skip the fixers, and per-page element overrides includes/perf/overrides.php, assets/perf-panel.js

New AEO sub-tabs: AI crawlers, Live files (every root endpoint with its HTTP status and who answered, including missing for a 404, plus a custom-path checker), and Settings.

New routes: /aeo/settings (GET/POST, with reset), /aeo/maps, /aeo/llms/tokens, /aeo/tools/config, /aeo/tools/preview, /aeo/tool (public), /aeo/crawlers, /aeo/trend, /aeo/export. /aeo/llms now takes ?file=full.

Six new post meta keys (_localeyes_aeo_exclude_llms, _localeyes_aeo_llms_pin, _localeyes_aeo_skip_fixes, _localeyes_aeo_skip_audit, _localeyes_aeo_summary, _localeyes_aeo_overrides), all purged on opt-in uninstall.

Selector matcher gotcha, fixed and tested: attribute selectors are consumed before id and class, because a[href*="youtu.be"] otherwise reads .be as a class and never matches. An unparseable selector matches nothing rather than guessing.

Phase 12 additions (AEO tab, agentic browsing, built, untested)

Lighthouse 13.3 added an Agentic Browsing category and PageSpeed Insights inherited it. It reports a pass ratio, not a score, and covers four things: llms.txt, WebMCP, an agent-usable accessibility tree, and CLS. The new AEO tab addresses all four, with one honest limit: LocalEyes does not build an accessibility tree. A real tree needs a browser engine to compute roles and accessible names, and there is none here. What it does is apply the subset of the rules that tree is judged by to the HTML the server returns — so anything rendered by JavaScript is outside its reach. llms.txt moved here out of SEO > Tools.

Feature Where
Agent-accessibility rule engine: link-name, button-name, image-alt, input-image-alt, label, le-aria-role, aria-valid-attr-value, frame-title, document-title, html-has-lang. Regex over the served HTML, not a DOM. le-aria-role is named that way deliberately: it is a hand-written deny-list for nine tags, not axe's full aria-allowed-role matrix, and borrowing that id would claim a completeness it does not have. Each failure carries the failing element and a derived, concrete suggested fix includes/aeo/a11y.php
Site-wide audit: crawls every URL and applies those rules plus WebMCP form coverage, with the four-worker live feed, pause/continue/cancel and resume includes/aeo/audit.php, table wp_localeyes_agent
Lighthouse verdict: probes for category=AGENTIC_BROWSING once and caches the answer; falls back to the agent-relevant accessibility audits plus CLS that PSI already returns le_aeo_psi_verdict()
llms.txt: generator that includes descriptions, CPTs, posts and NAP; live validation of the three Lighthouse checks (H1, Markdown links, length) plus two of our own for the ways a file loses its H1 (a BOM, a --- above the first H2); a report naming the entries whose description fell back to raw page text; detects another plugin answering /llms.txt. Note Google Search does not read this file and has said so — it is published for the answer engines that do, and for the Agentic Browsing score includes/aeo/llms.php
WebMCP declarative forms: detects every form, stores per-form config keyed by a builder-stable signature, and injects toolname / tooldescription / toolparamdescription on output includes/aeo/webmcp.php, includes/aeo/fixes.php
WebMCP site tools: search_site, get_contact_info, get_business_hours, list_pages, get_page_content registered through document.modelContext ?? navigator.modelContext. That API is an origin trial, so on a browser that does not expose it the script no-ops and nothing is registered — which the server cannot detect, so AEO > What is working reports this row as unverifiable rather than as a pass. There is no MCP server, no JSON-RPC transport and no /.well-known discovery here: an agent that does not execute the page's JavaScript sees only the annotated form attributes assets/webmcp.js, route /aeo/search
Opt-in fixers: agent_link_names, agent_aria_roles, agent_form_labels, agent_frame_titles, webmcp_forms, webmcp_tools, all registered through localeyes_perf_registry so they inherit snapshots, kill switches and per-page overrides includes/aeo/fixes.php

All six fixers are off by default and share ONE output buffer. agent_aria_roles removes only the invalid role, leaving aria-expanded and aria-controls intact so the accordion keeps working.

New REST routes: /aeo/llms (GET/POST), /aeo/llms/generate, /aeo/llms/validate, /aeo/forms (GET/POST), /aeo/tools, /aeo/search (public), /aeo/discover, /aeo/url, /aeo/latest, /aeo/summary, /aeo/results, /aeo/check, /aeo/psi. The old /seo/llms* routes are gone.

le_is_own_crawler() now matches the whole LocalEyes- user-agent prefix rather than only LocalEyes-Scan, so the AEO audit and the live-page inspector stop counting as real traffic in the 404 log and redirect hit counters.

Phase 11 additions (per-post performance panel, built, untested)

The WP Rocket per-post options box, in the right-hand column of every editor, plus four things Rocket's box does not have. One renderer (assets/perf-panel.js) mounted three ways by includes/perf/panel.php: a block-editor PluginDocumentSettingPanel, a Classic side meta box, and a floating drawer inside Elementor / Thrive / WPBakery. Everything saves immediately over REST, independent of the post save.

Feature Where
Never cache this page (also raises DONOTCACHEPAGE for third-party caches) perf/overrides.php, perf/page-cache.php
Per-module checkboxes matching Rocket option for option; a module that is off site-wide renders greyed and disabled perf/overrides.php (le_perf_panel_labels, le_perf_not_page_scoped)
Cache status + purge/warm this page (extra) perf/page-cache.php (le_page_cache_url_status), routes perf/page/purge, perf/page/warm
Page-specific Critical Path CSS generated from this page's own permalink, with revert (extra) perf/critical-css.php (le_critical_build, le_critical_generate_page)
Page LCP hero image + skip lazy-load for the first N images (extra) includes/autofix.php (le_speed_hero_url, le_speed_lazy_skip_take)
Test this page with PageSpeed inline (extra) assets/perf-panel.js on the existing psi/run route

New REST routes (all localeyes/v1, all le_rest_perm + a per-post edit_post check): GET|POST /perf/overrides (extended payload), POST /perf/page/purge, POST /perf/page/warm, POST /perf/critical/page, POST /perf/critical/page/revert.

New post meta, all unregistered and underscore-prefixed, all purged on opt-in uninstall: _localeyes_perf_never_cache, _localeyes_perf_hero, _localeyes_perf_lazy_skip, _localeyes_perf_critical_css (alongside the existing _localeyes_perf_exclude).

The duplicate per-page performance checkbox list was removed from the SEO panel, so per-page performance now lives in exactly one place.

Phase 10 additions (WP Rocket parity, built, untested)

Completes the performance suite (Performance tab, includes/perf/) toward WP Rocket parity.

Feature Where
Add missing image dimensions (width/height, same-origin, cached) includes/perf/image-dimensions.php
Preload links (instant.page hover/touch prefetch) includes/perf/preload-links.php
Disable WordPress emojis includes/perf/disable-emojis.php
Minify: Excluded CSS/JS files includes/perf/minify-assets.php (exclude fields)
LazyLoad CSS background images + YouTube/bg toggles includes/perf/lazy-media.php
Delay JS Safe Mode (never delay internal scripts) + exclude includes/perf/delay-js.php
Defer JS user exclusion list autofix.php + registry.php (field bridged to the perf UI)
Cache lifespan (hourly expiry cron) + auto-preload after purge + preload exclude includes/perf/page-cache.php
Never-cache cookies / user agents / URLs + query-string whitelist (honored pre-WP via a drop-in sidecar config) includes/perf/page-cache.php
DB: All transients + Optimize tables includes/perf/dbclean.php
Clear used CSS action includes/perf/unused-css.php

All modules render generically on the Performance screen (config fields + Deploy/Save/Revert + action buttons), gated on their toggle, snapshotted on deploy, and paused while real WP Rocket is active.

Phase 9 additions (Rank Math parity gaps + live compare, built, untested)

Closes the audited gaps to Rank Math (free + pro) and adds a live-vs-LocalEyes compare with adopt.

Feature Where
Live compare + adopt per-page (per-field Adopt + Adopt-all + schema block Insert). Every one of these only fills the editor; you still Confirm and Save includes/seo/inspect.php, assets/seo-panel.js
Live compare (site-wide) live tags vs settings, adopt into settings includes/seo/compare.php, SEO to Live compare tab
Per-term (category/tag) SEO editor (term edit screen + front-end admin bar) includes/seo/panel.php
Bulk edit titles/descriptions/robots across posts (merge-patch, never wipes) includes/seo/bulk.php, SEO to Bulk edit tab
SERP + Facebook/Twitter snippet previews (live) assets/seo-panel.js
Primary category + pillar content; wired into %category% + breadcrumbs + link suggestions meta.php/settings.php/breadcrumbs.php/links.php
External-link options (nofollow / new tab / nofollow image links), global + per-post includes/seo/linkopts.php
Schema presets (offered as suggestions on matching pages, never auto-attached) + import schema from a URL includes/seo/templates.php
Export / import all SEO settings (secrets excluded) includes/seo/exporter.php
Role Manager (which capability may use LocalEyes) includes/seo/roles.php, helpers.php
Link Counter + Quick Edit columns in the posts list includes/seo/listtable.php
RSS feed injection (before/after each item) includes/seo/rss.php
.htaccess redirect sync (non-regex 301/302/307) includes/seo/redirects.php
Automatic internal linking (keyword map) includes/seo/links.php
Social-image watermark (GD) includes/seo/watermark.php
Google Trends link-out for the focus keyword; max-video-preview, [localeyes_map], Local hours UI panel + local.php

Fixed defects: regex redirect normalization; Google Instant Indexing now uses the GSC OAuth token (scope added); breadcrumb home-label/separator/prefix UI; noindex.empty_archive honored.

Migration flow (why the compare exists): all new front-end output stays behind the deploy gate, so before Deploy the live-compare shows Rank Math's tags and you Adopt them into LocalEyes; then Deploy and remove Rank Math. New DB table: wp_localeyes_schema_templates (created on version bump).

Action Scheduler note: the queue uses Action Scheduler when some other plugin on the site already provides it; otherwise it falls back to per-tick WP-Cron events (works, but fires on traffic). Bundling Action Scheduler with the plugin is recommended for reliable low-traffic processing.

Still to verify on staging: OAuth redirect round-trip, loopback fetch (needed by canonical/parity/post-removal), and that no screen fatals. The two-level sub-tab reorg is partial: new panels were added to existing screens (SEO, GSC, Plugins) rather than a full hash-routed sub-tab bar.

Phase 11 additions (retiring Rank Math and WP Rocket, built, untested)

Aimed at one question: can the other two plugins be deleted without losing anything.

Feature Where
Rank Math redirections importer. Its redirects live in its own table, so they were the one data set that died with the plugin. Handles all five match modes, expands a row's multiple sources into one rule each, and reports every rule it could not reproduce instead of dropping it includes/seo/importers.php (le_seo_import_rankmath_redirects), POST /seo/import-rankmath-redirects
Migration reconciliation. Counts what Rank Math holds against what LocalEyes holds for per-post meta, schema blocks and redirects. Database only, so it still works on a host that blocks loopback includes/seo/rankmath-ready.php (le_rm_audit), GET /seo/rankmath-audit
Rank Math readiness checklist, the SEO-side twin of the WP Rocket one. Per area: covered and verified, or the reason it is not. ready only when every core area passes includes/seo/rankmath-ready.php (le_rm_readiness), GET /seo/rankmath-readiness
Live verification, the only part that fetches: samples real pages and compares emitted title/description/canonical against the LocalEyes store, probes imported redirects for the status they claim, and fetches the sitemap le_rm_probe_run, POST /seo/rankmath-verify
WP Rocket reclassified. It was listed as keep / "never replaced" while perf/wp-rocket.php was a full checklist for retiring it, so the Plugins tab and the Performance tab contradicted each other includes/inventory.php

Two properties worth keeping:

Unknown is never counted as covered. Every area reports what was measured. "Not verified yet" is reported as not working, because a checklist that treats unknown as covered is how the Rocket screen once invited someone to delete a paid plugin on nginx.

The reporting path makes no HTTP requests. le_rm_audit, le_rm_verify and le_rm_readiness read a stored probe result; only le_rm_probe_run fetches, behind its own route. checkHotPathHttp in tools/contractcheck.js pins all three, and that guard was negative-controlled: moving a wp_remote_get into le_rm_verify makes it fail at the exact line.

Staged schema is not covered schema. le_schema_import_rankmath() stages every block disabled by design (the review-first contract in schema.php). A block that is staged emits nothing, so the checklist treats "imported but all still unconfirmed" as a blocker rather than a pass. Confirming blocks in bulk is still an open design question, because per-block review is the contract.

WP Rocket is judged by the checklist, never by a page diff. le_feature_hosts('perf') hosts no segments: a cached page and an uncached one are byte-identical by design, so there is nothing for the segment inventory or the baseline/recheck diff to see. The baseline route now refuses to store an empty signature, so a zero-segment feature cannot produce a vacuous "nothing was lost".

Not built, and not losses this covers: Rank Math's Content AI and its Analytics history; WP Rocket's Cloudflare/Varnish/Sucuri purge integrations, OPcache purge, and separate cache for logged-in users. On nginx, compression and browser cache headers still cannot verify at all, because they are .htaccess rules, so le_rocket_readiness() cannot report ready there.

Prerequisites

Enable the PageSpeed Insights API on your GCP project and paste the key in Configure. Add your GSC property + key and an admin Application Password there too. Testing happens on staging.

Files in this folder

File What it is
localeyes-nap.php Loader + the original NAP engine (Search & Replace).
includes/ v2 modules: bootstrap, helpers, db, jobs, psi, overview, ttfb, admin-ui, and (as they land) autofix, inventory, duplication, replace/, seo/ (including seo/schema-meta.php, the per-page schema post-meta store), backups.
assets/ Enqueued front-end/editor assets: seo-panel.js + seo-panel.css (the per-page SEO/schema panel). Must be included in the zip.
localeyes-plugin-<version>.zip Upload-ready package (contains localeyes-plugin/ with the loader + includes/). Generated by build.ps1; never hand-assembled. The folder inside is always localeyes-plugin/, whatever the file is called.
build.ps1 Sets the version, packages the zip, and verifies it before shipping. The only supported way to build.
README.md This document.

If you edit any source file, run .\build.ps1 -Version <x.y.z> before uploading (see Rebuilding the zip), otherwise WordPress installs the stale copy.

Note on the NAP defaults

The old hardcoded Austin/Chicago/Dallas find/replace pairs were removed in v2.0. Search & Replace now operates on current live content and loads/saves whatever pairs you enter (the localeyes_pairs option). "Load defaults" restores your saved set, not any baked-in address.

Why this exists

When a Google Business Profile address changes (for example after a suspension and move to a new Regus/office address), the old NAP is usually scattered across dozens of places on the site: page body, Elementor Theme Builder headers/footers, Thrive Architect content, Rank Math schema, and widget/theme-option text. A plain search-and-replace plugin misses the builder data and the schema, which is exactly where AI answer engines and Google read the address from. This tool covers all of those surfaces in one pass and lets you roll back if anything looks wrong.

This is the practical companion to the suspended-profile reinstatement work (Austin / Chicago / Dallas). No addresses ship with the plugin: you enter the pairs for the move you are doing and save them, and Search & Replace reloads them next time (see Saved find/replace pairs below).

What it scans and edits (coverage)

Surface Where it lives Routing token
Classic page/post body post_content classic
WPBakery body post_content (detected by [vc_ / _wpb_vc_js_status) bakery
Elementor (incl. header/footer Theme Builder templates) _elementor_data post meta elementor
Thrive Architect tve_* post meta thrive
Rank Math schema (per page) rank_math_* post meta schema
Widgets + theme options (site-wide) widget_*, theme_mods_* options widgets
SEO plugin settings (site-wide) rank_math*, wpseo* options schema_opts

Builder is auto-detected per page (elementor | thrive | bakery | classic). Nested/serialized option values are walked recursively so replacements preserve structure. Site-wide option groups (widgets, schema_opts) are only scanned when you are not scoped to specific pages.

Installing

  1. In WordPress admin: Plugins to Add New to Upload Plugin.
  2. Choose localeyes-plugin-<version>.zip and install, then Activate.
  3. Open it from the LocalEyes Plugin item in the left admin menu (map-pin icon).

Requires a user with the edit_pages capability (editors and admins).

Updating

Do not deactivate, and do not delete. Uploading over a live install is the supported update path:

  1. Build with a bumped version: .\build.ps1 -Version 2.6.0 (see Rebuilding the zip).
  2. Plugins to Add New to Upload Plugin, choose the new zip, Install Now.
  3. WordPress recognizes the existing copy and offers Replace current with uploaded, showing the current and new version side by side. Confirm it.

Settings, the 17 custom tables, snapshots and every stored module state survive untouched — the replace swaps files only, and no deactivation hook runs, so the drop-in and .htaccess blocks stay live throughout. Schema changes land by themselves: le_bootstrap_maybe_upgrade() runs on init after the files change, gated on LOCALEYES_DB_VER, so the first request after the update applies them whether it is a front-end hit or an admin one.

Two things make this reliable, and both are worth knowing about:

  • If a leftover folder would block the upload, includes/install-heal.php clears it during the install. It only ever acts when the uploaded package's own header says LocalEyes. See "Destination folder already exists" below.
  • Update URI: false in the plugin header keeps this plugin off the wordpress.org update channel. Without it, WP 5.8+ matches an installed plugin against the .org directory by folder slug, and this installs to plugins/localeyes-plugin/ — so whoever registered that slug on .org could have their code installed over this plugin on every site running it, silently wherever auto-updates are on. tools/contractcheck.js fails the build if that header goes missing.

There is no self-update checker: sites are not told a new version exists, and nothing phones home. Updating is always someone uploading a zip.

If you want the site to report which build it is running

GET /localeyes/v1/ping returns LOCALEYES_NAP_VERSION, and the Plugins screen shows the header version. Both are only as truthful as the version bump, which is why the build now owns it.

Using the admin UI

Four tabs, left to right:

  1. Wrong NAP - enter the wrong text to find (address, ZIP, phone, anything). Click Load defaults to preload the Austin/Chicago/Dallas pairs. Then Start scan.
  2. Pages & schema found - dry-run results with a live progress bar, per-page context snippets, and checkboxes to pick which pages / content types to change.
  3. Fix to apply - type the replacement for each wrong value and apply. Apply to selected pages uses your ticks from step 2; Apply to all matches hits everything found.
  4. Updated pages - what changed, with the backup ID for rollback.

Nothing is written until you apply in step 3. A replacement with an empty "replace with" value is rejected (the tool never blanks a value).

REST API

Namespace: localeyes/v1. The original NAP routes below require the edit_pages capability; all v2 control-panel routes (everything under seo/*, replace/*, psi/*, gsc/*, snapshots/*, etc.) require manage_options via le_rest_perm (configurable through the Role Manager).

Method Route Purpose
GET /ping Health check; reports version + whether Elementor / Rank Math are present.
GET /targets List scannable posts (id, title, type) for progress display.
POST /nap/preview Dry run. Returns hit count + hits with snippets. No writes.
POST /nap/apply Apply replacements, create a backup, flush caches.
POST /nap/rollback Restore a backup by backup_id.
GET /backups

This README is longer than the copy stored here. Read the rest on GitHub →