| 开发者 |
kglundin
unseatai |
|---|---|
| 更新时间 | 2026年8月26日 00:12 |
| PHP版本: | 7.4 及以上 |
| WordPress版本: | 7.0 |
| 版权: | GPLv2 or later |
| 版权网址: | 版权信息 |
/wp-json/seo-machine/v1/status endpoint that reports WordPress version, active SEO plugin, permalink structure, and potential plugin conflicts.llms.txt and llms-full.txt pages as plain text at their canonical URLs, making your content index machine-readable for AI crawlers.robots.txt allowlist for GPTBot, ClaudeBot, PerplexityBot, CCBot, Google-Extended, Applebot, and other LLM crawlers with Crawl-delay: 0.<meta name="description">, canonical, OpenGraph, and Twitter Card tags from the connector's meta fields.wp_enqueue_script when configured through the REST endpoint.https://yoursite.com/wp-json/seo-machine/v1/status (requires authentication).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.
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.
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.
#person anchor, so Yoast's <home>#/schema/Person/{{ID}} is extended rather than duplicated.v2.5.3.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.#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.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.<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.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.@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.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.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.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".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.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./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.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.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.)* 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.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.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.* 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.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.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.Disallow: /** was seen by one reader and missed by the other in the same response.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.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.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.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.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.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.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.* 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.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.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.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.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.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.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.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.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.'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.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.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.Disallow: / while a sane physical robots.txt is what the web server actually serves, that file on disk is the only thing standing between the domain and de-indexing — and the physical-file branch, which fires first and does not read the ban, said "Remove the file at the host." Following that instruction publishes the ban. The whole-site verdict now outranks the physical-file, cache and competing-callback branches, names the file, and says plainly not to remove it until the Disallow: / is out of WordPress's output.cache_suspected, which is derived from this plugin's marker and knows nothing about that rule. When an SEO plugin's blob wins, this plugin's allowlist filter still appends its marker after it, so the marker IS in the rendered output; a pre-install edge copy lacks it; cache_suspected is therefore true, and the verdict told an operator "WordPress does not render that rule" while blocks_all_rendered: true shipped in the same response. They purge the CDN, the ban returns, and nothing pointed at the filter chain.* group ban the whole site" now run the identical comparison pipeline. One collapsed equivalent spellings and the other did not, so on Disallow: /** the one that gates the emergency branch saw no ban, stayed silent, and let the verdict fall through to "Crawling is safe" — about a site that disallows every URL to every crawler.\RankMath\Foo::robots_txt), which PHP treats as the same class. Without it the unhook silently does nothing and the release route's verification replay shows the state it just retired.seo-machine/v1/robots now reports EVIDENCE instead of a belief. It previously derived connector_governs from the absence of the two overrides it knew about ("no physical file, no Rank Math content, therefore we govern") — a guess, and a wrong one: revheat.com reported connector_governs: true while serving a stale hand-pasted blob containing the exact User-agent: Googlebot group the plugin exists to remove. A check that can only recognise causes it already has a name for reports "fine" for every cause nobody has named yet.rendered (what the robots_txt filter chain actually produces) alongside served (what a fetch of the live /robots.txt URL actually returns), and sets connector_governs true only when the plugin's marker is present in what a crawler really receives. Comparing the two localises the fault with no guesswork: rendered wrong means a competing robots_txt filter is winning; rendered right but served wrong means PHP never ran at all — a static file or a CDN/edge cache — and no amount of PHP will fix it.filters and actions fields list every callback registered on the robots_txt filter and the do_robotstxt action, with priority, file and line, flagging which are the plugin's own. When our rules go missing the cause is almost always another callback discarding them, and naming it turns a plugin-by-plugin bisect into a single HTTP request.connector_governs is now a TRUE/FALSE/NULL tri-state. Null means the loopback fetch failed and we could not find out — a site that blocks loopback HTTP must not be reported as "not governed", because "we could not look" is not "it is absent".cache_suspected is set when WordPress renders the correct file but the live URL returns a different one and there is no physical file on disk — i.e. an edge/CDN cache is serving a stale copy. This is the one cause PHP can neither see nor fix, and the release endpoint now says so out loud instead of reporting a successful release as a failure. (Cloudflare and GoDaddy WPaaS ignore a client Cache-Control: no-cache request header, so the fetch cannot bust it.)served_status and served_content_type. A 404, a 403, or an HTTP-Basic wall returns a body rather than an error, which previously made "the host 404s robots.txt" and "a CDN is serving the wrong file" indistinguishable. Bodies are capped at 8 KB.DOCUMENT_ROOT as well as ABSPATH. WordPress can live in a subdirectory, in which case the old ABSPATH-only check reported "no physical file" for a file the web server was serving at that very moment.Throwable guard, so a plugin that echoes cannot corrupt the JSON response and one that throws cannot 500 the endpoint. do_robotstxt is an echo hook by design and plugins routinely conflate the two.seo-machine/v1/robots endpoint: reports whether anything is shadowing the plugin's robots.txt output (a physical robots.txt file on disk, or a custom robots.txt stored by Rank Math — Rank Math replaces the whole file wholesale rather than appending to it), and can release a Rank Math override so the plugin governs robots.txt again. Releasing backs the old content up to the unseat_robots_txt_released_backup option first. It deliberately cannot WRITE robots.txt content — a stolen application password must not be able to inject Disallow: /.User-agent group that matches it and ignores User-agent: * entirely. The wp-admin / feed / wp-json rules are now repeated in every group the plugin emits, instead of being assumed to inherit.Disallow: /*?) to the User-agent: * group only, so search crawlers stop indexing cache-buster, internal-search, and tracking-parameter duplicates of pages that are already indexed. It is deliberately NOT applied to the AI crawler groups — several of those are user-triggered fetchers that retrieve the exact URL a person pastes into a chat, and those links routinely carry ?utm_source= / ?fbclid=. The block is also skipped entirely on sites using plain permalinks, where every post is itself a query-string URL.User-agent: * group, ahead of the Sitemap: line WordPress core appends at priority 0 (a blank line ends a group, and anything after it belongs to no group and is silently ignored by every crawler).GoogleOther group. Without one it fell through to User-agent: * and inherited that group's Disallow rules — the exact no-inheritance failure this release exists to fix.unseat_connector_robots_rules filter for themes and plugins to add a rule to every group. It receives the home_url() path prefix as a second argument — robots.txt paths are absolute from the domain root.home_url() rather than site_url(), so they are correct on installs where WordPress lives in its own directory.UNSEAT_CONNECTOR_VERSION and reported by the status endpoint (it had drifted from the plugin header, which made the deployed version unverifiable).unseat_connector_*), constants (UNSEAT_CONNECTOR_* / UNSEAT_*), options (unseat_*), and the enqueued script handle (unseat-connector-ga4) use the plugin's unique prefix. Added inline comments at each do_action() / function call into third-party plugin hooks (Rank Math, GoDaddy WPaaS, Cloudflare) documenting that those names are owned by those plugins and are intentionally invoked unprefixed, so they are not mistaken for unprefixed plugin code./wp-json/seo-machine/v1/status endpoint permission from edit_posts to manage_options, since it reports site configuration and software/plugin version details that should be limited to administrators.<title> dedupe and the sitemap Cache-Control TTL cap) are now explicitly closed on the shutdown hook via a level-matched check, instead of relying on implicit shutdown flushing — so they can never leave the output-buffer stack misaligned for other components.$_SERVER['REQUEST_URI'] through wp_unslash() + sanitize_text_field() and wp_parse_url(), matching the text-file handler.RankMath\Sitemap\Cache::invalidate_storage() (when present). Rank Math 1.0.272+ stores sitemap caches under indexed names the per-type invalidation misses — proven live by an 11-day frozen sitemap that survived every other flush; invalidating storage on each publish makes that freeze class structurally impossible.&v=): managed-host gateways rewrite no-cache headers in transit and CDNs cache the constant URL, so the cache key now rotates whenever content changes — an edge cache can never serve a stale copy to a crawler reading current robots.txt.uploads/rank-math/sitemap-cache/*.xml) and reports per-file deletion failures — live evidence showed a sitemap that survived post updates, transient clears, a module toggle, and a full wp_cache_flush(), which only a stale cached file explains.?unseat_sitemap=1 (no caching at any layer, so it can never go stale) and advertised via a Sitemap: line in robots.txt. Protects crawler discovery of new posts when the SEO plugin's cached sitemap freezes (observed in production: a persistent object cache ignoring per-key deletes left a sitemap frozen across post updates, transient clears, and a sitemap-module toggle).POST /wp-json/seo-machine/v1/flush (administrators only): performs a whole-namespace wp_cache_flush() — the one mechanism that clears an object-cache backend ignoring per-key deletes — then re-fires sitemap invalidation and a rewrite-rules flush. Intended for the unseat.ai platform's automated drift remediation.unseat-connector so existing installs upgrade in place.Cache-Control: max-age header on sitemap_index.xml and per-type sitemap responses so downstream CDNs revalidate sooner.unseat-connector so existing installs upgrade in place.Text Domain: unseat-connector header.parse_url() to wp_parse_url() for consistency across PHP versions.$_SERVER['REQUEST_URI'] with isset() + wp_unslash() + sanitize_text_field() before parsing.llms.txt page output through esc_html().wp_enqueue_script() + wp_add_inline_script() instead of raw <script> tags in wp_head./llms.txt and /llms-full.txt. On every save of the llms-txt and llms-full-txt pages the plugin now also writes a real file at the WordPress root (ABSPATH/llms.txt, ABSPATH/llms-full.txt) so hosts that strip .txt URLs before WordPress is invoked still serve the file..txt page)./llms.txt and /llms-full.txt 301-redirecting to trailing-slash variants. The plugin now suppresses WordPress's redirect_canonical for these paths so AI crawlers that don't follow redirects get the file directly on the canonical URL.robots_txt filter (GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, Perplexity-User, CCBot, Google-Extended, Applebot, Applebot-Extended, Amazonbot, Bytespider, Meta-ExternalAgent, FacebookBot, cohere-ai, cohere-training-data-crawler, DuckAssistBot, Diffbot, YouBot — each with Allow: /, Disallow: /wp-admin/, Crawl-delay: 0). No filter is injected when the site's "Discourage search engines" setting is on.