Changelog
Plugin release history. Updates install automatically inside WordPress.
3.16.0 2026-08-26
Deep-audit wave 2: the silent SEO deoptimizations. Everything here was output the site produced wrongly without ever reporting an error — wrong robots directives, unreachable sitemaps, wrong timestamps, duplicated schema, and readability scores that were noise on any non-English site.
Fixed
- Custom robots and canonical values on terms were never emitted. Two copies of the robots logic existed: the one that printed the tag knew only about singular posts, and the one that knew about terms was dead, because
init_hooks()removes core'swp_robotsaction. A category set tonoindexwas served asindex, follow. Both consumers now build from onebuild_robots_directives(). Terms can also carry a custom canonical. (includes/modules/meta/class-meta-frontend.php) - Per-post meta was read off post 0.
get_the_ID()returns 0 inwp_headon any theme that has not started the Loop, so custom robots, canonical and title values were silently discarded. Resolved viaget_current_post_id(). noindex_searchcould not be turned off — it read ametasettings group that does not exist, so the default always won. Moved totitles.noindex_searchwith a checkbox in the Titles tab. (Same defect previously fixed fornoindex_empty_archives.)- JSON-LD vanished on umlauts.
Schema_Type::get_description()truncated withsubstr(), cutting mid-sequence; the invalid UTF-8 madewp_json_encode()return false and the whole block disappeared. Nowmb_substr(). (includes/modules/schema/class-schema-type.php) - The sitemap index advertised URLs that 404. The Images provider counted posts with a featured image OR an inline image but selected only the former, so the index claimed pages that yielded nothing, and an empty page renders as a 404. Selection now matches the count, and an advertised page that still comes back empty serves a valid empty urlset. (
includes/modules/sitemap/providers/class-images-provider.php,class-sitemap.php,class-provider-abstract.php) - Every
<lastmod>carried the wrong instant. All callers pass UTC, butmysql2date()parses in the site timezone, so timestamps were off by the site's offset. Parsed as UTC and rendered withwp_date(), with a guard for empty and zero dates. (includes/modules/sitemap/class-provider-abstract.php) - IndexNow pinged around its own toggle. The sitemap module hooked publish and called
submit_url()directly, bypassing the auto-submit setting and the post-type filter — sites pinged on every publish while the UI showed the feature as off, and pinged twice once it was on. Submission now has one owner; the sitemap switch is the master gate it reads. A post carrying noindex is no longer announced. (class-sitemap.php,includes/modules/indexnow/class-index-now.php) - Saving a nested schema through REST destroyed it.
update_metahanded arrays tosanitize_text_field(), which returns an empty string. Sanitization is now per key: URLs throughesc_url_raw(), booleans as booleans, andschema_data/custom_schemasrecursively with their shape intact. (includes/rest/class-rest-controller.php) seo_generate_schemawas not idempotent. Each run keyed its result astype_<time()>and appended another copy, so the page emitted the same JSON-LD block once per run. The key is now the type; time-suffixed leftovers are swept on write. (includes/mcp/tools/class-schema-tools.php)- The homepage schema check never converged. It required a
localbusinesskey that is never generated — LocalBusiness comes from the Local SEO module at output time — so a homepage with a company address was reported as needing a fix on every run. (class-autopilot-schema.php,class-fix-tools.php) seo_duplicate_postbroke Elementor layouts.update_post_meta()unslashes its input, so copying_elementor_datastripped the backslashes out of its JSON and the duplicate opened empty. Copied meta now goes throughwp_slash(). (includes/mcp/tools/class-content-tools.php)- Readability was measured with English rules. Syllable counting deleted every character outside a-z (umlauts gone, Cyrillic reduced to zero syllables) and word counting used
str_word_count(), which split one German word into two. Syllables are now vowel groups for German and Italian with accents folded for English; reading ease uses Amstad (de) and Franchina & Vacca (it). (includes/modules/analysis/class-readability-analyzer.php) - The ASCII word count reached eighteen call sites — thin-content thresholds, audit checks, the GEO analyzer, and the
wordCountpublished in Article JSON-LD. All shareContent_Extractor::count_words()now.
Fixed (wave 4)
- The sitemap never excluded a noindex page and the audit never reported one. Four keys were in play for one concept: the editor and frontend use
_seowing_robots_meta, the sitemap providers excluded on_seowing_robots_noindex, and the audit engine read_seowing_robots— neither of the latter is ever written. Both checks answered "nothing found" because they were looking somewhere empty.Meta_Keys::robots()/is_noindex()own the question now, legacy keys are honoured as a read-time fallback, andSettings_Keys::META_ROBOTSno longer contradictsMeta_Keys::ROBOTS_META. - A third dead robots path, and
max-image-preview:large. Wave 2 unified the two copies inMeta_Frontend;Frontend::filter_robotswas a third, hooked towp_robots, which never runs because core's wp_head action is removed. Missing with it: core's ownmax-image-preview:large(absent from every SEOWing site since the tag was taken over), core's embed and search noindex, and this plugin's max-snippet and 404 noindex. The printed tag now runs throughwp_robots, so every consumer contributes and ours runs last. noindex_searchoff now means indexable. WordPress noindexes search results by default, so merely declining to add the directive left the toggle inert.
Added
- Nine schema types. Event, Recipe, VideoObject, Review, JobPosting, Course, SoftwareApplication, Person and Service. The picker offered seven types, so a site with a course, a vacancy or an event could not mark it up through this plugin at all. Each carries the properties Google documents as required and derives what it can (Recipe computes
totalTime; VideoObject falls back to the featured image for the required thumbnail; JobPosting, Course, Person and Service fall back to the site organization). Every type is generated on a real post and run throughseo_validate_schemain the test suite. seo_validate_schema— lints a page's JSON-LD before it ships. Anything could be written into a page's schema and it went into<head>unexamined; Google drops an invalid rich result without saying so, so the only signal was the absence of one. Checks Google's documented required properties per type, empty values, malformed URLs and@idreferences to nodes that do not exist, and stays quiet otherwise. Scoperead; the required-property map is filterable viaseowing/schema/required_properties.
Added (migration and announcement)
- Redirects survive a migration. Yoast Premium redirects (plain and regex) and Rank Math redirections are imported. Meta can be regenerated; a lost 301 cannot, and every old URL quietly starts answering 404. Re-running an import adds nothing. Rank Math's "contains" and "starts with" comparisons are skipped rather than approximated into a rule that would fire more broadly than the operator agreed to.
- TranslatePress and Weglot are used, not just detected.
get_plugin_translations()had no case for either, so those sites got no hreflang from their translations and their custom post types were skipped. Neither creates a post per language, so the translations are the published languages applied to the post's own path, default language unprefixed.
Fixed (multilingual and non-English content)
- A single hreflang conflict cost a page all of its hreflang. Tags come from the manual link table, WPML/Polylang, the self reference and x-default at once, and nothing reconciled them. Two entries claiming the same language with different URLs is a contradiction Google answers by ignoring the whole set. One URL per language now, first source wins, conflicts recorded and shown as an admin notice. (
includes/modules/international/class-international.php) - The GEO analyzer could not read non-English content. Its expert-quote pattern used
\u201c, which PCRE does not understand — the pattern failed to compile and matched nothing, ever. Named entities were matched with[A-Z][a-z],strtolower()left umlauts alone so brand mentions were missed, the topic check cut 500 bytes mid-sequence, and every structural marker was English-only, so a well-written German page scored zero on all of them. (includes/modules/geo/class-geo-analyzer.php)
Security (second pass)
/meta/bulkreturned any post's meta to any contributor. Its permission callback checksedit_postagainst anidparameter the route never sends, so the gate wasedit_posts. Authorization is per item now and the response reports how many were withheld. (includes/modules/meta/class-meta-rest.php)custom_metaaccepted any private key. "Starts with an underscore" was the whole rule, so an MCP write could repoint a featured image, replace a page's page-builder layout or plant an edit lock. Structural core keys and builder prefixes are refused (filterseowing/mcp/blocked_meta_keys).- Three copies of "what is the client's IP", all spoofable. The rate-limiter trait and the 404 monitor each believed whichever proxy header arrived first, so a client could be rate-limited as someone else and a 404 logged against any address they claimed. All three now use
MCP_Server::get_client_ip(), which trusts the header only when REMOTE_ADDR is in a trusted range. - The WP 7.0 Abilities surface ignored the license. Audit and analyze ran on an unlicensed or industry-blocked site while the notice said otherwise.
seo_get_site_infohandedadmin_emailto read-only credentials. It now appears only for one that may already read and write company settings.- The AI Execute loop had no write cap and no data boundary — see above.
Fixed (data integrity, second pass)
- Imagify never recognised an optimized image, and that cost quality. The optimization record is a PHP-serialized array; every query searched it for JSON (
"status":"optimized"), which serialized data does not contain. The statistics read zero on every site, andget_unoptimized_images()usedNOT LIKE— which matched everything — so a bulk run re-compressed images it had already compressed, losing quality each pass, silently. A scalar_seowing_imagify_statuskey is written and queried directly, and records written before this are still recognised by their serialized form so the first run after upgrading does not recompress the library. - Nothing stopped two optimizer runs meeting on the same file. Per-image lock, released in a
finally; aprocessingmarker left by a run that died goes stale after fifteen minutes instead of locking the image out for good. - Auto-setup snapshots could not be restored.
create_snapshot()has existed since 2.3.0 with nothing able to read it back — the reported snapshot id was good for nothing.seo_restore_snapshot(andseo_list_snapshots) put back exactly what was captured; mapped tosettings:write, so a read-scoped credential cannot call it. Snapshots moved from a transient, which an object cache may evict, to an option.
Fixed (settings that did nothing)
- SEMrush cache duration was a hard-coded property nothing wrote to; whatever the operator set, every response cached for 24 hours.
- WooCommerce could emit two Product nodes on a product whose schema type the editor had set. The integration stands down when a type is present.
- The Gemini key travelled in the query string, where it lands in access and proxy logs. Sent as
x-goog-api-key. - The AI connection test rendered the provider's reply with
innerHTML; built withtextContentnow. ajax_chathad no rate limit — admin-only, so not an access problem, but every turn costs the operator money and a stuck tab can fire in a loop. Bounded per user per minute (filterseowing/ai/chat_rate_limit).
Fixed (the setup wizard trapped the admin)
- The wizard could only be escaped by typing a company name. Whether it appeared was derived from "is the company name empty", and
mark_pending()runs from the activation hook — and a plugin update is an activation. A site that had skipped the wizard got every admin page hijacked again after every update. The decision is now recorded (filling it in and skipping are both answers), the redirect is consumed so it happens once instead of following the admin around, and a dismissible notice carries the invitation from there with a "Not now, stop asking" link. (includes/admin/class-setup-wizard.php)
Fixed (features that never worked)
- Norm numbers were always empty. The meta box saves
_norm_number; the admin column and the REST payload read_seowing_norm_number, which nothing writes. Every row showed a dash and the API field came back empty regardless of what the editor entered. (includes/modules/normen/class-normen.php) - Multi-location did nothing.
Local_SEO::get_schema( $location_id )accepted the id and ignored it, always returning the single business from settings. A location row now fills the node, gets its own@id(#location-{id}), points back at#organizationviaparentOrganization, and a non-primary site is typedDepartment. An unknown id returns nothing instead of the head office. - Locations were stored and never published. The locations table has been fillable since the module shipped and no branch ever reached a page, and the "multi location" setting meant to govern that was read by no code at all. With it on, each location is emitted as its own node under the organization.
- PageRank could time out and had no lock. Two overlapping runs read the same graph and wrote the same meta; on a site large enough for that to matter the request also outlived the PHP time limit and the operator saw a dead page. There is a lock, a weekly background pass, and above 2000 posts (filterable) the button hands the work to cron and says so.
- Glossary tooltips could not work. The frontend script has always requested
seowing_get_term; no handler was ever registered, so the request failed and the tooltip stayed empty. Registered, including for logged-out visitors. stringify_result()handed the AI provider invalid UTF-8, truncating withsubstr()mid-sequence on umlauts.
Removed
includes/modules/sitemap/class-index-now.php— a dead duplicate of the IndexNow module, never instantiated, that logged its key file's full path (key included) intodebug.log.Frontend::output_backlink()— a hardcoded goldenwing.at link for client sites. Its hook was already gone; the method was not. With it, the autopilot'sbacklinksection, which set an option nothing reads and then reported "fixed: 1" for work that had no effect.Site_Auditor::calculate_category_score(), and the emptyelementor-optimizer/views 2/iCloud conflict directory.- Not removed, though the audit lists them as dead:
get_depth_distribution(),filter_breadcrumbs(),AI::handle_save(),ping_search_engines(). All four are wired — to a view, a filter,admin_post, and AJAX.
Performance
- IndexNow fired a blocking HTTP request per publish. A bulk edit of fifty posts made fifty of them while the editor waited, and republishing a page announced it again each time. URLs are queued, a burst debounces into one batch, each URL has a day-long cooldown, and batches are chunked to the 10,000-URL limit. (
includes/modules/indexnow/class-index-now.php) MCP_Key_Store::touch()wrote a row on every authenticated request.last_used_atserves an operator checking whether a token is in use and the legacy-key migration checking one is warm; neither needs minute precision. Coalesced to one write per token per hour.- OAuth token validation built a whole class instance per request.
validate_access_token()did( new self() )->find_token(); that constructor registers four hooks and toucheswp_next_scheduled()/wp_schedule_event(). Lookup is static now. (includes/mcp/class-mcp-oauth.php) - Flaky 401s on concurrent token issuance. The token index was one site-wide option array with read-modify-write on both writers and no locking, so two tokens issued at the same moment lost one entry and the losing token answered 401 despite being valid. Each hash now owns its option row; the legacy array is still read and drains as tokens rotate.
- The MCP tool registry was built on every request. Registering it autoloads thirteen provider files and constructs ~60 tool definitions with full input schemas — on
init, for every front-end page view, for a surface only reachable over REST and admin AJAX. Admin, cron and CLI stay eager; elsewhere the first genuine use pulls it in. (includes/mcp/class-mcp-server.php) - Sitemap rendering was N+1 twice over. Both post providers queried with
fields => 'ids', which skips cache priming, then spent one query onget_post()and two on exclusion meta per post — thousands of queries per sitemap page. Asking for the objects primes posts and postmeta in two queries. - The internal-links graph did not scale.
url_to_postid()ran once per link (a full rewrite-match plus a query, tens of thousands of times), and membership wasin_array()— a linear scan of every post for every link. Resolution is memoized, membership is a hash lookup, content is read in chunks. (includes/modules/internal-links/class-internal-links.php) - Next-gen image lookups are cached across requests where a persistent object cache exists. (
includes/modules/imagify/class-image-serving.php)
Security
- Stored secrets were encrypted but not authenticated. AES-256-CBC with no tag is malleable: anyone able to write to the options table could flip bits in a stored API key and the plugin would decrypt the result without noticing. Everything written from now on uses AES-256-GCM, or encrypt-then-MAC where GCM is unavailable. Existing values still decrypt and are rewritten in an authenticated format on the next save. (
includes/class-encryption.php) - The AI Execute loop had no per-run write cap.
apply_plan()has capped writes at 25 since 3.2.0; Execute, the path a chat message takes, had none and could keep writing for as long as the step budget allowed. It now counts against the same cap (filterseowing/ai/run_write_cap) and tells the model to stop and ask. - Tool output is fenced as data. Read tools return page content, settings and search results the agent does not control, and it went into the conversation bare — an instruction sitting in a page read exactly like one from the operator. Results are fenced, a fence marker inside the payload is neutralized so content cannot close its own block, and the system prompt states that fenced text is never an instruction. (
includes/ai/class-ai-agent.php) - Exception messages leaked to MCP callers.
$e->getMessage()went straight into the 500 response and the JSON-RPC error text; those messages carry absolute paths, SQL fragments and occasionally credentials, and the endpoint is reachable by any holder of a low-privilege token. The detail now goes to the error log and the caller gets the tool name and a stable code (the detail stays on aWP_DEBUGinstall). (includes/mcp/class-mcp-server.php) - Per-site MCP tokens can expire. They carried no expiry at all, so a leaked token stayed valid until someone revoked it by hand. Expiry is opt-in at creation (30 / 90 / 365 days) and never retroactive: existing tokens, and new ones left on "Never", do not expire — a token that stops working on its own would break a customer's automation at a moment nobody chose. An expired token is treated as revoked at lookup and shown as "expired" in the token list. Adds
expires_atto{prefix}seowing_mcp_keys(DB version 1.4.0); the reads tolerate the column not being there yet, sincecreate_tables()verifies tables but not columns. (includes/mcp/class-mcp-key-store.php) - Deleting content is its own MCP scope.
seo_delete_post,seo_delete_page_schemaandseo_delete_redirectsat undercontent:write, so a token allowed to fix meta could also remove posts. They requirecontent:deletenow, offered in the token UI. Wildcard credentials and the legacy shared key are unaffected; a token deliberately scoped tocontent:writeloses deletion, as do OAuth connector tokens (restorable viaseowing/oauth/token_scopes). Breaking for scoped tokens that delete. Local_Enginedecoded images of any size. A 48-megapixel photo expands to roughly 190 MB in GD, past the memory limit on most shared hosting — and a PHP out-of-memory is a fatal, not an exception, so the surrounding try/catch never saw it and one oversized upload killed an entire bulk run. It now measures against the memory actually left before decoding, with a filterable pixel ceiling; the WebP and AVIF converters ask the same before their own decode. (includes/modules/imagify/class-local-engine.php)- The MCP license gate ran before authentication, so any anonymous request could read this site's license state off the 402, and an unlicensed site answered OAuth discovery with a 402 instead of the 401 challenge a client needs to start the authorization flow. Authentication moved into its own method; the gate runs after it. (
includes/mcp/class-mcp-server.php)
Changed
- One error contract for MCP tools. 118 call sites used
MCP_Tool_Base::error(), which returnserroras an object withcodeandmessage; sixteen returned it as a bare string, so a client readingerror.messagegot nothing from those tools. The sixteen now use the helper, andnormalize_tool_result()enforces the shape at the boundary as well — including results coming from theseowing/mcp/handlefilter. Successful results are untouched, so no existing integration's contract changes. - A failing tool was reported to the AI model as a step that had worked.
run_tool()answers its own pre-flight failures with aWP_Errorwhile the tools answer with an array, and the agent decidedokfromis_wp_error()alone.MCP_Server::result_failed()covers both. (includes/ai/class-ai-agent.php) - The MCP extension point is reachable.
required_scope()returned null for anything outside the compile-time map andallows()treats null as deny, so every tool added viaregister_tool()or the tools filter was denied to every credential. A tool may now declare its scope (ascopekey, or theseowing/mcp/required_scopefilter). Default-deny is unchanged: a tool that declares nothing stays denied, a declaration cannot claimself_update, and it cannot relax a built-in mapping. (includes/mcp/class-mcp-scopes.php) - The Guardian module is tracked in git. It shipped inside every ZIP since 3.14.0 but existed only in one working tree. Its wiring came with it: the drop-in installs on activation, is removed on deactivation, and on uninstall the data dir goes too — a drop-in must never outlive the plugin it calls into.
- Admin spacing for older templates. The utility reset zeroes margins so token-driven screens control rhythm via gap; templates that use plain
<h2>/<p>collapsed to none. Restored via:not([class]).
Tests
MetaRobotsTest,ReadabilityLanguageTest, and two additions toMCPScopesTestdrive the real methods with WordPress stubbed. Pure suite: 98 tests, 313 assertions.
3.15.0 2026-08-21
Freemium entitlements: the plugin now enforces the public pricing split, so the key-free download unlocks the SEO core while automation, image optimization and the AI/MCP stack require a license.
Added
- Server-driven entitlements in the License client. The license status now stores the server's
featuresarray;License::features()andLicense::can( $feature )resolve what this site may use. Without a license: the free core (audit, meta, schema, sitemap, redirects, onpage). With a license: whatever the server grants (Starter/Trial add autopilot, weekly audit, images, updates; Pro/Studio add internal links, GEO, MCP, AI). A cached status without a features array unlocks everything, so paying customers are never locked out by this update. Escape hatches unchanged (SEOWING_LICENSE_ENFORCE,seowing/license/enforce); new filterseowing/license/features. (includes/license/class-license.php)
Changed
- Tier-correct gates. MCP (
class-mcp-server.php) and the AI assistant (class-ai.php) now check their specific entitlement (mcp,ai_assistant) instead of the binary premium flag, so Starter/Trial licenses no longer unlock them — matching the pricing table. Dashboard connector badge follows. - Weekly scheduled audit bails without the
weekly_auditentitlement; the manual audit stays free. The event stays scheduled so automation resumes on activation. (class-audit-engine.php) - Image optimization (upload auto-optimize, single and bulk AJAX) requires the
imagesentitlement. Restore, stats and settings stay available — existing media is never held hostage. (modules/imagify/class-imagify.php) - Internal link suggestions (analyze, suggestions, PageRank AJAX) require
internal_links; existing links in content are untouched. (modules/internal-links/class-internal-links.php) - GEO/AEO analysis (AJAX + auto-analyze on save) requires
geo; stored results stay readable. (modules/geo/class-geo-analyzer.php)
3.14.2 2026-08-21
Fixed
- MCP OAuth: consent screen "Allow" no longer fails with
rest_cookie_invalid_nonce(403). The consent form posted its CSRF nonce in the default_wpnoncefield; on REST routes WordPress core intercepts any_wpnonceand verifies it against thewp_restaction, rejecting the whole request before the authorize callback runs. The nonce field is now namedseowing_consent_nonce, so core ignores it andcheck_admin_referer()keeps the CSRF protection intact. Regression from the security-audit consent screen; it broke every Claude-connector authorization since. Same core trap as the v3.5.4 login-loop fix, this time on the POST path. (includes/mcp/class-mcp-oauth.php)
3.14.1 2026-08-14
Deep-audit fixes, wave 1 — security and data-integrity blockers found in the full per-subsystem code review (DEEP-ANALYSIS-2026-08-14.md).
Fixed
- Security (AI agent): the tool blocklist is now enforced at execution. The agent loop called
run_tool()directly, so a filter that putseo_self_updateorseo_delete_postback into the advertised set could execute it. The blocked tools are now hard-rejected right before execution, not only hidden from the model. (includes/ai/class-ai-agent.php) - Data loss (Autopilot):
dry_runno longer writes to the database. A preview run ofseo_auto_setupwas persisting auto-detected company/social data because missing-info collection was always called withsave=true. Preview now collects without saving. (includes/mcp/tools/class-autopilot-tools.php) - Data loss (Autopilot):
auto_setupno longer activates every module. The module-activation step ignored its section→module map and enabled every inactive module — including deprecated and unverified ones — on a live site. It now activates only the modules the requested sections need. (includes/mcp/tools/class-site-configurator.php) - Fatal:
seo_update_term_metawas broken on every call — it used the non-existentmodules->get()instead ofget_module(). (includes/mcp/tools/class-meta-tools.php) - REST settings save now reports real success/failure.
POST /settingsalways returnedsuccess:true, hiding an unknown group or a submission whose field names did not match the group schema (a silent no-op). It now returns a proper error, which was the root cause of "saved successfully but the value did not persist." (includes/rest/class-rest-controller.php) seo_get_page_structureread the wrong meta keys (_seowing_title/_seowing_description/_seowing_robots) and therefore reported every page as missing its meta title/description. Now reads the canonical_seowing_meta_title/_seowing_meta_description/_seowing_robots_meta. (includes/mcp/tools/class-elementor-tools.php)- Settings export no longer leaks secrets. The JSON export bundled
mcp.api_key(and any other*api_key*/*secret*/*password*field) in plaintext; those fields are now stripped from the export. (includes/admin/class-tools-handler.php) - Schema: the LocalBusiness node now carries the
#organization@id. In non-graph mode the output buffer keeps the most specific business type and drops the plain Organization; without a shared @id that left every#organizationreference (WebSite.publisher, breadcrumb, WebPage.mainEntity) dangling. (includes/modules/local-seo/class-local-seo.php) - Redirects: loop protection + regex validation. A rule pointing a URL at itself (directly or via a regex replacement) no longer redirects (which would loop forever), and a user-supplied regex that does not compile is now rejected on save instead of erroring via
preg_matchon every front-end request. (includes/modules/redirects/class-redirects.php,class-redirect-repository.php) - Cache: stop full-cache flushes on every save (WP Fastest Cache / WP Super Cache). Their
purge_url()fell back topurge_all()for any URL it could not map to a post ID; since the cache bridge purges every archive URL on each post save, that wiped the whole cache many times per save. Only the site home now triggers a full flush. (includes/integrations/cache/class-wp-fastest-cache-adapter.php,class-wp-super-cache-adapter.php)
Deferred (needs staging verification, not shipped in this patch)
- Imagify statistics / bulk selection (D4). Optimization data is stored as a PHP-serialized array but queried with JSON
LIKE '%"status":"optimized"%', which never matches — so counters read 0 and bulk re-plans the whole media library (risking repeated recompression). The fix (a scalar_seowing_imagify_statusmeta key + rewritten count/selection queries + a one-time backfill of existing attachments) changes data-query behaviour and must be verified on staging before release; it is intentionally NOT included here. SeeDEEP-ANALYSIS-2026-08-14.md§1B/2.9.
3.14.0 2026-07-20
Adds Guardian — automatic recovery from another plugin's "critical error".
Added
- Guardian (Auto-Recovery) module. Installs a self-contained
fatal-error-handler.phpdrop-in (plus a must-use loader) that catches a PHP fatal caused by another plugin — typically right after a bad update or a broken install — and heals the site automatically so it stays online for every visitor: - Broken update → rollback. Before every plugin update/install Guardian snapshots the current, working files. If the new version fatals, it restores the snapshot (the broken version is quarantined, not destroyed). - No snapshot → deactivate. A broken fresh install is deactivated — only the plugin in the fatal's stack trace, nothing else. - Loop protection. After repeated fatals of the same plugin within 5 minutes it stops and shows the normal WordPress error page — it can never trap a site in a reload loop. - Self-protection. Guardian never rolls back or deactivates SEOWing itself. - Admin e-mail + a SEOWing → Guardian panel listing recovery events and snapshots, with one-click "reactivate" / "delete snapshot" actions. - Good-citizen install: it will not overwrite a foreignfatal-error-handler.phpalready present; it surfaces a notice and installs only on explicit confirm. - Runs even when SEOWing has not booted (drop-in + mu-plugin), and is removed automatically on deactivate/uninstall so it never outlives the plugin.
3.13.0 2026-07-19
Security-hardening + feature release. Fixes every finding from an adversarially verified security audit, closes the plugin self-tampering vector, and adds a visible per-page SEO score, AI-driven plugin management, and a safe file browser.
Security
- OAuth authorize now requires an interactive, nonce-protected consent screen; authorization codes are only issued on POST after the admin confirms, closing a cross-site connector-takeover vector.
seo_upload_fileno longer accepts.html, and file-writing tools moved to an explicitfiles:writescope that the*wildcard cannot reach (stored-XSS).- Object-level authorization (
edit_post) on the analysis, GEO and Gutenberg REST routes, fixing IDOR where a contributor could act on others' posts. - Industry scanner is gated behind an active license / explicit opt-in; CSV formula-injection is neutralised in the redirects export.
- Encryption is fail-closed (no derivable fallback key, no silent plaintext); MCP rate limiting is keyed per credential; audit-log parameters are redacted;
/healthreturns only a status to unauthenticated callers.
Changed
- Plugin self-protection:
seo_self_updateno longer installs a caller-supplied ZIP. It installs only the official, signed, license-gated release from the update server (verified against the official host), so the plugin can no longer be replaced with arbitrary code over the MCP connection.
Added
- Per-page SEO score (RankMath-style): a unified 0–100 scoring engine with a traffic-light meta box and a sortable "SEO Score" admin column. Editor, admin column and the
seo_calculate_scoretool all report the same score. - Plugin management tools (
seo_list_plugins,seo_activate_plugin,seo_deactivate_plugin) with hard SEOWing self-protection, aplugins:writescope and aSEOWING_ALLOW_PLUGIN_MGMTopt-in. - Safe file browser (
seo_fs_list,seo_fs_read,seo_fs_write): read-only browsing confined to the WordPress root withwp-config.php/.htaccess/.envblocked; writes confined to a non-executable sandbox with an inert-extension allowlist (no.php/.html/.js) and no delete, behind afiles:writescope and aSEOWING_ALLOW_FILE_MANAGERopt-in.
3.12.1 2026-06-08
Honest-audit + WordPress 7.0 release. The site-audit score now reflects the whole site truthfully, and SEOWing is verified on the new WordPress 7.0 core (including its built-in AI). Verified end-to-end on a real WordPress 7.0 install (plugin activates, audit runs, admin renders, abilities + AI provider work); the deterministic unit suite (84 tests) stays green. No data or settings changes.
Changed
- Honest audit score. The headline SEO score is now an evidence-weighted average: the per-page section counts in proportion to how many pages it covers, so a problem spanning most of the site can no longer be averaged away by a row of small configuration checks that happen to pass. Categories that do not apply to the site (e.g. legal checks on a non-DACH site, local-SEO on a non-local business) are excluded instead of counting as a perfect 100. A genuinely clean site still scores 100.
- Whole-site coverage, no sampling. The score now analyses every published page of every public post type (including custom types), not just the first 100. The audit page states exactly what it scanned (e.g. "Scanned every published page: 65 — Pages 12 · Posts 45 · Products 8").
- The audit page now shows, next to the score, the share of scanned pages that carry an open finding, split into critical (missing meta title/description) and minor (keyword/schema/image) — so a high score cannot hide a half-built site.
- The on-page "SEO Scores" check no longer reports pages as "not analyzed" that the same audit run just scored; per-page scores are persisted during the run so the figures agree.
Added
- WordPress 7.0 support, verified on a real WordPress 7.0 install (PHP 8.5/8.3).
- WordPress 7.0 Abilities API: the site audit is registered as a native, read-only ability (
seowing/run-audit), so the WordPress AI tooling and AI agents can run a SEOWing audit through the standard interface. No-op on older WordPress. - WordPress 7.0 AI Client provider: the in-admin AI Assistant can now use the AI provider configured in WordPress core (Settings → Connectors) with no separate SEOWing API key. Text generation only for now (the tool-driven agent still needs an OpenAI/Anthropic/Gemini key); the option only appears on WordPress 7.0+.
3.12.0 2026-06-04
Audit hardening release. Stacked fix waves (autoloader/defaults, compliance, settings sanitization + merge, sitemap unification, UX, and the settings-tab wiring), all deterministically verified (pure unit suite 74 tests green) and consolidated on release/3.12.0. Source behaviour and customer data are preserved throughout; the settings, sitemap and schema-output changes are the most central and require the live smoke test in SMOKE-3.12.0-FULL.md before rollout.
Fixed
- Autoloader root fix: the filesystem class loader mangled directory names with internal capitals (e.g. IndexNow, SEMrush), so those modules silently failed to load. The loader now resolves the directory by trying the hyphenated and the collapsed segment, with regression tests. This restores IndexNow and SEMrush.
- Duplicate default-module list removed; defaults now derive from a single source (Module_Manager::get_default_active_modules), seeded at install.
- Settings sanitization: Settings::validate_group() was a no-op, allowing raw values (stored-XSS round-trips) through every write path. It is now a real schema-driven sanitizer (per-field by type: bool / int / url / email / text / recursive array) running at the single save chokepoint, so the admin form, AJAX, REST and the MCP tools all sanitize on write. Title placeholders like %%title%% and legitimate URLs survive.
- Settings partial-write data loss: save_group_settings() merged partial writes with the DEFAULTS instead of the stored values, so writing one field of a group reset every other field of that group to its default. It now merges submitted fields onto the stored values (precedence default < stored < submitted); associative maps merge per key, list arrays and scalars are replaced wholesale, and an explicitly submitted '' / false / [] still overwrites. 16 regression tests.
- Sitemap double-store unified: the settings tab and MCP wrote a dead seowing_sitemap option the sitemap module never read, while the module read seowing_module_settings['sitemap']. All read/write paths are now aliased onto that one canonical module store with a single shared schema; the tab field names were aligned and unchecked boxes now post an explicit value so toggles work under the merge. A one-time, idempotent migration imports any canonical-named key from the legacy store (canonical store wins on conflict). 10 regression tests.
- Duplicate SEO meta box removed: a legacy, pre-rebrand SEO meta box rendered on every post-edit screen alongside the modern Meta-module box (both titled "SEOWing - SEO Settings"). The legacy box is gone; the sw-styled Meta-module box (a superset of fields, writing the same canonical _seowing_* keys) is the single box. No save path or field was changed.
- Settings tabs were not actually persisting: several tabs posted field names that did not match the group schema (the save whitelist dropped them) or the keys the consumer reads, so toggles "snapped back" and edits had no effect. Each tab was wired to the canonical keys the consumer reads (Weg B): - schema: the tab now controls the real JSON-LD output (enable_website, organization_schema, breadcrumb_schema, enable_search_action); the per-post meta box reads the same keys; default output is unchanged. Verified live on staging (toggling breadcrumb removes/restores the BreadcrumbList JSON-LD). - titles: fields renamed to the title-engine keys (post_title, page_title, and the real category/tag/author/date archive titles) with the engine's actual placeholders; noindex_empty_archives now reads from the titles group (it read a non-existent 'meta' group and was permanently on). - mcp / local / eeat / company: missing hidden-input pattern added so a checkbox can be turned off (under the new merge an unsubmitted box kept its stored value); eeat Specializations now persist as the canonical array; company per-day "closed" hours persist.
- Every settings checkbox now uses the hidden-input pattern so unchecking reliably saves false.
- Build packaging (rollout-critical): the release ZIP had no top-level seowing/ folder, so a WordPress plugin update created a second install (seowing-1/) instead of replacing the active plugin in place. The ZIP is now wrapped in a single seowing/ folder (with a build guard), so updates replace cleanly. Also excludes the WP-independent test config and run artifacts from the package.
Changed
- GDPR (DE/AT): IBM Plex is now bundled locally (14 woff2 + OFL license) and the admin no longer loads fonts from fonts.googleapis.com / fonts.gstatic.com, so no visitor/admin IP is sent to Google without consent.
- GDPR: the 404 monitor anonymizes IP addresses by default (IPv4 last octet zeroed, IPv6 truncated to 48 bits), with a toggle and a data-protection note. Existing logs age out via the retention period or the "clear logs" button (no silent backfill).
- Conservative default-module backfill: existing sites get only the autoloader-invisible IndexNow module auto-enabled; everything else is offered via a dismissible admin notice rather than force-activated (a flat module list cannot distinguish "never enabled due to the bug" from "deliberately off").
- Customer-facing UI no longer shows raw MCP tool names (e.g. seo_bulk_meta()) on the dashboard and audit screens; a central tool-name to human-label map is used for display. The tool names in the MCP registration and API are unchanged for developers and the connector.
- General settings: removed three dead toggles (auto-generate meta descriptions, remove duplicate tags, add canonical) that never persisted and whose features run unconditionally anyway; the behaviour is unchanged, the misleading switches are gone.
- Company settings: the Logo field's upload button now opens the WordPress media library and writes the chosen image URL into the field (it previously did nothing).
3.11.1 2026-06-04
Changed
- Internal refactor of the AI Assistant admin page. The 1,178-line markup block (HTML, CSS and JavaScript) was moved out of
AI::render()into a dedicated view fileincludes/ai/views/assistant-page.php.class-ai.phpshrinks from 1,780 to 623 lines. No behaviour change: the page renders identically (verified with a render harness in both unconfigured and configured states).
3.11.0 2026-06-04
Changed
- License enforcement is now ON by default. SEOWing is a licensed product, so a site without an active license must add one to use the premium features (AI assistant, MCP / Claude connector, autopilot writes and updates). Existing SEO output (meta, schema, sitemaps) is left in place, and a 14-day grace plus an API-outage grace ensure a paying customer is never cut off by a transient problem. Sites that were already licensed are unaffected.
- Escape hatch for own/staging installs: define SEOWING_LICENSE_ENFORCE as false (or set the seowing_license_enforce option / seowing/license/enforce filter to false) to keep enforcement off on a specific site.
3.10.0 2026-06-03
Self-hosted image optimization. SEOWing no longer depends on the Imagify cloud: it compresses and serves next-gen images on your own server.
Added
- Local compression engine (new Local_Engine): JPEG/PNG re-encode, downscale to max dimensions and metadata strip, using Imagick (preferred) or GD. No API key, no upload to any third party. Verified ~60% reduction on a test image.
- Next-gen serving (new Image_Serving): rewrites <img> to <picture> with AVIF and WebP <source> elements when the converted files exist, so supporting browsers download the smaller format and everyone else gets the original. Works via an output buffer, so it also covers theme and Elementor markup, not just the_content. Responsive srcset is preserved; opt out per image with a "no-picture" class, or globally via the seowing/imagify/serve_nextgen filter.
Changed
- The optimizer now defaults to the local backend. The Imagify cloud is optional: set the backend to "cloud" with a key to use it. Image optimization therefore works out of the box with no external account.
- WebP generation stays on by default; AVIF is opt-in (it is CPU-heavier and depends on the host). Existing bulk, auto-on-upload and restore-from-backup all use the local backend now.
Notes
- Quality and best-in-class compression depend on the host's image libraries; for the last few percent you can still plug in an Imagify key. The big win (next-gen images are now actually delivered) needs no configuration.
3.9.0 2026-06-03
Plugin side of the industry policy, plus the licensing gate it shares.
Added
- Prohibited-industry handling. When the license server marks a site as industry-blocked (gambling, adult, weapons, tobacco, alcohol, drugs), the plugin disables its active features (audit, AI assistant, MCP, automated changes, updates) and shows a clear admin notice. This applies regardless of the license-enforcement flag, because it is a policy block, not a licensing toggle. Existing SEO output (meta, schema, sitemaps) is left in place.
- Advisory industry scanner: a weekly, throttled heuristic scans the site name, tagline and a few pages (Elementor-aware) against the server's keyword lists and reports a SUSPECTED category to the license server for admin review. It never blocks the site itself (word-boundary matching avoids false positives like "rum" inside "forum"); the authoritative block stays admin-controlled and server-side.
Notes
- License enforcement remains OFF by default, so updating changes nothing for existing installs until an operator turns it on per site.
3.8.4 2026-06-02
Changed
- Removed the card lift-on-hover from the audit summary panel (score + Critical / Warnings / Passed tiles). It is a static panel, so the hover effect wrongly implied it was clickable. Interactive cards keep their hover. Done with a scoped
.sw-no-liftopt-out, so no other admin cards are affected.
3.8.3 2026-06-02
Fixed
- Setup wizard: the "Legal company name" field was marked
required, which blocked finishing or leaving the wizard until something was typed, even though the wizard is meant as a guard rail (the submit handler already accepts empty fields). The name is now optional-but-recommended, so Finish and Skip both work without it.
3.8.2 2026-06-02
Fixed
- Critical: the setup screen (and potentially the whole wp-admin) could enter a redirect loop (ERR_TOO_MANY_REDIRECTS) on hosts with a persistent object cache (for example LiteSpeed). The one-shot activation redirect could read a stale "redirect pending" option after deleting it, so every admin pageload bounced to the welcome dashboard, which the setup wizard then bounced to the setup page, with no exit. The activation redirect is now loop-proof: it never fires when you are already on a SEOWing screen or when the target is the page you are already viewing, so it cannot cycle even if the one-shot flag fails to clear.
3.8.1 2026-06-02
Content analysis now works in the languages the customer base actually writes in, and the image audit sees Elementor pages.
Fixed
- Readability analysis (transition words, passive voice) was English-only, so on German and Italian sites it reported zero transitions and zero passive voice and its guidance never applied. It is now locale-aware: German and Italian word lists and passive-voice patterns, with English as the fallback. The German passive pattern correctly handles participles without a ge- prefix (analysiert, bezahlt, entwickelt).
- E-E-A-T detection (personal experience, real examples, author credentials, original research, review/fact-check signals) was English-only. German and Italian phrases are now matched too, merged with English so multilingual (WPML) sites are covered in every language.
Added
- seo_image_audit now also reports Elementor on-page widget images (count and missing alt text), reusing the Elementor analyzer, and points to seo_elementor_audit / seo_elementor_fix_alt. The media-library scan alone could not see images placed through Elementor widgets.
3.8.0 2026-06-02
Two SEO capabilities that were advertised but did nothing are now real, both Elementor-aware.
Added
- seo_suggest_internal_links now works (it was a stub returning an empty list). It reads the real page text (Elementor-aware), finds published pages whose topic (focus keyword, else title) is already mentioned on the page but not yet linked, and proposes contextual internal links with a suggested anchor and target URL. It skips targets the page already links to (Elementor button/link widget URLs included) so it never suggests a duplicate link.
- seo_elementor_audit: a deep Elementor-specific audit exposed over MCP for the first time (the analyzer existed but was reachable only from an admin screen). Pass a post_id for one page or omit it for a site-wide summary. Reports heading hierarchy problems, multiple/missing H1, empty headings, images without alt text and thin text blocks, read from the actual Elementor widget tree.
- seo_elementor_fix_alt: bulk auto-fills missing alt text on Elementor widget images, deriving readable alt text from the file name. Runs as a dry-run preview by default and creates a restorable backup before writing.
3.7.0 2026-06-02
Elementor-aware content analysis. SEOWing's customer base is mostly Elementor sites, where the visible text lives in Elementor's own data, not in the WordPress post content. Every analysis that read only the post content therefore saw almost no text on Elementor pages: it counted ~0 words, flagged every page as "thin content", and could not see the real heading structure.
Added
- New Content_Extractor: one Elementor-aware source for "the real text of a page". It harvests visible text from all common Elementor widgets, including repeaters (tabs, accordions, icon lists, price lists, testimonials, CTAs), and falls back to the post content on non-Elementor pages. Word counting is now Unicode-correct, so German and Italian content (umlauts, accents) is counted properly.
- Heading-structure check in the per-page audit (deep mode): flags pages with no H1, more than one H1, or skipped heading levels (for example an H4 directly under an H2). These are rampant on Elementor sites because the heading widget lets editors pick any tag per element. Reported as an issue, not scored, so it surfaces the problem without a sudden site-wide score change.
Fixed
- Thin-content detection is now Elementor-aware everywhere it matters: it flows through Page_Evaluator, which is the single source used by the admin audit, the AI audit and the Claude connector. Elementor pages are no longer falsely flagged as thin.
- The in-editor readability analysis (Flesch, Gunning Fog, word count shown in the post sidebar) now reads the real Elementor text instead of the near-empty post content.
3.6.4 2026-06-02
Fixed
- The Claude connector now follows the same writing house style the in-admin AI Assistant already used: write in the page's natural language with correct characters (German umlauts and eszett ä ö ü Ä Ö Ü ß, never transliterated to ae/oe/ue/ss), and never use em or en dashes. Previously content written through the connector could come back transliterated. The MCP server instructions also no longer contained a transliterated "Datenschutzerklaerung" themselves.
3.6.3 2026-06-02
Finishes the "one score everywhere" work from 3.6.2. In 3.6.2 the admin audit and the AI audit shared the same formula but still ran over different category sets, so they could still disagree (e.g. 55 vs 99). Now they report the exact same number.
Fixed
- The admin "Run Full Audit" headline score now comes from the one canonical scorer, the same Site_Auditor the AI Assistant and the Claude connector use. Verified on a 939-page site: admin page and connector both report 99 where the admin page previously showed ~55. The detailed per-category checks stay as the actionable fix-it list; only the headline number is single-sourced.
- SSL check no longer reports a false "not using HTTPS" critical on the weekly cron audit and via WP-CLI. is_ssl() only inspects the current request, so it is false in cron/CLI even on HTTPS sites; the audit now also accepts an https site URL, matching how the AI auditor already checked it.
3.6.2 2026-06-02
One SEO score everywhere. Previously the same site could show three different numbers: the chat Site-Health card said "no issues", the admin "Run Full Audit" page showed ~50/100, and the AI/connector audit showed ~96/100. They each used their own math.
Fixed
- Single canonical scoring formula (new Audit_Score class) now drives both the admin audit and the AI/MCP site audit. Each category scores (pass + 0.5*warning) / checks; the overall score is the mean of the category scores. The admin audit no longer uses the old passed/total formula that collapsed to ~50 whenever a single page-level check failed, so the admin page and the connector audit now report the same number.
- The chat Site-Health card now reflects the real last full audit (meta descriptions, internal links, schema coverage, low-scoring pages, ...) instead of only a handful of settings checks plus two narrow SQL counts. It reads the stored audit data, so opening the card stays fast (no live site scan).
3.6.1 2026-06-01
Fixes from real connector usage (a full 939-page audit + auto-setup run through the Claude connector).
Fixed
- Audit recommendations now point at the tool that actually performs the fix. The "modern image formats (WebP/AVIF)" warning recommended seo_auto_setup, which only writes alt text and never converts images; it now points to seo_optimize_image. llms.txt and sitemap recommendations are routed correctly too.
- Glossary / term custom post types now get DefinedTerm schema (and norm / standard CPTs get TechArticle) instead of falling through to generic WebPage.
- Audit pagination returns next_offset = null on the last page instead of an offset past the total.
3.6.0 2026-06-01
Start unifying the two audits so they stop reporting different numbers.
Changed
- Added a single Page_Evaluator as the source of truth for per-page SEO judgment (meta title/description, focus keyword, schema, featured image, thin content). The AI/MCP full-site audit now uses it. This removes the main cause of the admin audit and the AI audit disagreeing on counts for the same site.
- Corrected an over-report: a page with no SEO meta-title override but a real post title is no longer counted as "missing title" (a title still exists).
Note
- The admin "Run Full Audit" engine keeps its detailed per-check affected-item output for now; migrating it onto Page_Evaluator is a follow-up so its UI contract is preserved. The shared evaluator is in place for that step.
3.5.7 2026-06-01
Fix the Claude connector rejecting the whole tool list.
Fixed
- Tools with no parameters declared
propertiesas an empty PHP array, which serializes to[]; JSON Schema requires an object{}, so Claude rejected the connector with "input_schema.properties: Input should be a valid dictionary" and loaded no tools. The MCP tools/list now normalizes every schema (root is an object, emptypropertiesbecomes{}), and the two offending tool definitions were fixed at source.
Changed (cleanup)
- API key generation now goes through a single canonical CSPRNG generator (two ad-hoc paths removed). The agent's blocked-tools list is a single constant instead of being duplicated at each use site.
3.5.6 2026-06-01
Audit-driven fixes, batch 2: security hardening (mostly the new connector) and assistant capability + cleanup.
Security
- The OAuth connector no longer auto-provisions a plaintext, full-privilege shared MCP key on fresh installs; per-site hashed tokens are the only issuance path. Existing legacy keys keep working.
- Connector access tokens now get a narrowed scope (read + content + settings write, never self_update) instead of full wildcard; filterable.
- OAuth token lookup is now an O(1) hash->user index instead of scanning every user's meta (fixes a scale/lockout risk on sites with many users).
- The MCP rate limiter only trusts the CF-Connecting-IP header when the request actually originates from a Cloudflare IP range, so the per-IP limit cannot be bypassed by spoofing the header.
- Dynamic client registration is rate-limited and only accepts https (or loopback) redirect URIs, closing a consent-phishing / option-bloat surface.
Changed
- The in-admin assistant gains seo_create_post, seo_suggest_internal_links and seo_fix_audit_issues in its toolset.
- save_meta_box nonce is now unslashed + sanitized before verification.
- Removed two dead methods from the audit engine (count_links_in_html, an unused calculate_seo_score) that still shipped in the build.
3.5.5 2026-06-01
Audit-driven fixes, batch 1 of 2: a critical guardrail regression, plus correctness and large-site performance.
Fixed
- CRITICAL: under the default (OpenAI) provider the AI assistant ran with NO system prompt, so every guardrail was silently disabled (do-not-invent business/legal data, preview-vs-execute discipline, correct umlauts, no false success). OpenAI now receives the system prompt like Anthropic and Gemini do.
- Autoloader could not resolve acronym class names (it splits before every capital, e.g. MCP_OAuth -> class-mcp-o-auth.php). Added an explicit map for acronym classes (MCP_Oauth, IndexNow, WebP_Converter, SEMrush, WooCommerce).
- The weekly audit cron used a 'weekly' interval that WordPress does not ship, so it never ran; the interval is now registered.
- create_tables() now verifies each table exists after dbDelta instead of always reporting success, so a failed activation is visible.
- MCP token revoke no longer reports a false failure when the token was already revoked. Dead always-'warning' branch in the SEO-scores audit fixed.
Changed (performance on large multilingual sites)
- The AI full-site audit no longer repeats the expensive site-wide scans on every paginated page; collect_missing_info and the PDF scan run once (offset 0) and are cached. collect_missing_info no longer pulls every post into one string (a 504/OOM risk); it scans a bounded set of key + recent pages.
- The admin audit no longer computes and writes an SEO score for every post during a read-only audit (it now reads stored scores only), and the image audit uses aggregate SQL plus a bounded file-stat sample instead of loading every attachment and stat-ing every file.
3.5.4 2026-06-01
Fix the Claude connector login loop.
Fixed
- After logging in with the WordPress admin account during the connector OAuth flow, authorization failed and bounced back to login. The authorize endpoint is a REST route, and WordPress only honours cookie auth on REST when an X-WP-Nonce is present; a plain browser redirect from wp-login has the login cookie but no nonce, so the user looked "logged out" and the flow looped. The endpoint now validates the WordPress auth cookie directly, so a normal admin login completes the connection.
3.5.3 2026-06-01
Changed
- Removed the "lift" (upward jump) on card and module hover. Cards still get a soft shadow on hover for feedback, but no longer move, which felt jumpy on the module and settings grids.
3.5.2 2026-06-01
Fix saving the MCP settings tab (the Claude-connector toggle would not stick, and a no-op save reported a false error).
Fixed
- The MCP settings tab dropped the "Enable MCP" and "Connect to Claude" (OAuth) checkboxes on save. The save whitelist only keeps keys present in the group defaults, and
enabled/oauth_enabledwere missing from those defaults, so the values were discarded and the checkbox reverted. Added both to the defaults. (A latent bug that could affect any tab whose field names were not in the defaults.) - Saving a settings tab without changing anything reported "Failed to save settings."
update_optionreturns false for an unchanged value, which is not an error; the save now treats "value already matches" as success.
3.5.1 2026-06-01
Critical hotfix for 3.5.0.
Fixed
- 3.5.0 caused a fatal error ("critical error", HTTP 500) on every page, including wp-admin. The new OAuth class was named
MCP_OAuthbut the autoloader maps that toclass-mcp-o-auth.php(it splits before each capital), while the file isclass-mcp-oauth.php, so the class could not be found and instantiating it fataled on every request. Renamed the class toMCP_Oauthto match the autoloader. Anyone on 3.5.0 should update immediately.
3.5.0 2026-06-01
Use SEOWing directly inside Claude.ai, and let the assistant finish image and llms.txt tasks it previously declined.
Added
- Claude custom connector (OAuth 2.1). SEOWing can now be added as a remote MCP connector in the Claude app and web, not only via Claude Code CLI. The plugin acts as its own OAuth 2.1 + PKCE authorization server: discovery metadata (RFC 9728 / RFC 8414), dynamic client registration (RFC 7591), a WordPress-login authorization step, and access/refresh tokens. An unauthenticated MCP call now returns 401 with a WWW-Authenticate header so Claude can start the flow. CORS is sent for the browser-based connector. Off by default; enable under SEOWing > Settings > MCP ("Connect to Claude"), HTTPS required. Tokens run tools as the authorizing admin, so all capability checks still apply; expired tokens are pruned daily.
Fixed
- The AI assistant can now set featured images, optimize/convert images, run bulk image updates, regenerate llms.txt and ping IndexNow. These tools existed but were not in the assistant's curated toolset, so it kept replying that it "could not do" those tasks. They are now available.
3.4.7 2026-06-01
Correct German spelling in generated content, and audit the whole multilingual site.
Fixed
- The AI assistant now writes real umlauts and eszett (ä, ö, ü, ß) and other accented characters in the content language, instead of transliterating to ae/oe/ue/ss or dropping them. Generated meta and content for German sites is now spelled correctly (for example "Prüflabor für Materialprüfung").
- Audits now cover every language. WPML/Polylang restrict WordPress queries to the active language by default, so on a bilingual site (e.g. German + English) only half the pages were audited. Both the AI audit and the admin "Run Full Audit" now scan all translations.
Changed
- The AI
seo_full_site_auditanalyzes more pages per call (default 100, up to 500) and the tool now tells the assistant to paginate through every page on large sites before summarizing, instead of judging the whole site from the first batch.
3.4.6 2026-06-01
Recent chats no longer disappear.
Fixed
- AI conversations are now stored in the database (user meta) instead of transients, so they survive page reloads and object-cache eviction (common on managed hosts). Previously a transient could be evicted at any time, which emptied the "Recent chats" list and lost context on the next message.
- Conversation expiry is enforced by timestamp (7 days idle) rather than a transient TTL. Legacy transient histories are still read once for a smooth transition.
- A turn that times out or errors now still saves its history, so the thread appears in Recent and a retry keeps context instead of starting blank.
3.4.5 2026-06-01
Second timeout root-cause: the AI tool seo_full_site_audit on large, document-heavy sites.
Fixed
- The audit's PDF scan is now bounded by both count and wall-clock time (defaults: 12 files, 8 seconds; filters
seowing/audit/pdf_scan_limitandseowing/audit/pdf_scan_seconds). On sites with many accreditation PDFs the scan previously read up to 50 files from disk in one request, which could exceed a strict host gateway timeout. The reported total PDF count is unchanged; only how many are read per call is capped. - The
seo_full_site_audittool raises its time ceiling as a safety margin.
Note
- This complements 3.4.4 (which fixed the admin "Run Full Audit" button). The two audits are separate code paths; both are now timeout-safe.
3.4.4 2026-06-01
Root-cause fix for the Full Audit timing out (504) on real sites.
Fixed
- The site-wide audit no longer fetches every page over HTTP. The internal-link check did one
wp_remote_get(15s timeout) PER POST in a single synchronous request, so on a site with many pages the audit reliably hit the gateway timeout and returned a 504. It now counts internal links from the stored content (post_content + Elementor data) with no network calls, producing the same result instantly. - The audit request raises its time and memory ceiling on large sites as a safety margin; with the HTTP loop gone the audit is now CPU/DB-bound and finishes well within it.
3.4.3 2026-06-01
Root-cause fix for AI Assistant timeouts (the 504 on long runs).
Fixed
- The agent now keeps a wall-clock budget (default 45s, filterable via
seowing/ai/time_budget) and stops itself cleanly between tool rounds before the server's gateway timeout can fire. It then summarizes what it completed and offers to continue, so a long task never ends in a silent 504. Partial work is always preserved and reported.
3.4.2 2026-06-01
AI Assistant reliability: it now always reports back, recent chats stay current, and timeouts no longer hang the chat silently. Audit screen redesigned.
Fixed
- The assistant always ends with a plain-language summary. When a tool run finished with no text, or the step limit was reached, it now forces a final text-only turn that says what it did and asks for anything it still needs, instead of going silent or showing "(no reply)".
- "Recent chats" refreshes after every turn (new server endpoint
seowing_ai_recent), so a chat shows up without reloading the page. - Chat and Apply requests now abort cleanly on a server timeout and show a clear message with any steps that finished, instead of hanging or a bare "request failed". A failed step in an Apply no longer aborts the rest.
Changed
- Audit & Autopilot screen rebuilt in the new design: severity-led issue cards (critical/warning/passed) with area tags, affected counts, the MCP tool name in mono, and an expandable preview of the affected items.
3.4.1 2026-05-30
Polish pass on the new brass interface.
Changed
- Warm neutral gray scale across the whole admin so the page chrome harmonises with the brass accent instead of the old cool, blue-tinted gray.
- Score ring gained depth (inner shadow + soft brass glow); cards now lift gently on hover with a refined shadow; health rows highlight on hover and their Fix arrow nudges.
- Primary brass buttons use a subtle gradient and press feedback; severity dots carry a faint coloured ring so they read on any background.
- All motion respects prefers-reduced-motion.
3.4.0 2026-05-30
Visual rebrand of the whole admin: warm brass and gold palette with IBM Plex typography, plus a rebuilt dashboard.
Changed
- New brand palette across every SEOWing admin screen: the old orange accent is replaced by a warm brass/gold scale, driven from a single set of design tokens so audit, meta, schema, settings and the setup wizard all pick it up at once.
- Typography switched to IBM Plex Sans for the interface and IBM Plex Mono for tool names and metrics, giving the cockpit a deliberate, instrument-panel character.
- Focus rings, gradients and module accents recoloured to match; no more stray blue or indigo from earlier themes.
Added
- Rebuilt dashboard (Data cockpit layout): a brass SEO-score ring, four metric tiles (indexable pages, SEO coverage, published posts, active modules), a live Site Health snapshot computed straight from the database (missing meta titles, missing descriptions, images without alt text) with one-click links to the fix, the MCP server status, and the active-module list.
3.3.7 2026-05-29
Proactive site-health snapshot in the AI Assistant empty state.
Added
- Site health card on the AI Assistant landing screen lists concrete issues the assistant can fix in one click: incomplete company profile, missing local-SEO fields, missing E-E-A-T experience markers, published pages with no meta title, images without alt text, and a missing llms.txt.
- Each issue is a clickable row coloured by severity (red high, amber medium, blue low) that pre-fills the chat composer with the fix prompt so the operator can hit Send or edit first.
- Re-scan control on the card to refresh after fixes; results are cached for five minutes so opening the page is cheap.
- New ajax endpoint
seowing_ai_health(capability-gated, nonce-protected) drives the snapshot.
3.3.6 2026-05-29
Recent chats in the empty state, collapsible run log.
Added
- Recent chats card in the AI Assistant empty state: shows the last six conversations (title, age) and resumes one with a click so the operator can continue where they left off.
- Per-user conversation index in user-meta backs the recent-chats list and is trimmed automatically when a conversation expires or is reset.
Changed
- Run log moved into a collapsed
<details>panel labelled "Run log (N)" so the chat stays the focus and the debugging surface is one click away when needed.
3.3.5 2026-05-29
When business identity fields are missing, the assistant now asks for them in the chat and can save them itself, instead of going silent or refusing.
Added
seo_update_settingsis now in the curated core toolset, so the assistant can persist business identity values (Company, Local SEO, E-E-A-T) directly from the conversation after the operator provides them.
Changed
- System prompt updated: when a needed business field is empty, the assistant asks the operator in plain language for the specific values, saves them via
seo_update_settings(groupcompany/local/eeat), and then continues with the original task. Pointing the operator to the matching Settings tab is offered as an alternative, not the only path.
3.3.4 2026-05-29
Fix a real correctness gap in the AI Assistant. The agent now sees the business identity instead of guessing it from page content, and is forbidden from inventing factual company data.
Added
- Company (legal name, address, phone, email, VAT/UID, founded year), Local SEO (NAP, opening hours, areas served, coords) and E-E-A-T (experience, expertise, authors, certifications) settings are now injected into the agent's cached site-context block on every run.
- When a business field is empty, the context flags it as
EMPTYand includes the exact Settings tab URL so the assistant can tell the operator where to fill it in.
Changed
- System prompt: the agent must use the BUSINESS IDENTITY block verbatim for any factual business detail and must NEVER invent the company name, address, phone, VAT, founded year, opening hours, services, authors or certifications. If a needed field is empty, it stops and points the operator at Settings.
Fixed
- The site-context cache is now invalidated when Company / Local / E-E-A-T settings are saved, so updates take effect on the next run.
3.3.3 2026-05-29
AI Assistant chat redesigned in a Vercel/shadcn style.
Changed
- Empty state with a centered "How can I help today?" greeting and a bordered skills card whose rows show an icon, title, description and arrow. The skill card disappears on the first message and returns after "New chat".
- Header is tighter with a model badge, a
⌘Kkeyboard hint, and an icon button for new chat. - Composer becomes a single framed control: textarea on top, a toolbar with mode select, post id, working indicator and a primary Send button with the
⌘⏎keyboard hint at the bottom. ⌘K/Ctrl+Kfocuses the composer;⌘⏎/Ctrl+Entersends.- Refined typography (system stack, tighter letter spacing on titles), hairline borders, neutral palette with the teal accent kept only for the Plan / Apply panel and the live progress pulse.
3.3.2 2026-05-29
Visual polish pass on the in-admin AI Assistant chat. Pulled the inline styles into a proper stylesheet, refined the type and colour palette, and tightened every component so the chat feels like a premium product instead of an admin form.
Changed
- Typography uses the system Inter stack with refined sizing, weights and letter-spacing. The whole chat reads as one piece, not a stack of cards.
- A neutral colour palette (zinc-ish greys + the SEOWing teal accent used sparingly) replaces the ad-hoc inline colours.
- Header: cleaner badge for the model, a refined custom-styled select for the Preview / Execute mode, and a quieter
New chatbutton. - Bubbles: smaller rounded-square avatars (
Du/AI), a small caption label, and content rendered through a proper typographic scale. Messages fade in. - Skill chips: pill style with hover state in the accent colour, always visible above the composer.
- Composer: focus ring on the textarea and the post-id input, primary
Sendbutton with a hover accent.Working…shows a pulse, not a static string. - Plan / Apply block: highlighted accent panel with a tighter call-to-action.
3.3.1 2026-05-29
UI polish for the in-admin AI Assistant chat.
Changed
- The assistant reply now renders a small subset of Markdown in the chat (paragraphs, bullet and numbered lists, bold,
codeand short##headings). Horizontal-rule lines and ALL CAPS pseudo-headings are now explicitly discouraged in the system prompt, so the model no longer falls back to those workarounds. - Chat bubbles get role avatars (Du, AI) and a cleaner flex layout, with monospace styling for
codespans so filenames and ids stand out. - The skill chips stay visible after the first message (no longer hidden), so the quick-action prompts remain one click away throughout the conversation.
3.3.0 2026-05-29
Feature release. A major overhaul of the in-admin AI Assistant: it now works reliably and feels like a real chat product.
Added
- Conversation memory: the assistant keeps context across turns (follow-ups like "now make it shorter" work), with a "New chat" reset.
- Preview to approve to apply: preview produces a concrete plan; the "Apply" button runs exactly those steps (no re-planning), with a per-run write cap.
- Live progress: a tool-by-tool ticker shows what the assistant is doing in real time instead of a blank wait.
- Run log and token tracking: every run records mode, steps, tokens, duration and result (shown under the AI Assistant settings).
- Full chat UI: the AI Assistant page is now a large chat. Provider, model and API key configuration moved to Settings, AI tab. Integrated SEO and GEO skill shortcuts (site audit, page meta, schema, GEO/AEO, redirects, image alt text).
Changed
- Curated the tools exposed to the model (a high-value core set) for better tool selection and lower cost.
- Stronger system prompt with injected site context and the SEO playbook.
- Bigger step and token budgets; retries with backoff on transient API errors.
- Refreshed default models (gpt-4.1, gemini-2.5-flash, claude-sonnet-4-6).
- The assistant never writes em or en dashes and replies in plain text.
Fixed
- Gemini tool-call id collisions on parallel or repeated calls.
## [3.2.0] — 2026-05-29
Feature release. In-admin AI assistant — make AI-driven SEO changes directly in wp-admin with your own model key, without Claude Code / MCP.
Added
- AI Assistant (
includes/ai/, SEOWing → AI Assistant). Add your own OpenAI / Anthropic / Gemini API key (stored encrypted) and chat with an assistant that edits the site for you. - Multi-provider LLM client with normalized tool-calling across all three providers, plus a connection test.
- Agent loop that drives the model with the plugin's existing ~55 MCP tools as callable functions (
MCP_Server::run_tool()), so the assistant can read and change meta, content, schema, images, redirects, etc. Step cap + result truncation guard against runaway loops/cost. - Gated: premium (
License::can_use_premium()) and off until a key is configured — so unlicensed/unconfigured sites are unaffected.
Safety
- Preview mode (default): the chat defaults to "Preview — plan only" — write tools are simulated (not executed) and the assistant lists exactly what it *would* change; switch to "Execute" to apply. Read tools still run so it can plan. Write vs. read is classified via the MCP scope map.
- Dangerous tools blocked:
seo_self_updateandseo_delete_postare never exposed to the agent (filterseowing/ai/blocked_tools). - Step cap (default 8,
seowing/ai/max_steps) + per-result truncation guard against runaway loops and token cost.
## [3.1.16] — 2026-05-29
Fix release. The first-run setup wizard's "Skip" was broken and trapped admins.
Fixed
- Setup wizard "Skip" now works (and no longer traps the admin). The wizard set a pending flag on activation and
maybe_redirect_to_wizard()bounced *every* admin page back to the wizard until the flag cleared — but the flag only cleared on form submit, and "Skip" was a plain link that didn't clear it. Net effect: the admin couldn't reach any page (incl. Skip) without filling in and submitting the form. "Skip" is now a nonce-protected action that clears the pending flag and goes to Settings; the wizard is a one-time nudge again, not a wall.
## [3.1.15] — 2026-05-29
Fix release. Removes a spammy-structured-data risk in FAQPage schema.
Fixed
- FAQPage schema no longer fabricates Q&A from ordinary headings. The content fallback in
FAQ_Page(class-faq-page.php) previously turned *any*<h3>/<h4>shorter than 200 characters into a fake FAQ "Question", so almost every page's headings could emit FAQPagemainEntityentries. That is "spammy structured markup" under Google's guidelines and an active manual-action risk. The fallback is now strict — only a heading that is itself a question (contains?, ≥15 chars) followed by a real paragraph answer (≥40 chars) qualifies, matching the autopilot'sextract_faq_pairseligibility check. Pages with an explicit FAQ block, FAQ meta fields, or a literal "FAQ"/"Häufig gestellte Fragen" section are unaffected; pages without genuine Q&A simply emit no FAQ schema.
## [3.1.14] — 2026-05-28
Feature release. License-based gating of the premium automation surface.
Added
License::can_use_premium()/License::enforced(): the gate primitive. When enforcement is on and the site is not licensed/activated, premium features are withheld.- MCP surface gate:
check_permissions()now refuses the entire MCP endpoint (402) when premium is not allowed — the per-site MCP token is the automation kill-switch, now also tied to license state. - Admin notice when enforcement is on but no active license is present.
Important
- Enforcement is OFF by default. Updating the plugin changes nothing on existing installs; the MCP keeps working. Turn it on per-site only after a license is active, via the
SEOWING_LICENSE_ENFORCEconstant, theseowing_license_enforceoption, or theseowing/license/enforcefilter. This mirrors the v3.1.5 rollout discipline — never batch-lock sites. - Frontend SEO output (meta, schema) is never gated: a lapsed license stops new automation but never breaks a site's existing, live SEO.
## [3.1.13] — 2026-05-28
Feature release. Turns SEOWing into a licensed product with secure, self-hosted auto-updates (delivered from api.seowing.io to licensed sites only).
Added
- License module (
includes/license/): a License admin page under the SEOWing menu to enter and activate a per-site license key. The key is stored encrypted and never leaves the site except over HTTPS to the license server. - Per-site activation bound to the site's domain, with seat limits and expiry enforced server-side. "Deactivate" frees a seat so a license can move.
- Licensed auto-updates via the bundled Plugin Update Checker library (
includes/lib/plugin-update-checker/, v5.7). Licensed + activated sites see "Update verfügbar" in the WordPress admin and update with one click; the download is license-gated. - Daily status re-check (cron
seowing/license/daily_check) with a 14-day grace window: if the license server is briefly unreachable, a valid license keeps working — an outage never bricks a paying customer's site. Update URIheader pointed athttps://seowing.ioso WordPress.org cannot hijack updates for theseowingslug.
Notes
- The license key is the credential for all license/update API calls — no shared secret is embedded in the plugin.
License::is_active()/is_valid()are exposed for future feature gating.
## [3.1.12] — 2026-05-28
Fix release. Hardens the author and navigation-menu tools added in 3.1.8–3.1.11.
Fixed
- Invalid
authorno longer reintroduces the service-account byline. When a post is created via a token request and an explicitauthoris given but cannot be resolved (typo, wrong email/ID), the new post now still falls back to the first administrator instead of silently being attributed to the low-privilegeseowing_mcp_serviceaccount — the exact byline bug 3.1.10 fixed. - Numeric
authorIDs are validated. A numericauthorreferencing a non-existent user is rejected (resolves to "not found") instead of being written as a danglingpost_author. seo_add_menu_itemrejects broken object references. Apost_typeortaxonomymenu item now requires anobject_idthat references an existing post/page or term; previously a missing or invalidobject_idcreated a dead menu entry pointing at object 0.
## [3.1.11] — 2026-05-28
Feature release. Adds Media Library uploads via MCP.
Added
seo_upload_media(settings:write): uploads a base64-encoded file (image, PDF, Word/Office document, etc.) into the WordPress Media Library as a proper attachment and returns its ID and URL. Hardened: onlyget_allowed_mime_types()are accepted, an explicit dangerous-extension blocklist (php/js/html/exe/svg/…) is enforced, a 25 MB cap applies, and the written file's real content is re-verified against its extension. Registered in the scope map.
## [3.1.10] — 2026-05-28
Fix. MCP-created posts showed the low-privilege service account as the public byline.
Added / Fixed
authorparameter onseo_create_post(user ID, login, slug or email): sets the post author / public byline.- New posts no longer default to the MCP service account. When a post is created via a token request (running as the
seowing_mcp_serviceuser) and no author is given, it is attributed to the first administrator instead, so the service account never appears as the public author.
## [3.1.9] — 2026-05-28
Feature release. Completes navigation-menu management with ordering.
Added
seo_reorder_menu(settings:write): reorders a menu by passing item IDs in the desired order; reassignsmenu_ordersequentially via a surgicalwp_update_postthat leaves the items' other fields untouched.positionparameter onseo_add_menu_item: place a new item at a specific order position instead of appending.
## [3.1.8] — 2026-05-28
Feature release. Adds taxonomy and navigation-menu management so content can be fully categorized and linked from the MCP tools, without manual WP-admin steps.
Added
- Categories, tags and custom taxonomy terms in
seo_create_post. Newcategories,tagsandtermsparameters. Category/term names that do not exist are created; assignments replace existing terms and are guarded by the post type's registered taxonomies. - Navigation-menu tools.
seo_get_menus(read) lists menus, their items and theme locations.seo_add_menu_item(settings:write) adds an item to a menu, creating the menu if it does not exist, and links a page/post, a taxonomy term, or a custom URL. Both are registered in the scope map (default-deny still applies to any future unmapped tool).
## [3.1.7] — 2026-05-26
Admin UI bug-fix release. Found during a runtime functional audit of the wp-admin screens (view -> JS -> REST/AJAX wiring). These are pre-existing admin defects, independent of the 3.1.5/3.1.6 MCP security work.
Fixed
- CRITICAL (data loss): Settings tabs silently reset to defaults on save.
handle_settings_post()read inconsistent POST keys (e.g.$_POST['titles']while the view postsseowing_titles[...]), so Titles & Meta, Social, Sitemap, Schema and the MCP form fields saved an empty array, whichsave_group_settings()then merged over with the group defaults, wiping the operator's input while showing a green "saved" confirmation. Replaced the per-group switch with one generic read (seowing_<group>first, bare<group>fallback). - Imagify quota showed "NaN undefined". The check-key AJAX returned the raw
/users/mebody instead of the normalized quota; now returnsget_quota(). - Local SEO "Business Info" tab rendered empty. Panels relied on a
hiddentoggle while the CSS only shows.seowing-tab-content.active; switched markup and JS to the.activeconvention. - Internal Links page unstyled + dead tabs. Added the missing
assets/css/modules/internal-links.css(overview cards, stat rows, PageRank bar, depth, badges) and enqueued it; fixed the tab handler (.sw-tab/.active, was.nav-tab/.hidden). - Tools "Regenerate Sitemap" button did nothing. It sent the generic
seowing_ajaxnonce; the handler verifies theseowing_sitemapnonce in thenoncefield. Passed it explicitly. - Sitemap: could not enable/disable individual types + settings never saved. Registered the missing
seowing_save_sitemap_settingshandler, added per-type toggle checkboxes (Authors, News, Videos, custom post types), and unified per-type on/off onto a single store (enabled_types) that every provider reads (Authors is no longer hardcoded off). Defaults preserve current behaviour until saved.
Verify on staging
Save each previously-broken settings tab and reload to confirm values persist; toggle a sitemap type (e.g. Authors) on and confirm it appears in the sitemap.
## [3.1.6] — 2026-05-25
Patch on top of 3.1.5, found during staging verification of the per-site MCP key feature. Both issues block or obscure token issuance on real installs.
Fixed
- Per-site token table was never created on existing installs. The
{prefix}seowing_mcp_keystable was added in 3.1.5 (2f5eb6d) without bumpingDatabase::DB_VERSION(last set to1.2.0in v1.5.29). On any site already at db_version1.2.0,needs_update()returned false, so the upgrade path skippedcreate_tables()and the table was missing. Issuing a token then failed inMCP_Key_Store::create()($wpdb->insertreturns false → "Could not store the token."). BumpedDB_VERSIONto1.3.0so theinitupgrade routine creates the table automatically on next load — no reactivation required, on every site. - MCP settings UI collapsed every AJAX error to a bare "Failed." The script read
res.data.message, butajax_error()returns the message at top-levelres.message. The issue/revoke/legacy-invalidate handlers now read both, so the real error surfaces instead of a generic "Failed."
## [3.1.5] — 2026-05-25
Security release (P0). Replaces the MCP authentication model. This version also contains the fixes that were prepared as 3.1.4 but never released; there is no separate 3.1.4 build, so nothing is missing between 3.1.3 and 3.1.5.
Security
- CRITICAL — MCP auth no longer grants full administrator. Previously a valid API key called
wp_set_current_user( first administrator ), making every per-tool capability check cosmetic. Removed. Token/legacy requests now run as a dedicated low-privilege service user (seowing-mcp-service, custom role with onlyread), and authorization is decided by scopes. - CRITICAL — per-site hashed tokens replace the shared plaintext key. Tokens are stored only as SHA-256 hashes (
{prefix}seowing_mcp_keys), compared withhash_equals(), shown once at creation, individually revocable. No clear-text round-trip. Issue/revoke from SEOWing -> Settings -> MCP. - Scope-based, default-deny authorization. Every one of the 55 MCP tools is mapped to a coarse scope (
read,content:write,settings:write,self_update) inMCP_Scopes. A tool not in the map, or a credential lacking the scope, is rejected at the dispatch gate. The wildcard*never coversself_update. The 7 previously capability-less autopilot tools now each carry an explicit scope. seo_self_updatenow requires the explicitself_updatescope in addition to the existingSEOWING_ALLOW_SELF_UPDATEwp-config constant (double-gated).
Migration (dual-accept, no downtime)
- During the migration window the server accepts BOTH the legacy shared key and new per-site tokens. Order: ship 3.1.5 everywhere -> issue + roll out per-site tokens -> only then invalidate the legacy key.
- Invalidation encrypts a break-glass backup of the old key via the existing
Encryption(AES-256) path, then clears it and blocks auto-recreation. A fail-safe purge (scheduled cron + opportunistic age check on admin load) destroys the backup after 7 days. Break-glassrestore_legacy()works only within that window. - The admin UI guards invalidation behind a safety check: a new token must be warm (used) AND the legacy key cold (unused 3+ days). Override with
force.
Fixed (carried from the unreleased 3.1.4)
seo_self_update: opt-inSEOWING_ALLOW_SELF_UPDATEgate + zip-slip guard.seo_get_settings: redactapi_key/imagify_api_key/semrush_api_key.- MCP key mismatch path no longer logs any portion of the key.
- MCP per-IP rate limiter: transient fallback when no object cache is present.
seo_upload_file: dropped.htaccessand.jsfrom the allowlist.
MUST verify on staging before any client site
- Valid new token accepted; invalid/revoked rejected; revocation takes effect immediately.
- Dual-accept (old + new) works during the transition.
seowing_mcp_keysstores only hashes (no clear text anywhere).wp_set_current_user(admin)is gone: asettings:writetool called with aread-only token is correctly DENIED.- Each of the 7 autopilot tools (
seo_full_site_audit,seo_get_playbook,seo_auto_setup,seo_fix_audit_issues,seo_fix_meta_bulk,seo_fix_norm_schema,seo_generate_schema) tested individually against its assigned scope, not sampled. seo_self_updatestill blocked without both the constant and the scope.- Legacy invalidation: backup is encrypted; the fail-safe purge removes it after 7 days even if cron was missed.
## [3.1.3] — 2026-05-12
Patch release. Fixes a long-standing bug where <meta name="description"> was missing from rendered HTML on most sites.
Fixed
- CRITICAL
<meta name="description">was missing from every page on sites where the theme fireswp_head()more than once (e.g. nexsas, many page-builder themes). Root cause was astr_replace()inSchema::cleanup_duplicate_meta_descriptions()that removed every duplicate of the meta description tag when the theme double-fired wp_head — including the one we wanted to keep. Replaced withpreg_replace_callback()that keeps only the first occurrence, removes the rest by callback (no global string replacement). OpenGraph and Twitter descriptions were unaffected because they don't pass through this cleanup buffer.
- MEDIUM
Meta_Frontend::get_description()usedget_the_ID()which can return0inwp_headbefore the Loop runs. Now prefersget_queried_object_id()(always reliable in wp_head on singular pages) withget_the_ID()as fallback. Without this fix, sites where the theme fires wp_head before the_post() would have had no description even before hitting the buffer bug.
Symptom on affected sites: third-party SEO checkers reported "Description is missing!" on every page even though the SEOWing UI showed the value as saved.
## [3.1.2] — 2026-05-12
Patch release. Adds self-protection against meta wipes caused by foreign save_post events (Elementor REST saves, page-builder AJAX, etc.).
Added
- Meta_Guard component (
includes/modules/meta/class-meta-guard.php). Captures non-empty_seowing_*meta before each save (pre_post_updatepriority 1) and restores any key that went from a real value back to empty during the same request (save_postpriority 999 +elementor/document/after_save). Does not interfere with intentional changes — only restores wipes. - Filter
seowing/meta/guard_enabledto opt out per post. - Action
seowing/meta/guard_restoredfor monitoring/logging.
Fixed
- MEDIUM SEOWing meta (title, description, focus keyword, robots, canonical, OG/Twitter overrides) no longer disappears when a page is saved through Elementor's REST endpoint or any other code path that fires
save_postwithout carrying the SEOWing nonce. Previously affected sites where the Elementor editor was opened on pages that already had MCP-set meta.
## [3.0.0] — 2026-04-29
Major release. Three confirmed schema-rendering bugs are fixed, the production ZIP is 15× smaller, and the plugin gains a first-time setup flow.
Fixed
- CRITICAL Schema fallback in
Frontend::get_organization_schema()/get_local_business_schema()no longer reads from the legacyseowing_general.company_*keys. Both methods now hydrate from the canonicalseowing_companygroup, sotelephone,foundingDate,vatID,geo, andsameAsactually render when the Schema module is disabled. - MEDIUM
Local_Business::generate()now inherits address, phone, email, geo, and price range fromseowing_company/seowing_local. Per-post LocalBusiness schemas no longer ship empty when the operator hasn't filled every field manually. - MEDIUM
Local_SEO::get_default_business_info()no longer falls back toget_bloginfo('admin_email')andget_bloginfo('name'). The "ghost" LocalBusiness schema we saw on production sites (with the WP admin email leaking into JSON-LD) is gone — defaults now come fromseowing_company/seowing_social/seowing_local.
Added
bin/build-zip.shreproducible production build script. Required before every release. Old ad-hoczip -rinvocations missedmcp-bridge/and every*.mdfile, which is why pre-3.0 ZIPs shipped at 9.7 MB instead of ~500 KB.- Real Tailwind v4 build pipeline.
assets/css/tailadmin.cssis now generated fromsrc/admin.cssby the standalone Tailwind CLI (bin/install-tailwind.shto vendor it locally;bin/build-zip.shinvokes it on every release). Result: 36 KB instead of 124 KB (−71 %), only the utilities actually referenced byincludes/andassets/js/are emitted, customshadow-theme-*tokens are preserved, and the CSS is reproducible from source.
Critical post-rollout patches
- Setup wizard rendered without styles because
seowing_page_seowing-setupwas missing fromAdmin::enqueue_assets()'s page whitelist. Added. - Every per-module stylesheet (
assets/css/modules/*.css,assets/css/integrations/elementor.css) consumes 20+--sw-*design tokens (--sw-brand,--sw-gray-50,--sw-radius-sm,--sw-success,--sw-ease,--sw-shadow-xs, etc.) that the pre-3.0 vendored TailAdmin dump declared but the new build never re-emitted — so every module page rendered colorless and unstyled. Restored insrc/admin.cssas first-class design tokens that alias the Tailwind theme tokens, so there is now one source of truth for color and spacing across utility classes AND hand-rolled module CSS.
Design / Visual fixes
- Restored full color palette in the Tailwind theme. The pre-3.0 vendored TailAdmin dump shipped
bg-success-*,bg-warning-*,bg-error-*rules that referenced colors which the new build couldn't generate, leaving dashboard cards, badges, and notices visually broken (transparent backgrounds, invisible icons). All four palettes (brand / success / warning / error) are now declared with full 50-900 scales insrc/admin.cssso every utility resolves correctly. - Replaced 87 off-scale
text-[13px]/text-[15px]/text-[11px]arbitrary classes with the standardtext-xs/text-sm/text-basescale. Forms, descriptions, and labels now sit on the type system instead of breaking it. - Description text contrast bumped from
text-gray-400totext-gray-500in 18 places — meets WCAG AA contrast for body copy on white. - Setup wizard re-styled from raw
<table class="form-table">to the same Tailwind card/grid system the rest of the admin uses. Hero gradient banner, numbered step badges, two-column responsive form, sticky action footer. - Page-load stagger animations (
.sw-reveal+ 6 delay tiers, pure CSS,prefers-reduced-motionrespected). Dashboard hero, four metric cards, coverage bar, and two-column grid fade up on first paint. Wizard hero plus three steps stagger in. No JavaScript, no library; respects users who request reduced motion. - Custom inline SVG illustration for the "no active modules" empty state — isometric stack of plates with a brand-orange spark, replacing the generic dashicon-in-a-circle. Pure SVG, no external assets, follows the brand palette via
currentColor. - Typographic polish via system-font OpenType features. The admin now opts into
font-feature-settings: "ss01", "cv11", "case", "calt"andtabular-numsfor a more refined feel — stats line up, headings get tighter letter-spacing (-0.02em), and we ship zero font files (no GDPR risk, no extra HTTP requests). Settings_Keysconstants class (includes/class-settings-keys.php) — single source of truth for settings group names and well-known field keys. Replaces magic strings like'company.phone'and$general['company_phone'].Setup_Wizardfirst-time onboarding flow at SEOWing → Setup. Triggered the first time an admin loads wp-admin after activation ifseowing_company.nameis empty. Captures Company info, basic E-E-A-T fields, and surfaces the MCP API token in one screen.- Module dependency validation —
Module_Manager::load_modules()now readsModule_Base::$dependenciesand skips modules whose required dependencies aren't loaded. Skipped modules surface viaget_dependency_failures()for admin notices instead of failing silently. - Capability checks on the six previously-unguarded
seo_*MCP tools inclass-seo-tools.php(analyze_page, calculate_score, bulk_analyze, bulk_calculate_scores, get_suggestions, suggest_internal_links). All requireedit_posts, matching the rest of the MCP write surface.
Removed
- 7 deprecated modules and their assets are gone — they were marked deprecated since v2.6.0 but still shipped:
content-generator/,templates/,content-decay/,clusters/,entities/,link-opportunities/,experts/(PHP, JS, CSS). Existing settings rows inwp_optionsare preserved; only the runtime classes are dropped. Sites that depend on these modules should pin to v2.x. mcp-bridge/is no longer bundled in the production ZIP. It's a developer tool, not a runtime dependency. Ships separately via GitHub releases.- All planning / audit Markdown files (
PLANNING.md,IMPROVEMENT-PLAN.md,MCP-ECOSYSTEM.md,MCP-TOOLS.md,tool-dokumentation.md,CONTEXT.md,CONTRIBUTING.md,ISSUES.md,AUDIT*.md,CLAUDE.md) are no longer bundled in the production ZIP. They live in the repo for developers only. phpunit.xml, rootpackage.json, rootcomposer.json,.gitignoreare no longer bundled.
Notes for upgraders
- The Schema module remains the recommended (and default) renderer. If you rely on the Frontend fallback path, expect richer JSON-LD on the next page load — fields that were silently dropped in v2.x will now appear.
- If you previously stored business info in
seowing_local_business(the Local SEO module's parallel option), nothing changes for you. Operators who configured everything inseowing_companyand never touched the Local SEO admin page will see the LocalBusiness schema light up correctly for the first time. - The ZIP exclude list is now strict. Don't ship
bin/build-zip.sheither — the script excludes itself.
## [2.5.65] — 2026-02-23
Changed
- Split
class-admin.phpgod class (2,532 lines) into 3 focused files: -Admin— menu, pages, settings, meta boxes, columns, core AJAX (915 lines) -Audit_Engine— all SEO audit checks, AJAX handler, cron audit (1,337 lines) -Tools_Handler— export/import, cache, DB optimize, orphan cleanup, migration (363 lines) - Audit engine and tools handler register their own hooks via constructors
- Deduplicated audit post-type exclusion logic into shared
get_auditable_post_types() - Unified AJAX/cron audit execution via shared
run_audit()method
## [2.5.64] — 2026-02-23
Changed
- Split
class-autopilot-tools.phpgod class (3,833 lines) into 7 focused sub-classes: -Site_Auditor— full site SEO audit (~800 lines) -Site_Configurator— auto-setup fix orchestration (~700 lines) -Data_Detector— missing info collection, auto-detect, PDF scan (~540 lines) -Autopilot_Schema— JSON-LD schema generation (~500 lines) -Autopilot_Content— meta title/description/keyword generation (~380 lines) -Playbook_Generator— SEO/GEO/AEO playbook (~260 lines) -Autopilot_Helperstrait — sharedis_dach_site()andget_public_post_types() Autopilot_Toolsreduced to thin router (~280 lines) with lazy-initialized sub-classes- Autoloader updated with fallback for
trait-prefix files outside theTraitsfolder
## [2.5.63] — 2026-02-23
Fixed
- Race condition in sitemap rewrite rules flush — transient lock prevents concurrent
flush_rewrite_rules()calls - Scroll-spy memory leak in Elementor TOC — singleton pattern +
requestAnimationFramethrottling - SEO score cached forever — now invalidated on
save_postso score recalculates after content changes - Glossary query without limit — capped at 500 terms to prevent memory overflow
- Nonce validation with
sanitize_key()— replaced withwp_unslash()to preserve nonce integrity - Dynamic tag empty output on 404/archive pages — added
wp_get_document_title()fallback - TOC widget
data-settingsdouble-escaped — use single-quoted attribute to avoidesc_attr(wp_json_encode())double encoding
Changed
- Cache bridge uses pattern-based tool matching instead of hardcoded list — new MCP tools auto-trigger cache purge
## [2.5.62] — 2026-02-23
Changed
- Remove unused
seowing_metacustom table — all SEO data useswp_postmeta(saves DB overhead on install/upgrade) - Frontend meta fetching aggregated via
get_meta()helper — pre-fetches all keys in one call instead of 12+ individualget_post_meta()calls per page - Admin SEO scoring now delegates to Analysis module when active (single source of truth)
- Admin fallback scoring simplified — removed expensive
apply_filters('the_content')call
Added
Meta_Keysconstants class (includes/class-meta-keys.php) — centralized meta key definitionsindexnowandimagifysettings groups in Settings class with migration from individual options- Static cache for entity auto-linking query — prevents unbounded
posts_per_page=-1on everythe_contentcall, limited to 200
Fixed
- Entity auto-linking performance: query now cached per request and limited to 200 entities
## [2.5.61] — 2026-02-23
Security
- Fix SQL injection risk in uninstall routine — all 6 queries now use
$wpdb->prepare() - Fix unescaped schema output in Breadcrumbs widget — now uses
wp_kses()
Fixed
strpos()logic bug in meta keyword analysis —falsewas cast to0, causing false-positivekeyword_at_start- Fatal error in
deactivate_module()when accessing->is_coreon a failed (cachedfalse) module load - Module loading retry loop — failed modules are now cached as
falseto prevent repeated instantiation attempts
Added
- CSS custom properties (
:rootblock) for Elementor widget styles (--sw-gray-50,--sw-gray-200,--sw-duration-fast,--sw-ease,--sw-radius-sm,--sw-success,--sw-warning,--sw-error) - Cache invalidation (
wp_cache_delete) after module activate/deactivate for Redis/Memcached compatibility
## [2.5.60] — 2026-02-23
Fixed
- HTML entities (
&etc.) appearing in JSON-LD schema output — addedhtml_entity_decode()in Local SEO, JSON-LD, and Graph Builder - CPT archive pages now get proper hreflang tags via
wpml_permalinkfilter
Changed
- Schema Generator module is now always active (
is_core = true)
## [2.5.59] — 2026-02-23
Fixed
- WPML hreflang for CPTs — added
wpml_object_idfallback whenicl_get_languages()fails for custom post types - Default language now auto-syncs from WPML instead of hardcoded
'en'
Changed
- SEO coverage dashboard now counts all public post types (not just posts/pages)
- Hreflang audit now includes enabled CPTs in coverage stats
## [2.5.58] — 2026-02-22
Added
- Meta title, description, and canonical support for CPT archive pages
## [2.5.57] — 2026-02-22
Fixed
- Robust WPML hreflang suppression on non-default language pages
- Trailing slash consistency in hreflang URLs
## [2.5.56] — 2026-02-22
Fixed
- WPML hreflang bug — wrong URLs generated on non-default language pages
## [2.5.55] — 2026-02-22
Added
seo_index_nowMCP tool for IndexNow instant indexing
## [2.5.54] — 2026-02-22
Fixed
- Removed
Disallow: /wp-includes/from robots.txt template (blocks CSS/JS needed for rendering)
## [2.5.53] — 2026-02-21
Added
ProfessionalServiceadded to LocalBusiness schema types
Fixed
priceRangeandimagefields in Organization schema
## [2.5.52] — 2026-02-21
Added
- Default OG image fallback for pages without featured images
## [2.5.51] — 2026-02-21
Fixed
og:imagewas outputting attachment ID instead of URL- Added
og_imageparameter toseo_update_metaMCP tool
## [2.5.50] — 2026-02-17
Added
- Author archive 301 redirect to homepage (configurable)
- Taxonomy/category
noindexcontrols via MCP settings - Admin CSS visual polish — 14 UI enhancements
## [2.5.49] — 2026-02-17
Changed
- Complete CSS migration to Tailwind v4 theme system (43 files, 660+ lines migrated)
## [2.5.48] — 2026-02-17
Fixed
- Missed
JSON_HEX_TAGin schema-tools Elementor fix
## [2.5.47] — 2026-02-17
Security
- Security hardening + audit quick wins
## [2.5.46] — 2026-02-16
Fixed
- Service page detection — only use custom schemas as source of truth
## [2.5.45] — 2026-02-16
Fixed
- Duplicate schema cleanup now detects
@typeinside@grapharrays
## [2.5.44] — 2026-02-16
Added
@graphSchema Builder for auto-generated rich JSON-LD
## [2.5.43] — 2026-02-16
Fixed
- MCP schema settings now sync to module storage
## [2.5.42] — 2026-02-16
Fixed
- Schema
headline→namefor non-Article types
## [2.5.41] — 2026-02-16
Fixed
- Definitive archive title prefix fix with JS fallback
## [2.5.40] — 2026-02-16
Fixed
- Regex fallback to strip archive title prefix
## [2.5.39] — 2026-02-16
Fixed
- Archive title prefix filter moved to main class for reliable loading
## [2.5.38] — 2026-02-16
Fixed
- Archive title prefix removal for glossary pages
## [2.5.37] — 2026-02-16
Added
wp_tagline/wp_blognamesync to WordPress core options
## [2.5.36] — 2026-02-16
Added
- German labels for glossary post type and clean archive title
## [2.5.35] — 2026-02-16
Added
[norm_field]shortcode for Elementor Dynamic Tags
## [2.5.34] — 2026-02-15
Added
- Expert email, phone, photo_url fields to norm meta
## [2.5.33] — 2026-02-15
Changed
- Expert card: white background with dark text
## [2.5.32] — 2026-02-15
Changed
- Experts module now always active (
is_core = true)
## [2.5.31] — 2026-02-15
Added
- Reusable Experts module with team CPT integration
## [2.5.30] — 2026-02-15
Added
- Registered norm meta fields +
custom_metasupport inseo_create_post
## [2.5.29] — 2026-02-15
Added
- Configurable HSTS header support
## [2.5.28] — 2026-02-15
Fixed
seo_generate_schemaMCP tool now defaults toWebPagefor pages (was overriding v2.5.27 fix)
## [2.5.27] — 2026-02-15
Changed
- WordPress pages now default to
WebPageschema instead ofArticle
## [2.5.26] — 2026-02-15
Fixed
- Skip
BreadcrumbListon homepage to avoid duplicate URL error
## [2.5.25] — 2026-02-15
Fixed
- Redirect MCP tools column name mismatch with repository schema
- Query-string redirect support (e.g.
/?page_id=520→/kontakt/)
## [2.5.24] — 2026-02-15
Fixed
- Article schema: image fallback chain (featured → OG → content → site icon → theme logo)
- Article schema: author fallback to Organization for generic usernames
- Article schema: publisher logo URL resolution
## [2.5.23] — 2026-02-15
Added
- Multiple Certification schemas allowed per page
## [2.5.22] — 2026-02-15
Fixed
- OG tags using wrong module check
- Schema internal deduplication (
cleanup_duplicate_schemas) - WP core sitemap disabled (SEOWing generates its own)
@typecase normalization (30+ mappings)
## [2.5.21] — 2026-02-14
Added
- Auto-remove duplicate JSON-LD schemas from themes/plugins
## [2.5.20] — 2026-02-14
Fixed
- Image audit: count unique files, file has alt if ANY post entry has it
## [2.5.19] — 2026-02-14
Fixed
- WPML image audit: use LEFT JOIN for shared media without translation entries
## [2.5.18] — 2026-02-14
Added
- WPML-aware image audit to exclude duplicate attachments
## [2.5.17] — 2026-02-14
Added
- Quick Connect command on MCP settings page
## [2.5.16] — 2026-02-14
Changed
- SEO-expert keyword generation from industry + search intent
## [2.5.15] — 2026-02-14
Added
- Smart focus keyword generation for all page types
## [2.5.14] — 2026-02-14
Added
- Intelligent auto-schema for homepage + CPTs
## [2.5.13] — 2026-02-13
Added
langparameter toseo_auto_setupfor WPML translations
## [2.5.12] — 2026-02-13
Fixed
- Schema coverage audit now includes CPTs and custom schemas
## [2.5.11] — 2026-02-13
Added
- Auto-setup fixes search engine visibility (noindex) setting
## [2.5.10] — 2026-02-13
Changed
- Fetch rendered HTML for internal link audit (more accurate)
## [2.5.9] — 2026-02-13
Removed
- "SEO by GoldenWing" backlink from frontend footer
## [2.5.8] — 2026-02-13
Fixed
- Audit internal links by parsing Elementor JSON data directly
## [2.5.7] — 2026-02-13
Added
- GEO Score Analyzer activated by default on existing installs
Fixed
- Audit internal links detection
## [2.5.6] — 2026-02-13
Fixed
- Broken modal CSS on redirects page
## [2.5.5] — 2026-02-13
Fixed
- Audit score calculation
- Elementor internal links detection
## [2.5.4] — 2026-02-12
Changed
- Exclude Elementor templates from CPT audit/auto-setup
## [2.5.3] — 2026-02-12
Fixed
- All 9 remaining issues from biopower site audit
## [2.5.2] — 2026-02-12
Fixed
wp_generate_attachment_metadata()crash in REST context
## [2.5.1] — 2026-02-12
Fixed
- MCP Streamable HTTP transport compatibility
## [2.5.0] — 2026-02-12
Added
- Native MCP Streamable HTTP Transport
## [2.4.0] — 2026-02-11
Changed
- Complete Tailwind CSS v4 migration — major UI overhaul
## [2.3.2] — 2026-02-11
Changed
- Migrate meta and schema meta box views to Tailwind utilities
## [2.3.1] — 2026-02-10
Added
- Auto-detect GMB, GPS coordinates, social profiles, and logo from site content
## [2.3.0] — 2026-02-10
Added
- Major autopilot upgrade: module auto-enable, PDF scan, missing info prompts, backlink injection
## [2.2.2] — 2026-02-09
Fixed
- Tools page: add spacing, implement all AJAX handlers
## [2.2.1] — 2026-02-09
Fixed
- CSS/UI issues: audit layout, toggle duplication, spacing, overflow
## [2.2.0] — 2026-02-09
Changed
- Smart Autopilot:
seo_auto_setupnow applies SEO/GEO best practices automatically
## [2.1.1] — 2026-02-08
Fixed
- MCP parameter handling — accept both
paramsandargumentskeys
## [2.1.0] — 2026-02-08
Added
- SEO Autopilot:
seo_full_site_audit,seo_auto_setup, andseo_get_playbookMCP tools
## [2.0.1] — 2026-02-07
Fixed
- Migrate missing CSS from legacy
admin.css, then delete it
## [2.0.0] — 2026-02-07
Changed
- Complete plugin overhaul: MCP split, CSS consolidation, deduplication, security hardening
## [1.6.2] — 2026-02-06
Fixed
- Form field accessibility: add id/name to module toggles, fix label for separator
## [1.6.1] — 2026-02-06
Changed
- TailAdmin UI integration — replace custom CSS with Tailwind v4
## [1.5.42] — 2026-02-05
Security
- Deep audit: fix 57 issues across security, logic, and dead code
## [1.5.39] — 2026-02-05
Added
- Improvement plan and automation ideas
## [1.0.0 – 1.5.38] — 2026-01 to 2026-02
Added
- Initial release with full module system
- Meta Manager, Schema Generator, Sitemap Engine, Redirect Manager
- Breadcrumbs, Local SEO, Image Optimizer, International SEO (Hreflang)
- Elementor + Gutenberg integrations
- Cache Bridge (LiteSpeed, WP Rocket, W3TC)
- WooCommerce SEO integration
- Analysis module with Readability & E-E-A-T scoring
- Content Decay Monitor, GEO & LLM Readiness modules
- Topical Authority modules
- Social Previews, IndexNow, AI Content Generator
- MCP Bridge with 40+ SEO tools
- Apple-inspired Admin UI