SEOWing
  • Features
  • Pricing
  • FAQ
  • Docs
EN/DELoginStart 30-day trial

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's wp_robots action. A category set to noindex was served as index, follow. Both consumers now build from one build_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 in wp_head on any theme that has not started the Loop, so custom robots, canonical and title values were silently discarded. Resolved via get_current_post_id().
  • noindex_search could not be turned off — it read a meta settings group that does not exist, so the default always won. Moved to titles.noindex_search with a checkbox in the Titles tab. (Same defect previously fixed for noindex_empty_archives.)
  • JSON-LD vanished on umlauts. Schema_Type::get_description() truncated with substr(), cutting mid-sequence; the invalid UTF-8 made wp_json_encode() return false and the whole block disappeared. Now mb_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, but mysql2date() parses in the site timezone, so timestamps were off by the site's offset. Parsed as UTC and rendered with wp_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_meta handed arrays to sanitize_text_field(), which returns an empty string. Sanitization is now per key: URLs through esc_url_raw(), booleans as booleans, and schema_data / custom_schemas recursively with their shape intact. (includes/rest/class-rest-controller.php)
  • seo_generate_schema was not idempotent. Each run keyed its result as type_<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 localbusiness key 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_post broke Elementor layouts. update_post_meta() unslashes its input, so copying _elementor_data stripped the backslashes out of its JSON and the duplicate opened empty. Copied meta now goes through wp_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 wordCount published in Article JSON-LD. All share Content_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, and Settings_Keys::META_ROBOTS no longer contradicts Meta_Keys::ROBOTS_META.
  • A third dead robots path, and max-image-preview:large. Wave 2 unified the two copies in Meta_Frontend; Frontend::filter_robots was a third, hooked to wp_robots, which never runs because core's wp_head action is removed. Missing with it: core's own max-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 through wp_robots, so every consumer contributes and ours runs last.
  • noindex_search off 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 through seo_validate_schema in 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 @id references to nodes that do not exist, and stays quiet otherwise. Scope read; the required-property map is filterable via seowing/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/bulk returned any post's meta to any contributor. Its permission callback checks edit_post against an id parameter the route never sends, so the gate was edit_posts. Authorization is per item now and the response reports how many were withheld. (includes/modules/meta/class-meta-rest.php)
  • custom_meta accepted 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 (filter seowing/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_info handed admin_email to 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, and get_unoptimized_images() used NOT LIKE — which matched everything — so a bulk run re-compressed images it had already compressed, losing quality each pass, silently. A scalar _seowing_imagify_status key 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; a processing marker 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 (and seo_list_snapshots) put back exactly what was captured; mapped to settings: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 with textContent now.
  • ajax_chat had 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 (filter seowing/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 #organization via parentOrganization, and a non-primary site is typed Department. 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 with substr() 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) into debug.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's backlink section, which set an option nothing reads and then reported "fixed: 1" for work that had no effect.
  • Site_Auditor::calculate_category_score(), and the empty elementor-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_at serves 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 touches wp_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 on get_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 was in_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 (filter seowing/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 a WP_DEBUG install). (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_at to {prefix}seowing_mcp_keys (DB version 1.4.0); the reads tolerate the column not being there yet, since create_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_schema and seo_delete_redirect sat under content:write, so a token allowed to fix meta could also remove posts. They require content:delete now, offered in the token UI. Wildcard credentials and the legacy shared key are unaffected; a token deliberately scoped to content:write loses deletion, as do OAuth connector tokens (restorable via seowing/oauth/token_scopes). Breaking for scoped tokens that delete.
  • Local_Engine decoded 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 returns error as an object with code and message; sixteen returned it as a bare string, so a client reading error.message got nothing from those tools. The sixteen now use the helper, and normalize_tool_result() enforces the shape at the boundary as well — including results coming from the seowing/mcp/handle filter. 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 a WP_Error while the tools answer with an array, and the agent decided ok from is_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 and allows() treats null as deny, so every tool added via register_tool() or the tools filter was denied to every credential. A tool may now declare its scope (a scope key, or the seowing/mcp/required_scope filter). Default-deny is unchanged: a tool that declares nothing stays denied, a declaration cannot claim self_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 to MCPScopesTest drive 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 features array; License::features() and License::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 filter seowing/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_audit entitlement; 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 images entitlement. 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 _wpnonce field; on REST routes WordPress core intercepts any _wpnonce and verifies it against the wp_rest action, rejecting the whole request before the authorize callback runs. The nonce field is now named seowing_consent_nonce, so core ignores it and check_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 put seo_self_update or seo_delete_post back 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_run no longer writes to the database. A preview run of seo_auto_setup was persisting auto-detected company/social data because missing-info collection was always called with save=true. Preview now collects without saving. (includes/mcp/tools/class-autopilot-tools.php)
  • Data loss (Autopilot): auto_setup no 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_meta was broken on every call — it used the non-existent modules->get() instead of get_module(). (includes/mcp/tools/class-meta-tools.php)
  • REST settings save now reports real success/failure. POST /settings always returned success: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_structure read 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 #organization reference (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_match on 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 to purge_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_status meta 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. See DEEP-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.php drop-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 foreign fatal-error-handler.php already 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_file no longer accepts .html, and file-writing tools moved to an explicit files:write scope 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; /health returns only a status to unauthenticated callers.

Changed

  • Plugin self-protection: seo_self_update no 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_score tool all report the same score.
  • Plugin management tools (seo_list_plugins, seo_activate_plugin, seo_deactivate_plugin) with hard SEOWing self-protection, a plugins:write scope and a SEOWING_ALLOW_PLUGIN_MGMT opt-in.
  • Safe file browser (seo_fs_list, seo_fs_read, seo_fs_write): read-only browsing confined to the WordPress root with wp-config.php/.htaccess/.env blocked; writes confined to a non-executable sandbox with an inert-extension allowlist (no .php/.html/.js) and no delete, behind a files:write scope and a SEOWING_ALLOW_FILE_MANAGER opt-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 file includes/ai/views/assistant-page.php. class-ai.php shrinks 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-lift opt-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 properties as 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, empty properties becomes {}), 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_enabled were 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_option returns 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_OAuth but the autoloader maps that to class-mcp-o-auth.php (it splits before each capital), while the file is class-mcp-oauth.php, so the class could not be found and instantiating it fataled on every request. Renamed the class to MCP_Oauth to 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_audit analyzes 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_limit and seowing/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_audit tool 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_settings is 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 (group company / 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 EMPTY and 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 ⌘K keyboard 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+K focuses the composer; ⌘⏎ / Ctrl+Enter sends.
  • 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 chat button.
  • 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 Send button 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, code and 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 code spans 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_update and seo_delete_post are never exposed to the agent (filter seowing/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 FAQPage mainEntity entries. 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's extract_faq_pairs eligibility 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_ENFORCE constant, the seowing_license_enforce option, or the seowing/license/enforce filter. 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 URI header pointed at https://seowing.io so WordPress.org cannot hijack updates for the seowing slug.

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 author no longer reintroduces the service-account byline. When a post is created via a token request and an explicit author is 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-privilege seowing_mcp_service account — the exact byline bug 3.1.10 fixed.
  • Numeric author IDs are validated. A numeric author referencing a non-existent user is rejected (resolves to "not found") instead of being written as a dangling post_author.
  • seo_add_menu_item rejects broken object references. A post_type or taxonomy menu item now requires an object_id that references an existing post/page or term; previously a missing or invalid object_id created 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: only get_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

  • author parameter on seo_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_service user) 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; reassigns menu_order sequentially via a surgical wp_update_post that leaves the items' other fields untouched.
  • position parameter on seo_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. New categories, tags and terms parameters. 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 posts seowing_titles[...]), so Titles & Meta, Social, Sitemap, Schema and the MCP form fields saved an empty array, which save_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/me body instead of the normalized quota; now returns get_quota().
  • Local SEO "Business Info" tab rendered empty. Panels relied on a hidden toggle while the CSS only shows .seowing-tab-content.active; switched markup and JS to the .active convention.
  • 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_ajax nonce; the handler verifies the seowing_sitemap nonce in the nonce field. Passed it explicitly.
  • Sitemap: could not enable/disable individual types + settings never saved. Registered the missing seowing_save_sitemap_settings handler, 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_keys table was added in 3.1.5 (2f5eb6d) without bumping Database::DB_VERSION (last set to 1.2.0 in v1.5.29). On any site already at db_version 1.2.0, needs_update() returned false, so the upgrade path skipped create_tables() and the table was missing. Issuing a token then failed in MCP_Key_Store::create() ($wpdb->insert returns false → "Could not store the token."). Bumped DB_VERSION to 1.3.0 so the init upgrade 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, but ajax_error() returns the message at top-level res.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 only read), 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 with hash_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) in MCP_Scopes. A tool not in the map, or a credential lacking the scope, is rejected at the dispatch gate. The wildcard * never covers self_update. The 7 previously capability-less autopilot tools now each carry an explicit scope.
  • seo_self_update now requires the explicit self_update scope in addition to the existing SEOWING_ALLOW_SELF_UPDATE wp-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-glass restore_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-in SEOWING_ALLOW_SELF_UPDATE gate + zip-slip guard.
  • seo_get_settings: redact api_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 .htaccess and .js from 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_keys stores only hashes (no clear text anywhere).
  • wp_set_current_user(admin) is gone: a settings:write tool called with a read-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_update still 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 fires wp_head() more than once (e.g. nexsas, many page-builder themes). Root cause was a str_replace() in Schema::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 with preg_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() used get_the_ID() which can return 0 in wp_head before the Loop runs. Now prefers get_queried_object_id() (always reliable in wp_head on singular pages) with get_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_update priority 1) and restores any key that went from a real value back to empty during the same request (save_post priority 999 + elementor/document/after_save). Does not interfere with intentional changes — only restores wipes.
  • Filter seowing/meta/guard_enabled to opt out per post.
  • Action seowing/meta/guard_restored for 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_post without 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 legacy seowing_general.company_* keys. Both methods now hydrate from the canonical seowing_company group, so telephone, foundingDate, vatID, geo, and sameAs actually render when the Schema module is disabled.
  • MEDIUM Local_Business::generate() now inherits address, phone, email, geo, and price range from seowing_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 to get_bloginfo('admin_email') and get_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 from seowing_company / seowing_social / seowing_local.

Added

  • bin/build-zip.sh reproducible production build script. Required before every release. Old ad-hoc zip -r invocations missed mcp-bridge/ and every *.md file, which is why pre-3.0 ZIPs shipped at 9.7 MB instead of ~500 KB.
  • Real Tailwind v4 build pipeline. assets/css/tailadmin.css is now generated from src/admin.css by the standalone Tailwind CLI (bin/install-tailwind.sh to vendor it locally; bin/build-zip.sh invokes it on every release). Result: 36 KB instead of 124 KB (−71 %), only the utilities actually referenced by includes/ and assets/js/ are emitted, custom shadow-theme-* tokens are preserved, and the CSS is reproducible from source.

Critical post-rollout patches

  • Setup wizard rendered without styles because seowing_page_seowing-setup was missing from Admin::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 in src/admin.css as 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 in src/admin.css so every utility resolves correctly.
  • Replaced 87 off-scale text-[13px] / text-[15px] / text-[11px] arbitrary classes with the standard text-xs / text-sm / text-base scale. Forms, descriptions, and labels now sit on the type system instead of breaking it.
  • Description text contrast bumped from text-gray-400 to text-gray-500 in 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-motion respected). 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" and tabular-nums for 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_Keys constants 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_Wizard first-time onboarding flow at SEOWing → Setup. Triggered the first time an admin loads wp-admin after activation if seowing_company.name is 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 reads Module_Base::$dependencies and skips modules whose required dependencies aren't loaded. Skipped modules surface via get_dependency_failures() for admin notices instead of failing silently.
  • Capability checks on the six previously-unguarded seo_* MCP tools in class-seo-tools.php (analyze_page, calculate_score, bulk_analyze, bulk_calculate_scores, get_suggestions, suggest_internal_links). All require edit_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 in wp_options are 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, root package.json, root composer.json, .gitignore are 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 in seowing_company and 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.sh either — the script excludes itself.

## [2.5.65] — 2026-02-23

Changed

  • Split class-admin.php god 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.php god 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_Helpers trait — shared is_dach_site() and get_public_post_types()
  • Autopilot_Tools reduced to thin router (~280 lines) with lazy-initialized sub-classes
  • Autoloader updated with fallback for trait- prefix files outside the Traits folder

## [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 + requestAnimationFrame throttling
  • SEO score cached forever — now invalidated on save_post so score recalculates after content changes
  • Glossary query without limit — capped at 500 terms to prevent memory overflow
  • Nonce validation with sanitize_key() — replaced with wp_unslash() to preserve nonce integrity
  • Dynamic tag empty output on 404/archive pages — added wp_get_document_title() fallback
  • TOC widget data-settings double-escaped — use single-quoted attribute to avoid esc_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_meta custom table — all SEO data uses wp_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+ individual get_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_Keys constants class (includes/class-meta-keys.php) — centralized meta key definitions
  • indexnow and imagify settings groups in Settings class with migration from individual options
  • Static cache for entity auto-linking query — prevents unbounded posts_per_page=-1 on every the_content call, 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 — false was cast to 0, causing false-positive keyword_at_start
  • Fatal error in deactivate_module() when accessing ->is_core on a failed (cached false) module load
  • Module loading retry loop — failed modules are now cached as false to prevent repeated instantiation attempts

Added

  • CSS custom properties (:root block) 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 (&amp; etc.) appearing in JSON-LD schema output — added html_entity_decode() in Local SEO, JSON-LD, and Graph Builder
  • CPT archive pages now get proper hreflang tags via wpml_permalink filter

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_id fallback when icl_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_now MCP 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

  • ProfessionalService added to LocalBusiness schema types

Fixed

  • priceRange and image fields 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:image was outputting attachment ID instead of URL
  • Added og_image parameter to seo_update_meta MCP tool

## [2.5.50] — 2026-02-17

Added

  • Author archive 301 redirect to homepage (configurable)
  • Taxonomy/category noindex controls 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_TAG in 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 @type inside @graph arrays

## [2.5.44] — 2026-02-16

Added

  • @graph Schema 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 → name for 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_blogname sync 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_meta support in seo_create_post

## [2.5.29] — 2026-02-15

Added

  • Configurable HSTS header support

## [2.5.28] — 2026-02-15

Fixed

  • seo_generate_schema MCP tool now defaults to WebPage for pages (was overriding v2.5.27 fix)

## [2.5.27] — 2026-02-15

Changed

  • WordPress pages now default to WebPage schema instead of Article

## [2.5.26] — 2026-02-15

Fixed

  • Skip BreadcrumbList on 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)
  • @type case 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

  • lang parameter to seo_auto_setup for 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_setup now applies SEO/GEO best practices automatically

## [2.1.1] — 2026-02-08

Fixed

  • MCP parameter handling — accept both params and arguments keys

## [2.1.0] — 2026-02-08

Added

  • SEO Autopilot: seo_full_site_audit, seo_auto_setup, and seo_get_playbook MCP 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
SEOWing

AI-first SEO for WordPress. One plugin instead of Yoast, RankMath and Imagify.

GOLDENWING Creative Studios e.U.
Czeikestrasse 4/21, 1100 Vienna, Austria

Product

  • Features
  • Pricing
  • Download
  • FAQ
  • Blog
  • Changelog

Support

  • Documentation
  • Contact
  • Get started

Legal

  • Privacy Policy
  • Terms
  • Imprint
  • DPA
  • Sub-processors

© 2026 GoldenWing Creative Studios e.U. All rights reserved.

support@seowing.io