Unseat Connector

Beschrijving

Unseat Connector is the official WordPress plugin for the unseat.ai content platform from Unseat AI Corporation. It lets the platform publish and manage SEO-optimized content on your site, and exposes the signals AI crawlers (ChatGPT, Claude, Perplexity, Gemini) look for.

What it does:

  • SEO Meta Fields — Registers Rank Math, Yoast SEO, and SEOPress meta fields (title, description, focus keyword, social tags) for REST API read/write access. Works with or without an SEO plugin installed.
  • Site Health Endpoint — Provides a /wp-json/seo-machine/v1/status endpoint that reports WordPress version, active SEO plugin, permalink structure, and potential plugin conflicts.
  • llms.txt Serving — Serves your llms.txt and llms-full.txt pages as plain text at their canonical URLs, making your content index machine-readable for AI crawlers.
  • IndexNow Key — Serves your IndexNow verification key file as plain text.
  • AI-Crawler Allowlist — Injects an explicit robots.txt allowlist for GPTBot, ClaudeBot, PerplexityBot, CCBot, Google-Extended, Applebot, and other LLM crawlers with Crawl-delay: 0.
  • Fallback SEO Meta Tags — When no SEO plugin is active, emits <meta name="description">, canonical, OpenGraph, and Twitter Card tags from the connector’s meta fields.
  • Optional GA4 Tag — Injects a Google Analytics 4 measurement ID via wp_enqueue_script when configured through the REST endpoint.

Requirements:

  • WordPress 5.5 or higher
  • PHP 7.4 or higher
  • An unseat.ai account (for the publishing platform; the SEO meta and llms.txt serving features work without one)

External services

This plugin can optionally load Google Analytics 4 (gtag.js) from Google. This integration is off by default and is only activated after a site administrator sets a GA4 Measurement ID (format G-XXXXXXXX) through the plugin’s REST endpoint (POST /wp-json/seo-machine/v1/ga4). No external service is contacted until that ID is configured.

What it is used for: when a Measurement ID is configured, the plugin enqueues Google’s analytics script from https://www.googletagmanager.com/gtag/js so the site owner can measure website traffic.

What data is sent and when: while a Measurement ID is configured, the script loads on front-end page views for visitors who are not logged-in administrators (logged-in administrators are excluded to avoid inflating analytics). On each such page view the visitor’s browser sends the standard Google Analytics data directly to Google — for example the page URL, referrer, IP address, and device/browser information. No data is sent when no Measurement ID is configured, and the plugin itself does not collect, store, or transmit any of this data to unseat.ai or to any other party.

This service is provided by Google. By configuring a GA4 Measurement ID you are choosing to use Google Analytics on your site; please review Google’s terms and privacy policy:

  • Google Analytics Terms of Service: https://marketingplatform.google.com/about/analytics/terms/us/
  • Google Privacy Policy: https://policies.google.com/privacy
  • How Google uses information from sites or apps that use its services: https://policies.google.com/technologies/partner-sites

No other external services are contacted by this plugin. All SEO meta, llms.txt, IndexNow, robots.txt, sitemap, status, and cache-flush features operate entirely on your own server.

Installatie

  1. Install directly from the WordPress plugin directory, or upload the plugin zip via Plugins > Add New > Upload Plugin.
  2. Activate the plugin.
  3. Verify the connector is working by visiting https://yoursite.com/wp-json/seo-machine/v1/status (requires authentication).

The unseat.ai platform will automatically detect the connector during onboarding.

FAQ

Do I need Rank Math, Yoast, or SEOPress installed?

No. The connector registers SEO meta fields that work with or without an SEO plugin, and emits its own meta/OpenGraph/Twitter Card tags when none is active.

What is llms.txt?

llms.txt is a machine-readable index of your site content, similar to robots.txt but designed for AI language models. It helps AI search engines discover and cite your content.

Does this plugin collect any data?

The plugin itself does not collect or store any visitor data — it adds REST API endpoints to your existing WordPress installation, and all of that data stays on your server. The unseat.ai platform connects to these endpoints using your WordPress application password.

The one exception is the optional Google Analytics 4 integration: if an administrator configures a GA4 Measurement ID, the plugin loads Google’s analytics script so visitor analytics data is sent from the visitor’s browser to Google. This is off until you configure it. See the External services section above for exactly what is sent and links to Google’s terms and privacy policy.

Can I use this without an unseat.ai account?

Yes. The SEO meta field registration, llms.txt serving, and robots.txt allowlist all work independently. The status endpoint is primarily useful for the unseat.ai platform.

Beoordelingen

Er zijn geen beoordelingen voor deze plugin.

Bijdragers & ontwikkelaars

“Unseat Connector” is open source software. De volgende personen hebben bijgedragen aan deze plugin.

Bijdragers

Vertaal “Unseat Connector” in je eigen taal.

Interesse in ontwikkeling?

Bekijk de code, haal de SVN repository op, of abonneer je op het ontwikkellog via RSS.

Changelog

2.16.1

  • Fixed: on a site’s FRONT PAGE the founder’s entity links published nowhere. Rank Math anchors its per-page author byline at the page canonical, which on the front page is the home URL — so the byline looked like the site’s own identity node and, when the founder had written the homepage, carried a matching name. The connector treated it as the site’s Person, merged the links onto it and skipped creating the real one; Rank Math then deleted that byline as unreferenced and the links went with it. On a site running Rank Math the front page now publishes the same identity node every other page already did.
  • Also fixed: where that byline sorted ahead of the real identity node, the Organization’s “founder” pointed at the byline — a reference to a node that was about to be deleted. It now points at the identity node.
  • On a site with NO SEO plugin, where the connector rewrites the page head directly, it cannot add an identity node beside a byline it declines to use; on such a site the front page publishes no person links rather than writing them onto the byline. No current site is affected — the one site on that path anchors its identity elsewhere and is unchanged.
  • No change on any other page type, and none to what is stored or to sites whose knowledge graph is a Person (that node is matched by its own anchor, unchanged).

2.16.0

  • Entity data now also publishes on a site where Rank Math is INSTALLED but renders no schema graph of its own — a switched-off schema module, a theme that emits the identity nodes instead, or a page type Rank Math declines to describe. Previously the mere presence of Rank Math stood the head rewrite down in favour of a filter that never fired, so neither path published anything.
  • On any request where Rank Math does render its graph, output is unchanged: the filter path still owns that request, and the head rewrite still never runs alongside it.

2.15.0

  • Company and founder entity data now publish on sites that do NOT run Rank Math — Yoast, SEOPress, AIOSEO, or no SEO plugin at all. The connector rewrites the JSON-LD already in the page head rather than adding a competing block.
  • The site’s own person is now recognised by the founder’s name as well as by the #person anchor, so Yoast’s <home>#/schema/Person/{{ID}} is extended rather than duplicated.
  • The company node is created when a page claims none, which needs the company name — so this requires platform build 2.15.0 or newer on both sides. An older platform’s push is reported as unpublished rather than recorded as a success it did not get.
  • Rank Math sites are unchanged: they keep the existing filter path and the head rewrite does not run on them.
  • The SEO-meta fallback comment now reports the real plugin version instead of a hard-coded v2.5.3.

2.14.0

  • The seo-machine/v1/entity endpoint now also stores the founder’s name and job title, and the rank_math/json_ld filter builds the site’s Person node from them when — and only when — the site does not already have one.
  • This is the one case where the plugin creates a node rather than merging into one, and it is not a reversal of the rule above. Rank Math’s knowledge graph is either an Organization or a Person, never both, so a company site emits no #person node at all and there is nothing to merge into and nothing to contradict. Where the site DOES emit its own Person node, this creates nothing and merges as before.
  • A name is required. sameAs links hung on a nameless node are an identity claim about nobody, so with no founder name the person links stay unpublished and the site is left exactly as it was.
  • The new node is anchored at <home>#person, carries worksFor pointing at the Organization node the page actually emits, and the Organization node gets the reciprocal founder edge — but only if it does not already declare one. A founder the site owner has set is the site owner’s claim, not this plugin’s to overwrite.
  • Requires platform build 2.14.0 or newer on both sides: the endpoint echoes the identity back, and a platform that does not receive it reports the push as unpublished rather than recording a success it did not get.

2.13.0

  • New seo-machine/v1/entity endpoint (administrator-only) stores the two sameAs lists — the organization’s authority profiles and the founder’s — and a rank_math/json_ld filter merges them into the Organization and Person nodes Rank Math ALREADY emits, on every page of the site.
  • It merges and it never creates. If Rank Math emits no knowledge-graph block, this plugin emits nothing and the links stay unpublished. That is deliberate: a second Organization block claiming to describe the same company is an entity conflict, and search engines resolve a contradiction worse than they resolve a gap. Silence is recoverable; a competing claim is not.
  • The nodes are identified by the @id suffix Rank Math anchors them at (#organization / #person). @type is only consulted for a node carrying no @id at all: it is whatever the site owner picked in the setup wizard, and the LocalBusiness subtype list is effectively unbounded, so matching on it alone would miss every site that chose a specific one. An @id that does not match is a refusal, never a fall-through — a real page also carries <post-url>#author (a Person named after the WordPress login account) and #schema-NNNNNN duplicates of the Organization node, and writing into those would assert one identity from many conflicting nodes.
  • Existing sameAs entries come first and keep their relative order; ours are appended. De-duplication is case-insensitive and ignores a trailing slash, and it runs across the WHOLE list rather than only over what we add — otherwise appending https://x.com/acme beside an existing https://X.com/acme/ would publish the same profile twice. Two consequences worth stating plainly rather than discovering later: an existing entry has surrounding whitespace trimmed off it, and if the site’s own list already carried the same URL twice under different spellings, the second copy is dropped. Nothing else about an existing entry is rewritten, and none are reordered.
  • Stored URLs are validated on the way in, not on the way out: anything that is not http/https is dropped, and the list is capped. A stored value is emitted into the <head> of every page, so the endpoint is the only place that check belongs.
  • The endpoint echoes back exactly what it stored, and the platform treats a mismatch as a failed push rather than a success. ok: true on its own does not prove the links landed — a redirect can downgrade the POST to a GET that returns the site’s OLD list, and the sanitizer drops silently — and a marker written on that flag would freeze the site’s entity data permanently.
  • seo-machine/v1/status now reports how many entity links the site is holding, so the platform can confirm a push landed without re-sending it — and can tell “we never sent them” apart from “we sent them and there is no Rank Math block to merge them into”.

2.12.0

  • The seo-machine/v1/robots diagnostic no longer speaks a single English paragraph chosen by a branch chain. It returns findings — a list, worst first, where each entry carries the evidence it read and a message that may assert nothing outside it. Findings COMPOSE: none suppresses another, and severity sorts the list rather than steering control flow. cache_suspected and reason are gone.
  • This replaces the design, not a bug in it. Twelve review rounds each found a fresh defect INSIDE the previous round’s fix, and all twelve were one class: a claim that outran its reads. That was inevitable — the explanation was thirteen hand-written branches over eight mostly tri-state facts, a state space in the hundreds, where branch ORDER decided which sentence got spoken and every new branch silently pre-empted an older one. A structure that cannot be reviewed to correctness will keep producing correct-looking sentences about faults it did not observe, however many rounds it survives.
  • A whole-site block is now read correctly when this plugin’s own output is merged into it. RFC 9309 merges records naming the same user-agent, and this plugin injects Allow: /?unseat_sitemap= and Allow: /wp-admin/admin-ajax.php into the * group. A maintenance plugin emitting User-agent: * / Disallow: / therefore lands in the SAME group as those Allows — and the predicate, which bailed on any non-empty Allow:, concluded the site was not blocked and returned connector_governs: true for a site no crawler could enter. The plugin’s own output was masking a whole-site ban from the plugin’s own detector. Only an Allow: of the ROOT re-opens a Disallow: /; Allow: /blog re-opens /blog and leaves the rest of the domain dead, per longest-match.
  • serving_wp_output replaces cache_suspected. It is a byte comparison of what the live URL returns against what WordPress generates. The old field was derived from the # Injected by unseat-connector marker, which this plugin appends AFTER whatever blob won the filter — so the marker can sit on output we did not produce, and any verdict drawn from it is fabricated evidence.
  • When the live /robots.txt does not return a 2xx, every served-side fact is set to null at the source rather than guarded at each consumer. A read that failed is not a fact, and a diagnostic that has to remember to check served_code before each of nine other fields will eventually forget once.
  • No finding gives remediation that depends on a fact it did not read. A physical robots.txt on disk is reported as present and the operator is told to read it and compare it against rendered — not to delete it, because if WordPress’s own output is currently wrong that file may be the only thing keeping the site crawlable. A crawler group that escapes * is named and the rules are to be added INSIDE it — not to delete the group, because all this endpoint read is that the group fails to block query-string URLs, never what else it carries.
  • connector_governs is now a projection of the findings and is false whenever any critical one fires. Two fields computed separately from the same reads is how this endpoint once shipped connector_governs: true in the same payload as a sentence describing a de-indexed site.
  • A SEARCH ENGINE banned from the entire domain by name is now a critical finding. It used to produce NOTHING. The coverage pass exempts any named group that blocks its own bot — correct for User-agent: AhrefsBot / Disallow: /, which is banned harder than our rules and where “delete that group” would unblock a scraper somebody banned on purpose — but it applied the same reasoning to Googlebot, where being banned IS the emergency and not the reason to stop looking. A file carrying User-agent: Googlebot / Disallow: / returned an empty findings list and connector_governs: true, on a site Google cannot crawl one URL of. It is reported and never remediated: this endpoint has read that the ban is there, not why it is there, and a staging box bans Googlebot on purpose.
  • Disallow: ** is read as the whole-site block it is. Both recognisers match a literal set of spellings and the collapse step returned an all-star path untouched, so ** was in neither set: a site with every URL blocked was reported as one where “query-string URLs are NOT blocked” — the exact opposite of the truth — while Disallow: *, the same directive, fired two criticals. A banned Googlebot spelled that way got an escape lecture telling the operator to add query rules to the group that had already shut Google out of his domain. All-star paths now collapse to the one canonical spelling. (An EMPTY path is not an all-star path: a bare Allow: is an explicit no-op in RFC 9309, and collapsing it to allow: * would turn a line that re-opens nothing into one that re-opens everything.)
  • The whole-site * critical reads the paths its group re-opens, not just the fact of the ban. Disallow: / with Allow: /blog/ shuts every crawler out of everything EXCEPT /blog/ — and the finding said the domain was “removed from search results” and to fix the root Disallow: “before anything else”, which would have thrown the entire site open to crawlers somebody fenced into one subtree on purpose. Same defect as the named-bot ban below, one group over, and * is the commoner place to write a fence. The predicate behind it stays deliberately LOOSE — the strict one is what a competing plugin’s merged Disallow: / hid behind the connector’s own injected Allow: lines — so what had to learn to read the paths was the prose, not the test. On a connector site that list is usually our own two Allow: lines, and naming them is exactly right: they are the only URLs a crawler can still reach.
  • A de-indexed WordPress site is no longer told its robots.txt is a deliberate fence it should ignore. “Is this a fence or a shut-out?” was answered by asking whether the group re-opened ANY path — but WordPress core’s robots.txt default always carries Allow: /wp-admin/admin-ajax.php, and this plugin injects two Allow: lines of its own, so on a real site that answer was always YES. The honest branch was unreachable: a site with Disallow: / and every content URL blocked was reported as a fence around admin-ajax.php, told “if that fence is deliberate, you should ignore this”, and expressly forbidden to delete the root Disallow: — the one repair. The same masking bug as the merged-Allow: defect above, one layer out: the boilerplate that once hid a ban from the PREDICATE went on to hide it from the OPERATOR. A fence is now judged on the paths somebody CHOSE — boilerplate subtracted — for the * group and for named bots alike, so a hand-pasted User-agent: Googlebot / Disallow: / / Allow: /wp-admin/admin-ajax.php reads as the total ban it is. The boilerplate list is fixed rather than derived from our own rules(), because rules() is filterable and a third-party filter that widened it would SUPPRESS a real fence; a test fails if our own directives ever drift out of it.
  • Disallow: /*$ is read as the whole-site block it is. A * immediately before the end-anchor makes the anchor vacuous — * already absorbs everything up to the end — so /*$ matches every URL on the domain, but the collapse step left any $-terminated path alone and both recognisers missed it, reporting a fully blocked site as one whose query-string URLs were not blocked. A $ that does NOT follow a star is a real anchor and still survives untouched: /$ is the homepage and nothing else, and /*.php$ is not a whole-site block.

  • A search engine fenced into a few paths is no longer reported as one shut out of the site. Disallow: / plus Allow: /blog/ is not a ban: under longest-match that crawler reaches /blog/ and nothing else. The whole-site-ban critical fired on it anyway — told the operator the crawler “will not crawl a single URL on this domain”, which is false and checkable against his own GSC, and then told him to remove the whole-site Disallow:, which would have thrown the ENTIRE site open to a bot he deliberately fenced into one subtree. A critical whose remediation is the harm. The two cases are now two findings: a total ban (a root Disallow: and no Allow: at all) stays critical, and a fence is reported as a fence, names the paths it re-opens, and orders nothing.

  • Two sentences no longer claim to be exhaustive about evidence they never read. The whole-site * critical said “every crawler that obeys robots.txt is excluded” — false for any crawler with a group of its own, which is the entire bug this endpoint exists for. And the served-differs-from-generated major named a CDN cache and a physical file as “the two things that do this”, so an operator behind an nginx location = /robots.txt rewrite purged a cache that was not the cause and hunted a file that was not there.

  • A site with “Discourage search engines” ticked is no longer told to hunt a plugin that does not exist. Both injecting filters no-op when search-engine visibility is off, so the connector’s rules are absent BY DESIGN — and the report shipped two majors blaming a competing robots_txt callback for discarding them, ranking the one sentence that told the truth below both. The cause is now read from blog_public rather than assumed, and both findings declare it. This is not an edge case: it is every staging clone promoted to production with the checkbox still ticked.

  • A UTF-8 BOM no longer hides a whole-site block. trim() does not strip a BOM, so User-agent: * failed to parse as a user-agent line and every rule in that group was dropped as belonging to no group. A robots.txt saved with a BOM by a Windows editor and carrying Disallow: / was reported as a file that merely fails to block query strings — the opposite of the truth, with the whole-site critical silent.
  • serving_wp_output compares content, not raw bytes. A host that rewrites line endings changes no directive, and the byte compare called that “the live URL is not WordPress’s output” and sent the operator after a CDN that was serving exactly what WordPress generated.
  • The normalize/collapse/merge pipeline lives in one function now that two readers need it. It had already drifted once: the whole-site reader skipped the trailing-star collapse, so Disallow: /** was seen by one reader and missed by the other in the same response.
  • A banned Googlebot no longer gets a critical and a major that contradict each other. Two readers held two different notions of “totally banned” — one had learned that boilerplate Allow: lines do not count, the other had not — so User-agent: Googlebot / Disallow: / / Allow: /wp-admin/admin-ajax.php shipped a critical saying Google “will not crawl a single page on this domain” NEXT TO a major saying that group “does not block query-string URLs” and to add query rules inside it. The major is false — Google is already blocked from every URL, query-string or not — and following it made two findings disappear while Google stayed banned. Both readers now share one predicate, so the total-ban exemption and the total-ban critical can never disagree about the same group again.

2.11.0

  • Fixed the Rank Math robots.txt check, which read an option name that has never existed. Rank Math stores its custom robots.txt in rank-math-options-general (hyphens); this plugin read rank_math_options_general (underscores), following the spelling of every other Rank Math name it touches — the post meta and rank_math_modules really are underscored. get_option() on an absent name returns false, so the check reported “no Rank Math override” on the one site that had one, and the release below silently did nothing. The name now lives in a single constant, read and written from one place.
  • This was the whole of the revheat.com bug. Rank Math registers the robots_txt filter only when its stored content is non-empty, and then returns that content and discards everyone else’s. A stale blob pasted into its editor had been replacing this plugin’s rules for months, re-declaring a User-agent: Googlebot group that held only Disallow: /wp-admin/ — and robots.txt has no inheritance, so Googlebot obeyed that group, ignored *, and never saw the Disallow: /*? that would have kept it off ~60 junk query-string clones of the homepage.
  • The release route (POST seo-machine/v1/robots with release_override=true) now re-reads the option after clearing it and reports released from what the database says, not from what it tried to do. A write swallowed by a pre_update_option_* short-circuit or a read-only database previously reported success over an unchanged site.
  • seo-machine/v1/robots now observes the query-string rules directly (query_rules_in_rendered / query_rules_in_served) instead of inferring them from the # Injected by unseat-connector marker. The marker is emitted by the AI-allowlist filter alone; the Disallow: /*? rule comes from a different filter that carries no marker and bails silently on plain permalinks or a first group with no User-agent: line. A site could therefore carry the marker, report connector_governs: true, and still be leaking every query-string URL to Googlebot.
  • That check is anchored to GROUPS, not to the document. robots.txt has no inheritance: a crawler with its own User-agent: group obeys that group and ignores * entirely. A file can carry Disallow: /*? under User-agent: * and also declare a User-agent: Googlebot group holding only Disallow: /wp-admin/ — and Googlebot will crawl every query-string URL. That is precisely the file revheat.com served, so “the rule appears somewhere in the document” is not a check, it is the bug wearing a tick. The new query_rules_shadowed_by_served names each crawler that has walked out of the group the rules are in and can still reach a query-string URL.
  • Directives are compared per RFC 9309 — names case-insensitive, whitespace around the colon insignificant, paths case-sensitive. Records naming the same user-agent are merged, as Google’s parser merges them. An exact string match read disallow: /*? as absent and told the operator “query-string URLs are NOT blocked” about a file that blocks them perfectly well, which is the wrong answer to give about the one input this endpoint exists to read: a robots.txt somebody else wrote.
  • A group is only an escape if a query-string URL is still REACHABLE through it — never merely because it does not repeat this plugin’s rules verbatim. A group carrying Disallow: /*? without the Allow: /?unseat_sitemap= exception is stricter than *, not looser; so is one blocked from the whole site (User-agent: AhrefsBot / Disallow: /). Neither is reported. And where a group IS reported, the remediation is to add the query-string rules inside it — never to delete it. This endpoint reads one thing about a group, that a query-string URL can still be reached through it; it does not read what else the group carries. “Delete it, * already covers them” is a coverage claim nothing here checked, and against a User-agent: Googlebot group holding Disallow: /staging/ it hands Googlebot the staging site.
  • Of the groups that genuinely do escape, only a crawler that actually INDEXES flips the verdict. A bot that ignores * but files nothing into a search index is named in query_rules_shadowed_by_* as evidence and does not fail the site — a diagnostic that reddens a healthy install teaches operators to ignore it.
  • Every reason is written from the reads in the same response. The live URL’s HTTP status is now read, so a host that 404s /robots.txt is no longer told to purge a CDN; a physical robots.txt on disk is named even when the verdict is healthy, because a file the web server may be serving directly is a frozen copy that no future release can update; and no branch asserts what the live URL returns on a request where fetching it failed.
  • The sentence that endpoint speaks is now a pure function of the reads that produced it, so it can be tested without a WordPress install, a live HTTP fetch or a filter replay. It could not be before, and that is where every defect above was found — a diagnostic whose explanation is untestable will happily explain a fault it did not observe.
  • The release route now verifies its BACKUP before it clears anything, and refuses to clear an override it could not back up. It re-read the option it cleared but not the option it saved — the write whose failure cannot be undone. A host or security plugin that filters option writes by name could swallow the backup while letting Rank Math’s own option through, and the route would report released: true over a customer’s hand-written robots.txt that no longer existed anywhere. backup_option is also named only when a backup provably exists; it used to be asserted on every call, including calls that wrote none.
  • The verdict now reads the live URL’s HTTP STATUS, not just its body. A non-2xx response still has a body, and that body can be a perfectly correct robots.txt — a plugin fataling on shutdown after the file has already flushed returns HTTP 500 with the right content on the wire. Every content check passed and the verdict came back connector_governs: true, in the same payload as a reason reading “what came back is an error page, not the robots.txt crawlers read”. A response we could not make sense of is a read we did not make.
  • On a subdirectory install the endpoint no longer invents a fault. There, the rules are scoped (Disallow: /blog/*?), and a * group carrying the site-wide Disallow: /*? blocks strictly more while our Allow: exception survives untouched — a correct site. It was told its Allow: /?unseat_sitemap= was missing, that its fallback sitemap was unreachable, and was pointed at a URL that install does not use. The endpoint now names the rules it actually observed to be absent rather than guessing which one it was.
  • A group that blocks query-string URLs in a DIFFERENT SPELLING is no longer called an escape. Disallow: /*?* blocks exactly what Disallow: /*? blocks — a trailing * matches the empty string — and it is the commoner spelling in the wild. A User-agent: Googlebot group carrying it was reported as escaping, told the operator “query-string URLs are wide open” for a crawler that could not reach a single one, and returned connector_governs: false on a correct file forever. A trailing wildcard run is now collapsed before comparison. Only a trailing run: Disallow: /*?*=* needs a = and is genuinely narrower than our rule, so it is still reported.
  • The rules named in query_rules_missing_from_star_rendered are printed in the spelling this plugin emits, matching query_rules; they were being emitted lower-cased, so the two fields could not be diffed against each other. The field is null, not an empty list, when the filter replay threw and the * group was never read — an empty list reads as a clean bill of health on a request that looked at nothing.
  • The release route explains every failure it can have, not just one. A backup that verified while the CLEAR write was rejected — a settings-lock plugin guarding Rank Math’s option specifically — returned released: false with nothing to explain it, and the operator was left reading a reason about a competing robots.txt filter, which is true of the site but wrong about that call. release_blocked is now non-null in both failure states and says plainly which write was refused.
  • The backup verification now bypasses the object cache. update_option() seeds the cache with the value it just tried to store, so re-reading it handed back our own array out of memory and passed the check without the database ever being consulted — a verification that verified itself.
  • The Rank Math unhook matches a statically-registered callback too, not only an object instance — in all three callable spellings, including the 'RankMath\Foo::robots_txt' string. It exists to survive a priority change without becoming a silent no-op; testing only for an object left it one refactor away from exactly that.
  • A User-agent: * group that disallows the ENTIRE site (Disallow: /) is now read, named, and reported before anything else. Nothing in the endpoint had ever looked for it: the query-rule checks read the same file, found Disallow: /*? absent, and printed “Query-string URLs are NOT blocked” — the exact opposite of the truth — about a site where every URL is blocked to every crawler, without once mentioning the disallow that was removing the domain from search results. A staging robots.txt pasted into an SEO plugin’s editor is precisely how a live site arrives in that state, which makes it the single worst input this endpoint can be handed. It is now checked on both the rendered and the served copy, and it names its own cause: a stale edge copy, a competing callback, or a file on disk. It is measured straight off the body rather than off the query-rule pass, so a plain-permalink site — where this plugin emits no query rules and the coverage pass never ran — is no longer exempt from the check, which is how one could previously be handed a clean bill of health while de-indexed.
  • The equivalent-spelling collapse now applies to Allow: as well as Disallow:. Allow: /?unseat_sitemap=* is this plugin’s own rule — a trailing * matches the empty string — and it was being reported missing, which told a correct site that its Allow: exception was gone and its fallback sitemap unreachable. The same fabrication as the Disallow: /*?* case above, aimed at our own output instead of a stranger’s.
  • Disallow: *? is recognised as the same rule as Disallow: /*? — a leading * matches the leading slash. A User-agent: Googlebot group carrying it was called an escape and the operator told query-string URLs were wide open for a crawler that could not reach one.
  • release_blocked no longer tells the operator to disregard filters when the clear write is refused. The override IS still in place and still winning in that case — that is why they called the route — so the sentence contradicted the reason shipping beside it in the same payload.
  • The whole-site-ban verdict no longer tells an operator to delete the file that is keeping their site in …