=== AI Rank Suite — AI Search, llms.txt, Schema & SEO ===
Contributors: namishams
Tags: seo, ai seo, llms.txt, schema, aeo
Requires at least: 6.4
Tested up to: 7.1
Requires PHP: 8.1
Stable tag: 0.2.45
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

AI Rank Suite shows whether AI assistants name your site, and fixes what stops them: llms.txt, AI page scores, schema and classic SEO.

== Description ==

More and more people ask ChatGPT, Claude, Perplexity or Google's AI Overviews instead of scrolling a page of blue links — and an assistant names one or two businesses in its answer, not ten. AI Rank Suite tells you whether yours is one of them, and fixes the things on your site that decide it.

That is the whole idea. An assistant does not "rank" your pages. It fetches a handful of URLs, reads them, and writes one answer. Four things decide whether you are in that answer, and this plugin is built around all four — with a complete classic SEO toolkit alongside, because Google still sends the clicks you have today.

= 1. Can an assistant find the right pages? =

An AI crawler pulls a few URLs per question, not your whole site. `llms.txt` is the one file where **you** choose which few.

* **llms.txt and llms-full.txt**, generated from your own content and served at your site root
* Pin your most important pages to the top, in the order you want them read — not in date order
* One sentence of meaning attached to every link, so the file explains your site instead of just listing it
* A live token count, so the file never grows past what a model will actually read
* Preferred citation, contact and content-usage lines that a model reads word for word
* **Control 31 named AI crawlers individually** — OpenAI, Anthropic, Google, Perplexity, Apple, Meta, Amazon, Mistral, plus ByteDance/Doubao, Baidu/ERNIE, Alibaba/Qwen, Huawei, Tencent and DeepSeek. Allow the ones you want citing you, block the ones you don't.
* Where a vendor publishes no crawler name, the plugin says so instead of selling you a rule that cannot be enforced

= 2. Can it read what it finds? =

Models read in chunks. A page with descriptive subheadings, a self-contained opening paragraph and a few lists or tables survives being summarised. A wall of prose does not.

* **An AI score for every page**, next to the classic SEO score, in the editor sidebar and the classic meta box
* Concrete checks, not a vague grade: does the opening paragraph answer the question in the first 40 words, is there roughly one subheading per 300 words, are any paragraphs over 120 words, are there lists or tables, is there a question-style heading, is this page even in your llms.txt
* Every check comes with the fix and what it is worth
* A site-wide list of every page and its score, filtered by what is actually broken — weak scores, missing descriptions, thin content, accidentally noindexed
* Runs on your own server. No AI key needed for any of the scoring.

= 3. Will it quote you by name? =

An answer needs a source it can name and a fact it can trust.

* **Structured data across 77 schema.org types** — Article, Product, Service, FAQ, HowTo, Recipe, Event, JobPosting, Course, Review, VideoObject, Person, LocalBusiness and more — set per page or as site-wide rules
* **Local SEO with 97 LocalBusiness types**, opening hours and multiple locations
* Open Graph and Twitter cards, social profiles, per-platform share images
* Automatic FAQ, speakable and video structured data from what is already on the page

= 4. Is it actually working? =

Everything above is a guess until you check it.

* **AI Answer Simulation (Pro).** Write the questions your customers really ask ("who does company formation in Dubai?"), and the plugin puts them to an AI model and reads the answer back: were you named, how early in the answer, in what tone, and **who was named instead of you**.
* Up to 25 monitored questions and 10 tracked competitors
* Every run is stored, so it becomes a trend line rather than a one-off screenshot
* Honest about what it is: a repeatable simulation against the AI model you configure. It is not a live scrape of the ChatGPT, Gemini or Perplexity apps, and nobody can promise you that.

= And everything you expect from an SEO plugin =

* Title and meta description templates, site-wide and per content type, with a live Google result preview
* XML sitemaps, plus news and video sitemaps, and an HTML sitemap block for readers
* Robots control per page, per archive, and a full robots.txt editor
* Google Search Console positions, Google Analytics 4, Matomo, Microsoft Clarity and PageSpeed Insights
* Instant indexing through IndexNow (Bing, Yandex, Seznam, Naver) and the Google Indexing API
* **(Pro)** A rank tracker with position history, and Bing Webmaster Tools alongside Search Console
* **(Pro)** Backlink monitoring from real referrer traffic, with optional connections to Ahrefs, Semrush, Moz, Majestic, Sistrix, DataForSEO and seven SERP APIs
* **(Pro)** Broken, redirected and unreachable link checking across the whole site
* A redirect manager with regular expressions and CSV import/export, plus a 404 monitor
* **(Pro)** An internal link graph — what links to what, how deep each page sits, which pages are orphans — computed entirely on your own server
* Breadcrumbs, image alt text and title templates, canonical control, cornerstone pages
* WooCommerce product schema **(free)**, plus Google Merchant Center and Meta catalog feeds **(Pro)**
* A cookie consent banner with Google Consent Mode v2, in opt-in (GDPR), opt-out (CCPA) or automatic mode
* **(Pro)** Scheduled email reports you can put your own agency branding on

= You do not have to switch SEO plugins to try this =

This is the part most people ask about first. AI Rank Suite is built to run **beside** Yoast, Rank Math, All in One SEO, SEOPress, The SEO Framework, Slim Seo, Squirrly, WPSSO, Schema Pro or Jetpack, not instead of them.

Turn it on and, by default, your existing SEO plugin keeps doing exactly what it does today — titles, descriptions, canonicals, Open Graph, its own schema. AI Rank Suite adds only the layer nothing else on your site is doing: llms.txt, AI crawler control, AI page scoring and AI Answer Simulation. Every part of your page's `<head>` gets exactly one owner, checked on the rendered page, so you never end up with two titles or two sets of structured data.

When you are ready to consolidate, one setting hands the whole `<head>` over. And if you want to move your existing work across first, the importer reads **Yoast, Rank Math, All in One SEO, SEOPress, The SEO Framework, Slim SEO, Squirrly, SmartCrawl, Genesis, Jetpack, SEO Ultimate, WP Meta SEO, Redirection and WooCommerce** — without deactivating or modifying the source plugin, filling only empty fields by default, and with an undo journal for every run.

= Built for real sites =

* Measured on a 50,000-post fixture: the analyzer, the link graph, the sitemaps and every admin screen hold up
* Long jobs (scoring, imports, the link graph, bulk work) run in resumable background batches with their own indexed tables — a timeout resumes where it stopped instead of starting over
* Works with WPML, Polylang, TranslatePress and Weglot, including the two different shapes those plugins use for translations
* Detects and reads Elementor, Divi, Beaver Builder, Bricks, Oxygen, WPBakery, ACF, The Events Calendar, WP Recipe Maker, WP Job Manager and more, so builder-rendered content is scored and put in your sitemaps
* SEO fields appear inside Elementor's own Page Settings, writing to the same values as the block editor sidebar
* Every module can be switched off, and a disabled module loads no code at all
* Your settings and data are **kept** when you deactivate or uninstall, unless you explicitly ask for them to be removed

= Free vs Pro =

**Free, forever.** Nothing in the free plugin expires, counts down, or stops working, and none of it needs an API key:

* Titles, meta descriptions and templates with a live Google preview
* Open Graph, Twitter cards, social profiles
* XML, news, video and HTML sitemaps
* All 77 schema.org types, Local SEO with opening hours and locations
* llms.txt and llms-full.txt, checked against Google's own Agentic Browsing rules
* Individual control over 31 named AI crawlers, with a served-file verifier
* The AI readiness score for every page — computed on your server, no key
* The full site audit and the content list with its filters
* Redirects with regex and CSV import/export, plus the 404 monitor
* Breadcrumbs, robots.txt editor, image SEO, canonicals
* All 14 importers — Yoast, Rank Math, AIOSEO, SEOPress and the rest — with undo
* WooCommerce product schema; WPML, Polylang, TranslatePress, Weglot
* Search Console, GA4, Matomo, Clarity and PageSpeed connections
* Cookie consent banner with Google Consent Mode v2

**Pro adds** the features that write with AI on your own key, and the measurement moat:

* AI Answer Simulation — 25 questions, trend line, share of voice, who was named instead of you
* AI Autopilot — writes every missing description, title, FAQ and alt text, with a full undo
* AI writing on your own key: descriptions, titles, FAQs, alt text, schema, translations
* Insights: trends, movers, issue leaderboard
* Rank tracker and keyword planner
* Backlink monitoring that re-reads the linking page
* Broken-link checker and internal link graph with suggestions
* Bulk editing, catalog feeds for Merchant Center and Meta
* Third-party SEO data providers (Ahrefs, Semrush, Moz, DataForSEO and more)
* Scheduled white-label email reports

Pro is sold per year through Freemius, starting at one site, with updates and support included, and a 14-day money-back guarantee. Details and pricing: https://airanksuite.com

= External services =

This plugin can talk to the third-party services below. **None of them are contacted unless you switch that feature on and supply your own credentials or identifier.** With a fresh install and nothing entered, the plugin makes no outbound requests except the ones WordPress itself makes. For each service: what it is for, what is sent and when, and its terms and privacy policy.

* **AI providers (Pro): OpenAI, Anthropic, Google Gemini, Mistral, xAI, Perplexity, Groq, Cohere, Azure OpenAI, DeepSeek, Qwen, and any OpenAI-compatible endpoint including a local Ollama, LM Studio or vLLM.** Used only when you enter an API key, and only for the actions you trigger: generating descriptions, titles, FAQs, alt text or schema, and running AI Answer Simulation. What is sent: the text of the page you are working on, after e-mail addresses, phone numbers, shortcodes, scripts and anything you wrap in `[airanksuite-private]` have been stripped out. Your key is stored encrypted. Terms and privacy: OpenAI https://openai.com/policies/terms-of-use and https://openai.com/policies/privacy-policy; Anthropic https://www.anthropic.com/legal/consumer-terms and https://www.anthropic.com/legal/privacy; Google https://policies.google.com/terms and https://policies.google.com/privacy; Mistral https://legal.mistral.ai/ and https://legal.mistral.ai/terms/privacy-policy/; xAI https://x.ai/legal/terms-of-service and https://x.ai/legal/privacy-policy; Perplexity https://www.perplexity.ai/hub/legal/terms-of-service and https://www.perplexity.ai/hub/legal/privacy-notice; Groq https://groq.com/terms-of-use and https://groq.com/privacy-policy; Cohere https://cohere.com/terms-of-use and https://cohere.com/privacy; Microsoft Azure https://azure.microsoft.com/support/legal and https://privacy.microsoft.com/privacystatement; DeepSeek https://cdn.deepseek.com/policies/en-US/deepseek-terms-of-use.html and https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html; Alibaba Qwen https://www.alibabacloud.com/help/legal and https://www.alibabacloud.com/help/privacy-policy. A self-hosted endpoint sends data only to the server you named.
* **Google Search Console API and Google Analytics Data API** (www.googleapis.com, analyticsdata.googleapis.com, analyticsadmin.googleapis.com, oauth2.googleapis.com, accounts.google.com): read-only, to show your search positions and traffic, and only after you connect a Google account — either with "Sign in with Google" (next entry) or with your own Google Cloud OAuth credentials. Sent: your OAuth token and the property you selected, each time the report screens load or the daily snapshot runs. https://policies.google.com/terms and https://policies.google.com/privacy
* **AI Rank Suite connect service** (airanksuite.com/google-connect), run by us, the authors of this plugin: used only if you press "Sign in with Google" instead of entering your own Google Cloud credentials. It holds the Google OAuth client, so that you do not have to create a Google Cloud project. Sent when you press the button: your site address, the plugin version and a one-time random value that ties the answer to your browser. Sent afterwards, whenever Google's access token has expired and a report needs a new one: your site address and the refresh token Google issued for your connection. It receives none of your Search Console or Analytics data. Those requests go from your site to Google directly. Disconnecting, or entering your own credentials, stops all contact. Terms https://airanksuite.com/agb and privacy policy https://airanksuite.com/datenschutz
* **Google tag / Google Analytics 4** (www.googletagmanager.com): if you enter a measurement ID, the Google tag is loaded on your public pages and visitors' page views are reported to Google, subject to the consent banner you configure. https://policies.google.com/terms and https://policies.google.com/privacy
* **Google PageSpeed Insights API** (www.googleapis.com): sends a page URL of yours to Google to fetch its performance score, only when you press the button. https://developers.google.com/speed/docs/insights/v5/get-started, https://policies.google.com/terms and https://policies.google.com/privacy
* **Google Indexing API** (indexing.googleapis.com): sends URLs of yours to Google when you ask for them to be indexed, using your own service account. https://developers.google.com/search/apis/indexing-api/v3/quickstart, https://policies.google.com/terms and https://policies.google.com/privacy
* **Google Places API** (places.googleapis.com): only in Local SEO, with your own key, to look up your business listing when you press the button. https://cloud.google.com/maps-platform/terms and https://policies.google.com/privacy
* **OpenStreetMap Nominatim** (nominatim.openstreetmap.org): only in Local SEO, and only when you press "Find coordinates from the address". Sent: the street, postal code, city, region and country you typed into the business address, and a user agent naming this plugin and your site address, as Nominatim's usage policy asks. It returns the latitude and longitude for your LocalBusiness markup; no key is needed. Usage policy https://operations.osmfoundation.org/policies/nominatim/, terms https://osmfoundation.org/wiki/Terms_of_Use and privacy policy https://osmfoundation.org/wiki/Privacy_Policy
* **IndexNow** (api.indexnow.org, www.bing.com/indexnow, yandex.com/indexnow, search.seznam.cz/indexnow): if you enable it, sends the URL of a page of yours and your IndexNow key to these search engines when the page is published or updated. Nothing else is sent. Terms https://www.indexnow.org/; Microsoft privacy https://privacy.microsoft.com/privacystatement; Yandex https://yandex.com/legal/confidential/; Seznam https://www.seznam.cz/ochranaudaju
* **Bing Webmaster Tools API** (ssl.bing.com): with your own API key, to read your Bing positions and inbound links when the report screens load. Sent: your key and your site URL. https://www.microsoft.com/servicesagreement and https://privacy.microsoft.com/privacystatement
* **Microsoft Clarity** (www.clarity.ms): if you enter a Clarity project ID, the Clarity tag is loaded on your public pages and visitor sessions are recorded by Microsoft, subject to your consent banner. https://clarity.microsoft.com/terms and https://privacy.microsoft.com/privacystatement
* **Matomo**: only if you enter the address of your own Matomo instance (self-hosted or Matomo Cloud). The tracking snippet reports page views to that instance, and the Reporting API is read with your token to show visits from AI assistants. Data goes to the server you named and nowhere else. Matomo Cloud terms https://matomo.org/matomo-cloud-terms-of-service/ and https://matomo.org/privacy-policy/
* **SEO data providers (Pro): Ahrefs, Semrush, Moz, Majestic, Sistrix, Ryte, DataForSEO, SerpApi, Serper, ScaleSERP, ValueSERP, Zenserp, TrajectSERP.** Optional, each with your own key, used to look up keyword positions and backlinks when you ask for them. Sent: your key and the keyword or domain you are looking up. Ahrefs https://ahrefs.com/terms and https://ahrefs.com/legal/privacy-policy; Semrush https://www.semrush.com/company/legal/terms-of-service/ and https://www.semrush.com/company/legal/privacy-policy/; Moz https://moz.com/terms-of-use and https://moz.com/privacy-policy; Majestic https://majestic.com/support/terms and https://majestic.com/support/privacy-policy; Sistrix https://www.sistrix.com/data-privacy/ (its terms are shown at sign-up); Ryte https://en.ryte.com/about/tos/ and https://en.ryte.com/about/privacy/; DataForSEO https://dataforseo.com/terms-of-service and https://dataforseo.com/privacy-policy; SerpApi https://serpapi.com/legal (terms and privacy on one page); Serper https://serper.dev/terms and https://serper.dev/privacy; ScaleSERP, ValueSERP and TrajectSERP https://trajectdata.com/traject-data-terms-of-service/ and https://trajectdata.com/privacy-policy/; Zenserp https://zenserp.com/terms and https://zenserp.com/privacy
* **Google Ads API (Pro)** (googleads.googleapis.com): only for keyword ideas in the rank tracker, with your own Google Ads developer token and customer ID. Sent: the seed keywords or the page URL of yours you asked ideas for, the language and the country. https://developers.google.com/google-ads/api/docs/start, https://policies.google.com/terms and https://policies.google.com/privacy
* **The link checker (Pro)** requests the external URLs your own content links to, in order to find out which of them are broken. Nothing about your site is sent beyond a normal HTTP request with this plugin's user agent.
* **Webhooks you set up**: if you add a webhook in the developer tools, the events you ticked (a site audit finished, a score was recalculated, a cache was purged) are sent as a signed JSON POST to the address you entered. Nothing is sent until you add one, and only to that address; its terms and privacy policy are those of whoever runs it.
* **Addresses you type in**: "Import schema from a URL" requests the one page you name and reads the JSON-LD in it. Nothing about your site is sent beyond a normal HTTP request with this plugin's user agent.
* **Your own site**: several checks (robots.txt, llms.txt, the sitemap, the head of a page, the coexistence scan) request pages of your own site from your own server, to test what a crawler would actually receive. These requests do not leave your server's network.
* **Freemius**: the licensing and update service for the Pro version only. The free plugin from WordPress.org contains no Freemius code and sends nothing to Freemius. With a Pro licence, the plugin contacts Freemius to activate the licence and check for updates; what is sent is the licence key, the site URL, the WordPress and plugin versions, and, only if you opted in, an admin e-mail address. https://freemius.com/terms/ and https://freemius.com/privacy/

Links from the plugin's screens to Google's Rich Results Test, Google Maps, Google Search Console and Bing Webmaster Tools open in your browser when you click them; the plugin itself sends nothing there.

= Building from source =

The two compiled files, assets/admin/build/index.js and assets/editor/build/editor.js, are built from the sources shipped next to them in assets/admin/src and assets/editor/src (React, JSX, one stylesheet) with Vite. To rebuild either one:

1. cd assets/admin (or assets/editor)
2. npm install
3. npm run build

Nothing else in the plugin is compiled: the metabox, the blocks and every PHP file are shipped as written.

= Honest about what nobody can promise =

No plugin can guarantee that an AI assistant will recommend you, or that Google will rank you higher. What this plugin can do is make your site findable, readable and quotable by the systems that write those answers, and then measure whether it worked — repeatably, so you can see a trend instead of guessing.

== Installation ==

1. In WordPress, go to **Plugins → Add New**, search for **AI Rank Suite**, and click **Install Now**, then **Activate**. Or upload the plugin folder to `/wp-content/plugins/` and activate it from the Plugins screen.
2. The setup wizard opens on first activation. It takes about three minutes and every step writes real settings — nothing is a preview.
3. **If you already run another SEO plugin**, the wizard detects it and defaults to running alongside it. You do not have to deactivate anything. Optionally, import your titles, descriptions, redirects and settings from it on the same screen.
4. On the **llms.txt** screen, turn the file on and pin the five or ten pages you most want an assistant to read. Check it at `https://yoursite.com/llms.txt`.
5. On the **AI Crawlers** screen, decide which AI crawlers may read your site.
6. On the **Content** screen, press **Analyze** to score every page. This runs in the background; you can leave the screen.
7. Optional: on the **AI Engine** screen, add an API key from OpenAI, Anthropic, Google or any OpenAI-compatible provider if you want AI writing and AI Answer Simulation. Everything else works without one.

**Requirements:** WordPress 6.4 or newer and PHP 8.1 or newer. The plugin will not load on older PHP and will tell you so rather than fataling.

== Frequently Asked Questions ==

= Does this replace Yoast or Rank Math? =

It can, but it does not have to, and on day one it does not. If another SEO plugin is active, AI Rank Suite defaults to adding only the AI-search layer — llms.txt, AI crawler control, AI page scoring, AI Answer Simulation — and leaves your titles, descriptions, canonicals, Open Graph and schema exactly as they are. Nothing about your current search results changes.

If you later want one plugin instead of two, switch the compatibility mode to let AI Rank Suite own the whole `<head>`, import your settings and content fields from the other plugin, and then deactivate it. Doing it in that order means you never have a gap.

= Can I really run it with Rank Math (or Yoast, or AIOSEO) installed at the same time? =

Yes, and that is the intended way to start. Every part of the page head — title, description, canonical, robots, Open Graph, Twitter cards, JSON-LD, hreflang — is assigned exactly one owner, and the assignment is enforced on the finished HTML rather than by hoping the two plugins use different hooks. So you will not get duplicate titles or two competing sets of structured data. Detection covers Yoast, Rank Math, All in One SEO, SEOPress, The SEO Framework, Slim Seo, Squirrly, WPSSO, Schema Pro and Jetpack.

= Do I need an OpenAI or Claude API key? =

No, and most of the plugin never asks for one. Page scoring, llms.txt, schema, sitemaps, the link checker, redirects, the internal link graph, Search Console, indexing, imports — all of it runs without any AI key, entirely on your own server.

A key is needed for exactly two things: AI writing (descriptions, titles, FAQs, alt text, schema suggestions) and AI Answer Simulation. You bring your own key from OpenAI, Anthropic, Google, Mistral, xAI, Groq, DeepSeek, Qwen, Azure OpenAI, OpenRouter or any OpenAI-compatible endpoint — including a model running locally through Ollama, LM Studio or vLLM, in which case nothing leaves your machine at all.

= What does it cost to run if I do use AI? =

You pay your AI provider directly at their list price; the plugin adds nothing on top. To keep that from being a surprise, the plugin estimates the cost of a job **before** you run it, records the actual cost after, and enforces spending limits — out of the box, $10 a day, $100 a month, and $25 for any single bulk job, with a hard stop when a limit is hit. You can change or remove all three.

In practice: a meta description on a small model costs a fraction of a cent, so writing descriptions for a few hundred pages is a coffee. AI Answer Simulation is the more expensive one because it asks a full model a real question — budget for it per run, not per page.

= What is llms.txt, and does anything actually read it? =

`llms.txt` is a plain markdown file at the root of your site — `yoursite.com/llms.txt` — that tells an AI system what your site is, which pages matter, and how you want to be credited. Think of it as robots.txt's opposite: robots.txt says what not to read, llms.txt says what to read first.

Honest answer on adoption: it is a proposed convention, not a ratified standard, and no AI vendor has publicly committed to honouring it. It costs you nothing to publish, it is trivially cheap to serve, and if the convention holds you are already there. The plugin also produces `llms-full.txt`, which contains the actual text of your chosen pages as markdown — useful today for anyone who feeds your site to a model deliberately. Neither file is the whole strategy, which is why page structure, schema and measurement are the other three quarters of this plugin.

= Does it work on a multilingual site? =

Yes. WPML, Polylang, TranslatePress and Weglot are supported, including the two different ways they work — WPML and Polylang give each translation its own post, TranslatePress and Weglot translate one post on the fly. Pages are scored, listed and sitemapped per language, the content list filters by language, and hreflang is handled. The admin interface itself is available in English and German.

= Will it cope with a large site? =

It is built for that case specifically. Every long-running job — page scoring, imports, the internal link graph, bulk edits, the link checker — runs as a resumable background job in its own indexed database tables, processing a slice at a time. A PHP timeout or a killed request resumes at the cursor rather than starting again. The link graph and the URL inventory were measured on a 50,000-post fixture with over 700,000 internal links inside a 128 MB memory limit, and every admin screen was checked at that size.

If your host is aggressive about request limits, everything continues on the next tick. Nothing here depends on WP-Cron running reliably.

= Is my content or data sent anywhere? =

Only where you send it. With no keys configured, nothing leaves your server.

When you do use an AI feature, the page's text is sent to **the provider you chose**, and only after e-mail addresses, phone numbers, shortcodes, HTML comments, scripts and anything you wrap in `[airanksuite-private]…[/airs-private]` are stripped out — the preview tells you exactly what was removed before anything is sent. Page content is also framed as untrusted data in the prompt, so text on one of your pages cannot issue instructions to the model.

Your API keys are stored encrypted at rest. All scoring, the link graph, link suggestions, sitemaps, schema and the audits run locally with no external call at all. The full list of services and what each one receives is in the **External services** section above.

= What happens to my data if I deactivate or uninstall? =

Nothing is deleted. Settings, scores, redirects, keywords and history are kept by default so that deactivating to test something, or reinstalling, never costs you work. If you genuinely want everything removed, there is an explicit switch for it under Settings → Data, and only then does uninstalling clear the plugin's options and tables.

= Will this make ChatGPT recommend my business? =

No plugin can promise that, and you should be wary of any that does. What this one does is remove the reasons an assistant would not: it makes your pages retrievable, gives them a structure a model can summarise without mangling, states in machine-readable form what your business is and how to cite it, and then measures whether your name shows up in answers to the questions you care about. The measurement is the point — you get a trend line rather than a hunch.

= Does it slow my site down? =

The front end does very little: meta tags, JSON-LD and the redirect check, all from cached values. Sitemaps, llms.txt and catalog feeds are cached and served from rewrite rules, not built on each request. The heavy work — scoring, crawling links, building the graph — happens in the admin as background jobs, never in a visitor's request. Any module you do not use can be switched off completely, and a disabled module loads no code.

= I run WooCommerce. What do I get? =

Product structured data with prices, availability, brand and reviews (including from Judge.me, Yotpo and the common brand plugins), a Google Merchant Center feed and a Meta / Instagram catalog feed served from cached URLs those platforms can poll, and a catalog audit that tells you which products are missing the fields those feeds require. The Catalog Feeds screen only appears when WooCommerce is active.

= Can I give a client or an editor limited access? =

Access follows WordPress itself: the plugin's screens and routes are for administrators, and a route that names a post also asks WordPress whether this user may read or edit that post. The plugin no longer ships capabilities of its own.

= I'm migrating from another SEO plugin. Will I lose my work? =

The importer reads Yoast, Rank Math, All in One SEO, SEOPress, The SEO Framework, Slim SEO, Squirrly, SmartCrawl, Genesis, Jetpack, SEO Ultimate, WP Meta SEO, Redirection and WooCommerce. It never modifies or deactivates the source plugin, so your old setup stays intact and reversible. By default it fills only fields that are empty here and tells you exactly how many values it left alone and why; every run is logged and can be undone. It also imports redirects from a CSV or JSON handover file, previewing what it would do before it does it.

= Does it support the block editor and page builders? =

Both. SEO and AI fields appear in the block editor sidebar, in the classic meta box, and inside Elementor's own Page Settings — all three writing to the same values. Content rendered by Divi, Beaver Builder, Bricks, Oxygen, WPBakery, Elementor and ACF is read for scoring and sitemaps, so builder-heavy pages are not scored as if they were empty.

== Screenshots ==

1. The dashboard: your site's AI search score, the three things worth doing next, and the numbers behind them.
2. Site Audit: every published page checked against 37 rules, the comparative problems a single page score cannot see.
3. The editor: an AI score and a classic SEO score for the page you are writing, with the Google snippet preview and every check spelled out.
4. The llms.txt editor: the file assistants read, with its token count, the pages it lists, and a live preview of exactly what a crawler receives.
5. AI crawler controls: the named crawlers from OpenAI, Anthropic, Google, Perplexity, Apple, Meta and others, allowed or blocked one at a time.
6. Every page with its score in one list, filtered down to what is actually broken: weak scores, missing descriptions, thin content, accidentally noindexed.
7. Structured data: schema.org types applied per page or as a site-wide rule, with a preview of the JSON-LD that will ship.

== Changelog ==

= 0.2.45 =
* The plugin's own capabilities no longer guard anything. Every REST route and every screen asks WordPress directly, and the capability stands in the route itself: `current_user_can( 'manage_options' )`. Routes that name a post also ask WordPress about that post.
* The Roles screen is gone with them. It handed out four capabilities that now decide nothing, and an installation that still carries them has them removed on update. Access follows WordPress roles: administrators manage the plugin.
* A test fails the build if a route is registered without a WordPress capability on the spot, or if one of the old capabilities is checked anywhere.

= 0.2.44 =
* Every REST route now asks WordPress for one of its own capabilities before the plugin capability is considered. A plugin capability can be handed to any role in Settings, Roles, so it can never be the only gate.
* Values read from $_SERVER and $_COOKIE are sanitized, not only unslashed.
* The audit export hands spreadsheet rows to the browser, which writes the file. Nothing is echoed from the route, and a cell that starts with an equals sign is written as text, so a spreadsheet cannot run it as a formula.
* llms.txt is stripped of markup on output.

= 0.2.43 =
* **Elementor no longer overwrites a title applied elsewhere.** Elementor keeps its own copy of the SEO title, description, keyword and noindex switch in its page settings and posts all of them on every save, layout edits included. Written back as they came, that copy put the old title back over one applied minutes earlier from the AI review, the block-editor sidebar or the content list. The panel now remembers what it handed the builder and writes only the fields that were changed inside Elementor. The meta box and the block-editor sidebar already behaved this way; a test now holds all three to it.

* **Long keyword lists fold.** A page can carry a hundred keywords, and the meta box and the sidebar drew every one of them as a chip, a wall that pushed the page down and a counter that read "100 / 10". Both now show the first twelve (eight in the sidebar), one chip says how many more there are, and the opened list scrolls inside a box. The limit reads 100, which is what it always was.

= 0.2.42 =
* **The SEO score is a formula.** Four pillars with fixed points, one hundred in total: keyword 40 (in the title, the description, the URL, the first paragraph, a subheading, the density and an image alt), title and description 25, structure 20 (one H1, heading order, 300+ words, alt texts, a compact slug), links 15. Until now the keyword checks fed no score at all, so a page with the keyword nowhere scored like one with it everywhere. Without a focus keyword the keyword pillar earns nothing and the score cannot pass 60. The four numbers are shown under the rings in the editor sidebar and the meta box.
* **llms.txt written in one click.** "Write everything" gives every page in both files a summary (from the AI engine when a key is stored, from the page's first sentences otherwise), writes the site's lead paragraph and rebuilds both files, with a progress bar. Pages that already have a summary are kept unless you choose to rewrite.
* **Site Audit: seven more rules, a way out of every finding, and a spreadsheet.** Meta description length, missing focus keyword, keyword missing from the title, very long slug, post without a featured image, no internal links out, oversized featured image. Every finding links to the optimiser as well as to the editor, and the whole run exports as CSV.
* **Measure has an overview.** Search Console and Analytics for the same days side by side, each against the same days before, the two lines on one axis, the top pages with both numbers. Bing sits next to Google: the Search Console property beside the site Bing expects, with Bing's import from Search Console one click away.
* **Image SEO counts the media library.** How many images have no alt text, are wider than 2500 pixels or heavier than 500 KB, the largest ones listed, and one run that writes the missing alt texts from the template, fifty at a time.
* **Import shows what is there.** The plugin that is installed or left data behind gets a card with the numbers and the buttons; the rest are one line each. Yoast and SEOPress have their marks.
* **The AI proposal compares against the page as it is now.** "Now" in the title and description review showed the snapshot the proposal had taken when it was written; after an earlier apply that was an old title, and the content list, reading the page, showed the real one. The review now reads the current values with every load and says when the metadata changed since the proposal.
* **Job postings carry what Google's report asks for.** datePosted from the post date, a job ID, the applicant country for remote jobs, and whether applying happens on the page.

= 0.2.41 =
* **The Pro package carries its translations again.** The 0.2.40 Pro build shipped without the languages folder, so every screen fell back to English on a German site. The build now refuses to run without the translation sources, and the release check verifies that the compiled catalogues are in the package.
* **The word counter is called Words again.** The tab under Content had been renamed Length; the counter of every page's words, and of the whole site, is the same tab and now says so.

= 0.2.40 =
* **The dashboard takes a score reading itself.** WP-Cron only runs when somebody visits the site, so on a quiet site the daily reading added in 0.2.39 could wait for days. When the dashboard opens and the last reading is older than twenty hours, it takes one on the spot. The findings card gained a button: "Run an audit now" starts one and opens Site Audit, where it runs slice by slice.
* **The editor toolbar shows both scores.** The pinned AI Rank Suite button carries GEO and SEO as two small pills in their verdict colours, and it sits first in the pinned row. With Rank Math active, its two pinned score buttons step back into the ⋮ menu, where its sidebar still opens, so the toolbar holds one set of numbers instead of two.
* **Keywords in bulk.** The keyword field in the meta box and in the sidebar accepts a pasted list (commas, semicolons or line breaks) and adds every entry at once, and the AI suggestions have an "Add all" button. Ten remains the ceiling.
* **The meta box and the sidebar look like WordPress.** Core blue for every button, the secondary as an outline of the same blue, 3 and 4 pixel corners, core input borders, no gradients and no tinted shadows. The score rings are honey rather than marigold, and the number sits in the centre of the ring in every browser: it is HTML laid over the ring now, not SVG text rotated back into place.
* **The post list says what Rank Math says.** The SEO details column adds the schema type the page will carry and three link counts as icons: internal, external and incoming. Score pills are top-aligned like every other cell, so they stay next to the title when another column makes the row tall.

= 0.2.39 =
* **The AI search score is read once a day.** The dashboard's "AI search score over time" card promised one point per recalculation, by hand or on schedule, and only the first half was true: a point was stamped when somebody pressed Recalculate, and on a site where nobody did, the line's last date stayed where it was for weeks. A daily job (`airanksuite_job_score_snapshot`, listed under Autopilot's systems) now takes a reading, so the line moves on its own. Recalculate still adds a point on the spot.
* **Both trend cards say when they last moved and when they move next.** Under the score card: the last reading and the next automatic one. Under the findings card: the last completed audit and the next scheduled one, or, when none is scheduled, that an audit has to be run or scheduled for the line to gain a point. A last label from three weeks ago is a schedule now, not a fault.
* **The "Open findings" tile updates with the chart under it.** Finishing an audit cleared the chart's cache but not the tile's, so for up to five minutes the chart showed the new run while the tile kept the old count.

= 0.2.38 =
* **Recurring events are scheduled on init, not on plugins_loaded.** Scheduling an event asks every plugin for its list of cron schedules, and WooCommerce translates the label of its schedules while answering. Asked during our boot, that made WooCommerce load its translations before init, which WordPress 6.7 and later report as a notice under WP_DEBUG, against this plugin. All eight recurring events (job resumer, retention, rank snapshots, backlink checks, listing refresh, reports, audit alerts, AI visibility) now go through one helper, `Cron::ensure()`, that waits for init. On a site without WooCommerce nothing changes; on a site with it the notice is gone.
* **Running an import needs a WordPress administrator.** 0.2.37 added that check to the settings import and the backup restore, and left it off the plugin importer, which can replace settings, redirects and post data just the same. Starting, ticking and cancelling an import run, and reading an uploaded file, now require `manage_options` as well. Listing the sources, detecting installed plugins, exporting and reading the backup stay with the plugin's own capability.

= 0.2.37 =
* **Changing who may do what needs a WordPress administrator.** The plugin has its own capabilities so that an agency can hand "manage settings" to an editor. That capability alone also opened the role editor, so somebody who had been given it could have given it, or anything else the plugin offers, to further roles. The role editor now requires WordPress' own `promote_users` as well. The same second check, `manage_options`, was added to the other routes that reach beyond the plugin's settings: writing and restoring .htaccess, importing a settings file, restoring a settings backup, the maintenance tasks under Developer tools, and creating, changing or testing a webhook, which sends site data to an address somebody typed in. WordPress is asked first, so a refused request does not use up the rate limit. For an administrator nothing changes.
* **Values reach JavaScript as JSON.** The toolbar's script received its REST address and nonce by text substitution into the source, and the Matomo snippet had its address and site id pasted between quotes. Both now get one JSON object built by `wp_json_encode()`. The addresses were the site's own, so nothing could be injected, but a value that is encoded for a URL is not encoded for a script, and only one of the two is right.
* **Structured data is printed with the default JSON encoding.** The JSON-LD in the page head was encoded with flags that leave slashes and non-ASCII characters as they are. With the defaults every slash is escaped, so no headline can close the script element, and the output is the same data to every consumer.
* **Six importer queries pass the table name as an identifier.** The readers for Redirection, Rank Math and All in One SEO appended the other plugin's table name to the statement. It travels as `%i` now, like everywhere else.

= 0.2.36 =
* **Everything the plugin names is named `airanksuite` now.** Options, tables, post and term meta, capabilities, scheduled events, hooks, constants, the REST namespace (`airanksuite/v1`), script handles and the WP-CLI command (`wp airanksuite`) used a four-letter abbreviation that the directory's review found too short to be safe from collisions with other plugins. **Your data moves by itself**: on the first request after the update, before any setting is read, tables are renamed, options and meta keys are carried across, capabilities and scheduled events follow. Nothing is copied row by row and nothing is left behind under the old names. Shortcodes already typed into pages, such as `[airs_breadcrumbs]` and `[airs_business_hours]`, keep working under their old names as well as the new ones, and so does the `[airs-private]` marker. A settings export or a pre-update backup made by an earlier version restores under today's names. If you wrote code against the old hooks, constants or REST paths, replace `airs` with `airanksuite` in it.
* **A route that names a post asks WordPress about that post.** A plugin capability says what somebody may do with the plugin, not which posts they may see. Reading a post's schema or its PageSpeed result needed only "view reports", so a role with that capability could read them for a colleague's private draft. Those routes now require `read_post` on the post itself, and writing requires `edit_post`. The same check was added where a post id arrives in the body or in a list: applying an internal link suggestion, rejecting or undoing an Autopilot proposal, a bulk run over chosen ids, submitting a URL for indexing. The Autopilot queue and log, the content list and its CSV leave out drafts the current user could not open in WordPress.
* **No SQL statement is assembled any more.** The redirect list built its ORDER BY from the sort the screen asked for. The value came from an allow-list, so it was never injectable, but a statement put together from pieces cannot be told apart from one that is. Every list in the plugin now sends one constant statement: the sort column travels as an identifier placeholder, the direction selects one of two literals, and a filter that is not in use is switched off by a flag instead of being left out of the text. Same results, same order, same paging.
* **The sitemap's browser view is a file.** The XSL stylesheet behind `/sitemap.xsl`, CSS included, was a string inside the PHP source; it is `assets/sitemap/sitemap.xsl` now, and PHP only serves it.
* **No translation is loaded before init.** A cron interval label was translated while modules scheduled their events on plugins_loaded, which WordPress 6.7 and later report as a notice under WP_DEBUG. The interval was a copy of WordPress' own `weekly` and is gone.
* **Four services were missing from the list of external services**, and are documented with what is sent, when, and their terms and privacy policies: OpenStreetMap Nominatim ("Find coordinates from the address" in Local SEO), our own connect service behind "Sign in with Google", the Google Ads API (Pro keyword ideas), and the webhooks you configure yourself.

= 0.2.35 =
* **Every database query goes through $wpdb->prepare().** 107 reads and writes were built as strings with the table name interpolated and no placeholder at all. None carried user input, but a query that is not prepared cannot be told apart from one that should have been, and the directory's review said so. Table names are now passed as %i identifiers, IN lists get one placeholder per value, and LIKE patterns go through esc_like(); nothing is pasted into SQL by hand any more.
* **No plugin CSS is printed into the page.** The upgrade menu entry's colour, the boot skeleton behind the admin app, the upgrade screen's stylesheet and the post-list score columns were each an echoed <style> tag; all four are registered handles with inline styles now, so the style loader and any optimiser can see them.
* **The head arbiter cannot leave its output buffer open.** If another plugin opened a buffer above ours and never closed it, finish() gave up and the plugin's own ob_start() stayed open for the rest of the request. Foreign buffers are flushed through first now, and a shutdown guard closes ours if nothing else did.
* **The admin app never guesses where admin-ajax.php is.** The endpoint always came from PHP, but the script kept a literal /wp-admin/admin-ajax.php as a last resort, which is wrong on every renamed or subdirectory admin. The fallback is gone.
* **The compiled admin and editor bundles ship with their source.** assets/admin/src and assets/editor/src, the two Vite configs and the build steps in the readme, so anyone can read or rebuild what runs.
* **Every external service is documented with its terms and privacy policy**, host by host: the Google APIs, the Google tag, IndexNow and its four endpoints, Bing Webmaster Tools, Microsoft Clarity, Matomo, Places, PageSpeed, the AI providers and the SEO data providers.
* One review platform that has withdrawn API access from all plugins no longer appears in the local directory suggestions.
* **Two answer-simulation reads were behind the licence gate.** The trend and the competitor matrix are reports on data already collected, and an inactive licence is meant to pause new work, never to hide what exists. Both read freely now, as every other report does.
* **A site that moves from Pro to the free edition no longer fires empty cron events.** The rank snapshot, the answer simulation, the e-mail report and the backlink re-check stayed in the cron table with nothing listening, so WordPress ran them into nothing on schedule for ever. Any scheduled event of this plugin's with no module behind it is removed once, the first time the plugin loads after the change.
* **The free edition's admin no longer asks for routes it does not have.** The dashboard card, the content screen, the analyzer and the licence tab each requested an endpoint that only exists with a Pro module installed and left a 404 in the browser console; each asks the module list first now, and the licence tab says what the free edition is instead of reading a licence that cannot exist.
* The 404 monitor explains what it records and where an entry goes, instead of showing one line when it is empty.
* The free edition never ran its uninstall cleanup. The Pro build wires it to Freemius' own uninstall hook inside a file the free package does not carry, so the "remove my data on uninstall" setting there was a switch connected to nothing. The free edition registers WordPress' uninstall hook itself now; data is still kept unless you opted out of keeping it.
* Nine explanatory captions added in 0.2.34 (the score ring, the four counters, the three trend notes, the directories hint in Local SEO) had no German translation and showed in English on German sites. They have one now.

= 0.2.34 =
* **The dashboard says what it is connected to, and what is being done with AI.** The card that reports the agent now carries the wiring underneath it: a row of every service the plugin is plugged into — the AI engines, Search Console, Matomo, Analytics, Bing, IndexNow, WooCommerce — each with what stops working while it is dark, fed by one moving artery that carries current only while an engine can actually answer. Under it, the ten kinds of work the plugin does with a model and which engine each one goes to. It is drawn with or without a key: without one, the same diagram greys out, which is the argument for connecting one.
* **Every figure now says what it counts.** Calls today, spend today, the run's progress, the run's cost, an engine's load, the score in the ring, pages scored, open findings, broken links and each of the three trend lines carry one sentence explaining what is measured, over what window, and whether the number is metered or forecast.
* **Five figures were measuring the wrong thing.** "Calls today" and "Spent today" counted from UTC midnight while the site keeps its own clock, so in the Gulf every morning until 4am reported the wrong day. A finished run went on advertising the estimate it opened with instead of the metered cost of the calls it actually made, and showed a full progress bar beside the word "idle". The search clicks sparkline selected the *oldest* thirty snapshots, so on any site with more history it froze on its first month for ever. The findings line plotted a run that was still crawling, drawing a cliff that reversed itself an hour later, and labelled its points in UTC while the score line beside it used the site's timezone. The coverage line bucketed its days in UTC for the same reason.
* **On a translated site the desks were invisible.** The panel matched each job against the *translated* name of its desk, so on a German or French install nothing matched and the whole lower half of the diagram rendered empty, with no error anywhere. Jobs now carry a language-independent key. The engine load bar likewise divides by a total the server sends rather than by the length of whatever list the screen happened to be holding.

= 0.2.33 =
* **The compliance work from the WordPress.org review reaches the Pro edition too.** Analytics tags are enqueued rather than printed into wp_head, so they are visible to the script loader and to caching layers, and Consent Mode's requirement that its default runs before the Google tag is a declared dependency instead of a hook priority that another plugin could land inside. The cookie banner's stylesheet moved to the head, where it belongs, and no longer arrives after the banner it styles. Five reads of `$_SERVER` that carried a `phpcs:ignore` are sanitized through one helper that unslashes before it sanitizes.
* **The licence gate cannot be filtered into refusing.** `airanksuite/licensing/allows` still lets a host or an agency widen access; it can no longer narrow it, so a third-party snippet cannot lock a customer out of something they have paid for.

= 0.2.32 =
* **The WordPress.org edition carries no licensing code at all.** Directory guideline 5 forbids a hosted plugin from locking any feature behind a licence, and says the locked code must not even be present. The build already left the Pro modules out; it now also leaves out the licensing module, the Freemius SDK and the licence bootstrap, so no code path remains in the free package that could answer "no" to a feature. The licence gate additionally refuses to enforce anything, and refuses to be filtered into enforcing anything, without the Pro build's own marker.
* **Analytics tags are enqueued instead of printed.** The Google tag, its configuration, Microsoft Clarity, Matomo and the consent banner used to print `<script>` and `<style>` straight into `wp_head`, which makes them invisible to the script loader and to every caching or optimisation layer. They register handles and attach inline code now. Consent Mode's requirement that its default runs before the Google tag is expressed as a dependency rather than a hook priority, so nothing can land between them, and the cookie banner's stylesheet moved to the head instead of arriving after the banner it styles.
* **Request input is sanitized rather than excused.** Five reads of `$_SERVER` were annotated with `phpcs:ignore` instead of being cleaned. Headers and the request URI are attacker-influenced like any other input, and they now go through one helper that unslashes before it sanitizes, in that order.
* **Translations come from the directory.** WordPress.org generates and serves language packs for a hosted plugin, and a bundled catalogue silently shadows whatever the community contributes. The free package ships the `.pot` for translators and nothing compiled.
* **A plugin role could edit posts WordPress would not open for it.** The SEO editor preset grants "edits titles, descriptions and keywords"; it never said *which* posts, and the content route did not ask. A user holding that capability could rewrite the SEO title, description, slug, robots and canonical of any post over the REST API, including somebody else's private draft, and read that draft's text and previews from the same screen. The route now makes the object check WordPress makes: `edit_post` before any write, `read_post` before an unpublished post is shown at all, and an unreadable id answers "not found" rather than confirming it exists. Two tests hold it from both sides.
* **A clean audit no longer shows the last one's findings.** The dashboard tile derived the run from the newest row in the findings table. A run that finds nothing writes no rows, so the newest row still belonged to the run before it and the number stayed up after everything was fixed; a run in progress, conversely, was counted half-finished. The tile now reads the run the auditor publishes when it completes, which is the same run the audit report shows. No finished run, no number.
* **A large redirect import cannot time out half-applied.** Fifty thousand rows, each a lookup and a write, do not fit in one PHP time limit, and what a timeout left behind was some redirects live, the rest missing, and nothing on screen saying which. The import now applies two thousand rows per request and answers with where to continue; the screen walks through the slices and reports the totals as before.

= 0.2.31 =
* **The page drawer no longer runs off the right edge.** On a window around 1100px wide the drawer's own column was sized by its content rather than by the drawer, so the header chips, the tab strip and the whole right-hand rail were drawn 40px past the edge and cut off; the search preview looked clipped and the view switch broke into two rows. The drawer's column is now an explicit track that cannot exceed the drawer, every grid inside the rail is the same, the preview frames fill the rail, and the view switch reads Google, AI answer, Social, X on one line.
* **Titles show their dash, not `&#8211;`.** WordPress hands the admin a texturised title with the dash as an HTML entity, and the drawer header, the list row and the SEO title fallback printed it literally. Decoded once on the server, so every screen gets the same plain text.

= 0.2.30 =
* **The admin looks like WordPress again.** An earlier pass gave the plugin its own typeface, its own violet and softer, rounder surfaces so it would stop looking like every other SEO plugin. It worked, and it was the wrong goal: the people who use this are WordPress administrators, and what reads to a designer as identity reads to them as a SaaS dashboard dropped into their admin. The typeface is now the admin's own, the working colour is core's blue, the radii and card shadows are core's, and the violet survives only as the accent on the score ring. The upgrade page keeps the website's look on purpose; that page is the shop window.
* **The dashboard verdict now sees the audit.** It read site checks, broken links and missing descriptions and then declared "no failing check, no broken link and every page described" on a site whose audit, one click away, held 43 warnings. Critical findings now outrank everything but a disconnected engine; warnings sit just below a broken link.
* **Every calculation was read end to end.** The AI-search score, the audit score, the page GEO and SEO scores, the library averages, the numeric sort and the null handling all check out; the arithmetic is consistent (pass 1, warn ½, fail 0, weighted) and unanalysed pages are excluded from averages rather than counted as zero. One accessibility slip: the score ring told screen readers "0 / 100" for a page it drew as "not yet scored". Fixed.
* **Every route the screens call exists** (176 of 176) and every field they read is one the server sends. Six more strings were built as English fragments and are translated now, including the dry-run summary on the autopilot.

= 0.2.29 =
* Build housekeeping only, no change to the plugin itself. The release script wrote the finished build into one folder and the second copy was made by hand afterwards, so that folder held the version before the one just shipped, every time. Both are written by the script now, and both keep the newest three builds.

= 0.2.28 =
* **The dashboard said "0 open findings" while the audit it linked to reported 335.** The tile counted the site checks, a handful of foundation tests, but wore the audit's label. The first number anyone looks at gave an all-clear that was wrong by 43 warnings and 292 notices. It reads the audit now, from the same query as the chart below it, so the two cannot disagree. A test holds the arithmetic.
* **"Pages scored over time" stopped at 20,000 pages and started its total at zero.** Three faults in one chart: a hard stop after 20,000 analyses with nothing on screen saying so, pagination by OFFSET with no ORDER BY (so two loads of the same site could draw different curves), and a cumulative line that ignored everything scored before the last sixty days — drawing a site with 40,000 scored pages as a line rising from nothing. Stable keyset pagination, no silent ceiling, and the curve starts where the site already was.
* **More English on a German install:** the audit's "156 pages · 30 rules · 21 passing" line, "nothing outstanding", the severity counts, and "Page 1 of 7" on two screens. All were built as template literals, which is why they were invisible to the translation test as well.

Found by an independent audit of the 0.2.27 release. Every finding above was reproduced before it was fixed.

= 0.2.27 =
* **The AI can no longer write a meta description longer than the limit.** It was asked for 120-155 characters and returned 196, and the editor drew its own red bar under it: the product told you the text was too long and had written it itself. A prompt is a request, and the JSON schema was no better — OpenAI's structured outputs ignore a maximum length on a string. The length is enforced in code now, after the model has answered, on titles and descriptions from every path: the editor, bulk, the autopilot and translations.
* The cut ends on a full sentence where one fits, on a whole word otherwise, and drops a word left hanging by the cut, so "for entrepreneurs and" becomes "for entrepreneurs". No ellipsis is added; Google adds its own.
* The schema also allowed 70 characters of title and 170 of description while the screens drew their limits at 60 and 160. All four numbers now agree.
* Fifteen strings were still English on a German install, including the character-limit labels. Translated.

= 0.2.26 =
* **"Fill in street and number" did nothing.** Its whole action was switching to the tab that field lives on — and that is usually the tab you are already looking at, so the click set the tab to itself, scrolled nowhere, and moved nothing on screen. It goes to the field now and puts the cursor in it.
* **The bulk job card said "Finished" in English on a German site.** Its three states — working, finished, cancelled — were written as plain strings and never marked for translation, so no translator ever saw them and the card printed English above a German subtitle. Four more visible strings had the same problem: the Head tab's copy button, the licence link, and the snippet-length options.

= 0.2.24 =
* **The link graph could not be built on some hosts, and never said so.** Two of its phases created temporary tables, which needs a MySQL privilege plenty of shared hosts do not grant — so the build died there every time. It used ordinary tables now, which need no special right. The run holds a database lease, so two builds cannot overlap.
* **A failed graph build was indistinguishable from one nobody had started.** The reason was written to the run row and never read back, so the screen said "the link graph has not been built yet" and offered the button again — for ever, with no hint that anything had gone wrong. It says what happened.

= 0.2.23 =
* **"Write 3 descriptions (AI)" did nothing.** The count behind that button includes every post status and the run it starts only took published posts, so a site whose three gaps were three drafts was offered three descriptions and then told, on the very next screen, that there was nothing to do. The run takes drafts, pending and scheduled posts now: a description written for a draft is live the moment the post is, which is exactly when it is needed and far too late to start writing.
* **"Show broken links" appeared to do nothing.** It set the filter on the results table correctly and stopped there, and that table is far below the summary the button sits in — so nothing moved on the screen the reader was looking at. It now goes to the results it just filtered.

= 0.2.22 =
* **An SEO title improved with the AI was audited against the title it replaced.** Whether a page's analysis is out of date was judged by the post's modified time, and writing post meta does not change that — so applying a new title stored it, showed it in the field, and then went on failing the checks for the old one while neither score moved. Any change to a scored field now marks the analysis as out of date, and the next read recomputes it.
* **Applying an AI proposal left the content list showing the previous state.** The apply writes to the database itself and returns what it stored, but the drawer only updated its own fields, so the row behind it kept its old title indicator, description indicator and scores until the page was reloaded. The Head tab and the schema panel were stale for the same reason and now refresh too.
* **The content list showed the WordPress title where the SEO title was expected.** Editing the SEO title changed nothing visible in the list, because that column was never showing it. The SEO title is now shown beside the page title whenever the two differ.
* **The link scan looked stuck at 100%.** It was not — after the last destination the run needs one more pass to find nothing left and write its results, and that pass follows a batch of HTTP requests, so it can take a while. Meanwhile the screen showed a full bar over the words "checking destinations 215 / 215", which reads exactly like a scan that has hung. It now says it is writing the results. The line underneath also counted link occurrences while the bar counted unique destinations, so the same run reported "215 / 215" and "403 checked" side by side; both now say which they mean.
* **Seven more buttons and labels lowercased German nouns.** 0.2.21 fixed the one that had been reported — "firmenname ausfüllen" — and the same call sat in seven other places, so "straße und hausnummer ausfüllen" turned up next. Every label is now shown as written; the remaining lowercasing is comparison for search and de-duplication, which never reaches a screen.
* **A stored AI proposal from an older version could make a page unopenable.** The review panel read straight through the proposal's `original` and `model` blocks, which older proposals do not carry, and because that happens during render the whole screen was replaced by an error rather than one panel missing a line. Every read is guarded now.
* **The suggestions panel on the Checks tab was empty.** It read two fields the server has never sent, so every suggestion rendered as nothing: a heading above a column of blank rows. It reads the title and detail that actually arrive.

= 0.2.21 =
* **The eight explanations on the link structure screen were English on every install.** They were never wrapped for translation, so the translation test could not see them either: an unmarked string is not a missing translation, it is not a string at all as far as the test is concerned. All eight are translated now.
* **Local SEO offered a button reading "firmenname ausfüllen".** It lowercases the field name, which reads well in English and is a spelling mistake in German, where every noun is capitalised. It uses the label as written.

= 0.2.20 =
* **The link structure filters said "Orphans (undefined)".** They asked the report for counts it has never returned; it returns the lists. Two of them said "(0)" instead, which was worse: a confident nought beside a table with rows in it. They count the list each one switches to now, so the number and the table can never disagree. They are also translated, which they were not.

= 0.2.19 =
* **The link graph finishes. This is the fault underneath every other symptom.** A link that resolves but is not a post is recorded as `-1`, and a broken one as `-2`. The column those go into was unsigned, so the database turned both into `0`, which is the value that means "not resolved yet". The resolving phase therefore read the same 300 links on every pass, wrote nothing that stuck, and never emptied. Any site with a single unresolvable internal link, which is every real site, could not build a graph: twenty seconds of work per slice, forever, with a progress bar that never moved. The column is signed now. Every query that reads it tests for a positive id, so nothing else changes.
* **A phase that resolves nothing now fails and says so** instead of looping in silence. That is the part that made this take so long to find.

= 0.2.18 =
* **The link graph's progress bar covers the whole run.** It counted pages, which only the first of five phases moves, so it reached 100% the moment the crawl finished reading pages and then sat full through resolving, aggregating, scoring and publishing. A full bar on a run with four phases to go looks exactly like a run that has hung, which is what it was reported as.
* **It also says which phase it is in** — reading pages, resolving links, counting what links to what, working out where authority ends up, publishing — instead of claiming to be reading pages through all five.

= 0.2.17 =
* **The link graph finishes now.** Building it posted one request, which crawls for twenty seconds and hands the rest to WP-Cron. WordPress runs cron on page views rather than by the clock, so on a quiet site the rest arrived hours later or never, while the screen sat on a progress bar frozen where that one request stopped. The screen drives the run itself now, one slice at a time, the same way the site audit has for months.
* **A link scan no longer stops when you close the tab.** The site-wide scan was driven entirely from the browser, by design and with no fallback, so a run abandoned at 84 of 215 destinations stayed at 84 and nothing on the server intended to finish the rest. It carries on in the background now. Keeping the tab open is still the quickest way, not the only way.
* **Pressing "Build the graph" while a crawl is running no longer costs a run.** The action is capped at six an hour. Resuming an existing crawl starts nothing, but it was charged the same as a fresh one, so a customer whose run had stalled could press the button six times, start nothing at all, and be locked out of their own link graph for the next hour. Only a genuinely new crawl is charged now.
* **One progress bar, everywhere.** There were two designs: a striped bar that moves while the run does, on the site audit and the link structure, and a thin flat one with hand-written labels on the link checker, the bulk screen and the site analyzer. Same product, same kind of wait, different answers to "is this still working?". They are all the striped one now, and the start screen draws it too instead of only changing the button to "Working...".
* **Reloading no longer loses a run in flight.** Reading the graph answered "not built yet" for a crawl that was halfway through, which is the same answer as for a site that never started one. It carries the progress now, so opening the screen picks up an abandoned run and carries it to the end.

= 0.2.16 =
* **Fixes a crash on the six start screens.** Pressing the button that starts the first run threw `ReferenceError: Can't find variable: __` on Links, Broken links, Link structure, URLs, AI visibility and Site tests. The shared start screen used the translation function without importing it, so the name did not exist once the button switched to "Working...". Present since the screen was written, in every build.
* **The release now runs a JavaScript linter, and a failure stops it.** Two crashes in two versions had the same root: a name that does not exist. The bundler accepts one without complaint and the PHP test suite never executes JavaScript, so nothing between the editor and the customer ever resolved a name. Something does now.
* The preview harness carried two copies of the same mock route; the first was dead and edits to it did nothing. Removed.

= 0.2.15 =
* **Fixes a crash on the Links and Bulk screens introduced in 0.2.14.** Giving the progress bar a "running" state was wired into three screens with a search-and-replace, and the check that the variable existed matched `job.busy` and `status.busy`: properties on an object, not variables in scope. Two of the three screens then threw `ReferenceError: Can't find variable: busy` on load. They read the run's own flag now.

= 0.2.14 =
* **Disconnecting Search Console no longer depends on the report loading.** The connection panel — and the only button that disconnects a property — sat inside the performance report. A report that fails or never finishes takes the whole component with it, so the one screen you would open *because* the connection is misbehaving was the one screen that hid the fix. It renders from the connection status alone now.
* **The answer simulation shows a bar.** It is the slowest thing in the plugin — every prompt is a round trip to a model — and the only sign it was moving was a number inside the button label.
* **The link scan says it needs this tab.** It is driven from the browser, so closing the page stops it where it stood, which reads as a crash. The site audit says this; the link checker did not. Pressing Rescan picks it up where it stopped.

= 0.2.13 =
* **A running job now looks like one, on every screen that reports one.** The link checker, the bulk runs and the analyzer already drew a bar with a count; what they lacked were the moving stripes the site audit has. A bar that only advances every twenty seconds looks stopped, and to somebody watching it that is the difference between "working" and "crashed". The stripes moved into the shared bar rather than into each screen, so anything reporting a job gets them.

= 0.2.12 =
* **The graph scan shows its progress instead of "Working…".** It uses the same block the site audit does: "Checking pages — 24 of 158", a moving bar, a percentage, and a line saying the run carries on in slices. A crawl that takes several requests behind a button that says nothing is indistinguishable from one that has hung — which is what it looked like.
* **"Working…" was hardcoded English.** It sits in the shared first-run panel, so every first-run screen in the plugin said it in English to a German customer. The translation was already in the catalogue.

= 0.2.11 =
* **The link graph was building all along — it just never said so.** "Build the graph" answered in two seconds and reported "not built", which is what a dead button looks like. Two things were behind it. A run holds a lock for fifteen minutes, so a request that died holding it — a timeout, a closed tab — left every press for the next quarter of an hour exiting immediately. The lock only has to outlive one twenty-second slice, so it is two minutes now. And a crawl of a few hundred pages never finishes inside one request: it works for twenty seconds and continues in the background, but the answer described the last *published* graph, so a build in flight and a build that never started said exactly the same thing. The screen now says "Building — 24 of 161 pages".

= 0.2.10 =
* **The editor panel's right edge was off the screen.** It sat 24px past the right of the window, so the live preview and the "fix next" list were simply not there. The panel is anchored to the window and had no business moving — its entrance animation, a slide in from the right, was stranded on its first frame long after it should have finished. The slide is gone: it fades and lifts instead, which reads the same and cannot push a right-anchored panel sideways.
* **"Too many requests, please wait a moment" — when the wait is an hour.** So you try again, and again, and meet the same encouraging sentence. It now says how many runs the action allows and how many minutes are left.
* **Building the link graph allowed three runs an hour**, the strictest limit in the plugin by a factor of three: one failed attempt for any reason and you were locked out until the next hour. Six now.
* **The answer-score chart keeps the whole row** when there is nothing beside it. It split into two columns whenever there was a trend to plot, but the card on the right only appears when the last run named a competitor — so a site nobody is compared against got a half-width chart and an empty half.

= 0.2.9 =
* **The site tests add up.** The card breaks a run into failing, worth fixing and passing, with the number of tests beside it — and those three never summed to it. A fourth status was counted nowhere: response time, pages set to noindex and unpublished drafts are readings rather than verdicts, so they were dropped from the breakdown silently. Anyone adding the numbers got a different total than the one printed next to them. They are counted now, shown as "Noted", and still excluded from the score, because whether a result scores is a different question from whether it is reported.
* **Charts run to the edge of their card.** The gutter that is right for text is a second margin around a drawing that already carries its own axis inset; on a wide card it cost 40px of the one element that could use them. And the width fix in 0.2.8 leaned on a browser callback that does not always arrive — where it did not, the chart quietly went back to being letterboxed. It is measured on layout now, which cannot be skipped.
* **The sitemap screen drops a tile that was always zero.** "Excluded by ID" spent a quarter of the top row on a number that is zero on almost every site, because nobody excludes pages one at a time. It appears when there is something in it. Row widths follow the number of tiles everywhere, so a screen can no longer end a row in an empty cell.

= 0.2.8 =
* **Charts fill the card they sit in.** Every line chart declared a fixed 640-wide drawing on a box as wide as the card, so the browser scaled it down to the height and centred it: the line sat in a narrow column with dead space either side. Visible on Search Console, on the site audit, and on every other screen that carries one. The chart now measures its own box and draws at that width.
* **The score is the size it was designed at.** Two unrelated components answered to the same class name, and the later one is full width by necessity. The arc gauge inherited it and filled whatever card it was in — on Site tests, a "96" roughly a foot tall.
* **The check groups look like they open.** In the meta box and the editor sidebar they have always been foldable, but the marker that says so was suppressed and nothing replaced it, so a group that starts closed read as a bug. There is a caret now, it turns, and the meta box gained one control that opens or closes all of them at once.
* **The duplicate-tag firewall now runs on the home page and on archives.** It finds our tags by two HTML markers, and the half of the code that writes the head for everything that is not a single post never wrote them. On those pages the firewall did nothing at all: two canonicals shipped, and the live conflict scan — which checks the home page — counted our own tags as the other plugin's and reported the site clean.
* **Taking over works against a plugin we cannot name.** "Take over" only started the firewall when one of ten known plugins was detected. A white-labelled build, a theme with its own SEO fields or twenty lines in functions.php writes a canonical just as happily and defines no constant we recognise. A scan that finds foreign tags now counts, and an explicit choice of "take over" or "per tag" starts the firewall on its own.
* Owning a surface is not the same as having written it: a site with no tagline gave us no home-page description, and the other plugin's copy was stripped anyway. One description became none. Theirs is now left alone wherever ours does not exist.
* The dashboard offered Pro at $99 a year. It is $49, and has been since the price changed.

= 0.2.7 =
* **"Check for a new version" on the licence screen.** The button beside it, "Re-check now", only re-reads this plugin's own answer, so it reports all is well while a release sits invisible. The new one re-asks Freemius. It is not instant — Freemius takes its own time between publishing a release and serving it, and nothing in WordPress shortens that — but it is the only control that asks at all, and it is what realigns things after a manual ZIP install.
* **The Indexing API key can be pasted.** It could only be uploaded as a file, but by the time that key reaches whoever is setting the site up it is usually a block of text in a message — and a screen that insists on a file makes them save the clipboard to disk for nothing. The route always took it as a string; it just had no field.
* **Post types are listed by name.** The Indexing screen offered `unterkunftsarten` and `sso_places` to tick. WordPress knows what those are called.

= 0.2.6 =
* The discount code is GETNOW30. The old LAUNCH30 stays valid, so an upgrade link inside a copy of the plugin you already installed keeps working — a code that has quietly stopped existing is a sale lost without anybody finding out.

= 0.2.5 =
* **The dashboard says whether the AI is actually doing anything.** "The AI at work" opens the screen everybody uses with the one thing the autopilot screen kept to itself: what the agent is on right now, how far through it is, how many calls it made today and what they cost — counted by the router in micro-dollars, not estimated. A run that has stalled says so, explains why (WordPress runs scheduled work on page views, so a quiet site runs it late) and offers to pick it up. It inherits the operations floor's rule: it moves only while something is genuinely running, because a card that animates when nothing is happening teaches you to stop looking at it.
* The operations floor now reads every list defensively. It is fed straight from the agent's status and is mounted on two screens; one missing key would have taken both down.

= 0.2.4 =
* **The Measure screen could not be drawn.** A shared chart component asked for a list of score bands that two of its three callers never passed, and it read that list without checking. The moment a chart had nothing to plot — a fresh install, the first day, before any history exists — the screen threw and went to the error panel. Both ends are guarded now: the component refuses a missing list instead of crashing, and the two cards that have no bands to give it show their note alone rather than a scale that would invent a ceiling out of 100.
* **One broken screen no longer takes the whole plugin with it.** The error panel wraps the entire admin app, and it never cleared itself — so until you pressed one of its two buttons, every other screen was behind it, including the sidebar you would use to leave. Moving to another screen clears it now, and the browser's back button works again.

= 0.2.3 =
* **Every link out of a setup wizard did nothing.** The wizards are overlays drawn in front of the whole application, and the code that watches the address bar could only ever switch one on — never off. So "Open AI Engine" did change the page underneath, and the overlay stayed on top of it: nothing appeared to happen because nothing appeared. The same break hit "Open Autopilot", the Search Console fallback, and the hand-off from the setup wizard into the AI one.
* **"Add a key, then come back here" now has a way back.** The AI Engine screen offers the return, and says whether a key actually arrived.
* **"Open developer tools" was dead.** The button offered when a screen fails to load pointed at an address that does not exist — in the one place where you are already stuck. Two new checks in the test suite hold both of these: a wizard has to follow the address bar in both directions, and every in-app jump has to name a screen that exists.

= 0.2.2 =
* **"160 pages have no description" was wrong, and this is the rest of the fix.** Counting the meta keys of other SEO plugins caught the sites that store a description per page. It did not catch the commonest case of all: a description that comes from a *template*, which has no meta anywhere and is still what the page publishes. The analyser has always resolved the description the way the page actually emits it — from our field, from the plugin you migrated from, or from a template — and now it writes that verdict down as one row, so the counters read the truth instead of guessing at it. Every page is re-analysed once on update to fill it in.

= 0.2.1 =
* The score rings in the editor sidebar and the post metabox sweep in instead of appearing. A CSS transition only fires on a change, so the one number those panels exist for was the only thing on them that did not move. Reduced-motion gets the number without the sweep.
* "1 % have a meta description, 160 do not" is two counts now. A share beside an absolute asks the reader to work out the total themselves, and on a small post type it rounded to a number that looked wrong.

= 0.2.0 =
* The colours harmonise. A cool, vivid violet beside an earthy sage green and a muddy ochre is what "the colours do not go together" means: the primary was updated and the statuses were left in the register they had when the primary was WordPress blue. Green, amber and red now come from the same set the website uses. Only the graphic values moved — the inks stay dark, because they sit on tonal backgrounds and have to clear 4.5:1, and the suite checks both.
* No bare brown on a white card. Status words in a stat tile are tonal chips now, so the ink sits on the background it was measured against instead of appearing as a discoloured word.
* Fixed, and it was a real one: the memory baseline a job records when its slice starts was never cleared, so the *second* job in the same request measured its growth against a number taken before the first job allocated anything — and yielded after a single record. A migration moved 1 row of 60 per slice instead of 25.
* "Recalculate" shows a bar while it works. It carries no percentage, because the request answers when it is done and there is no progress to report — an invented percentage is a lie told with a straight face.

= 0.1.9 =
* **A description written by the SEO plugin you are migrating from now counts as a description.** The library counted its own meta field alone, so a site whose 160 pages all carry a Rank Math description was told 160 pages had none — and every headline, tile and recommendation on the dashboard was built on that number. It reads Rank Math, Yoast, SEOPress, AIOSEO and four more, for descriptions and focus keywords alike, from the same key list the importer uses. Three checks hold it.
* The mud is gone. The score rings in the editor sidebar and the metabox were drawn in #9a5a0a — the right ink for a word on a tonal pill, and the wrong colour for a 10px arc. The arcs take the product's amber; the verdict under them is ink, because the ring already says it in colour.
* Four untokenised greys in the editor and metabox were replaced with the palette they were drifting from.

= 0.1.8 =
* The score spread is big enough to read. 92px of box minus a number and a label left about 50px of bar, so the difference between 125 pages and 32 pages was a centimetre. It is taller, each range shows its share, and an empty range is a neutral hairline instead of a red stub that reads as "a few bad pages" — the opposite of the truth. And a caption says what the chart is, which nothing on the screen did.

= 0.1.7 =
* The Site Audit screen had its own copy of the findings chart, so the fix that stopped a flat line pretending to be a measurement never reached it. Both screens use the same two components now: a line when it has a shape worth drawing, the composition of the total when it does not.
* Three more English phrases on a German screen: the run comparison chips read "−0 fixed" and "342 recurring", and the severity tiles were headed Critical, Warning, Notice.

= 0.1.6 =
* Fixed: the score scale was 200px wide in a 1900px card, so the card read as mostly empty. A flex container inside a grid cell shrinks to its content, and the band widths are percentages — they resolved against almost nothing.

= 0.1.5 =
* The trend cards show something real from day one instead of a flat line or an empty box. Where there is no history yet, the score card draws where it stands against the range it lives in, and the findings card draws what the total is made of — both from data that already exists. The line takes over the moment it has a shape worth drawing.
* The weakness bars have a legend. Seven rows of red, amber and green, and the only explanation was in a tooltip a phone cannot show.
* The per-page checks: the weight dots get a key, and the one action per row sits on the row's right edge instead of wherever the text above it ended.
* The score pills read "60 / 100" — the space before the slash was being collapsed by the browser, so they read "60/ 100".
* The Links column is gone where the internal-link graph is not installed. A column that has never once held a value reads as broken rather than absent.

= 0.1.4 =
* Fixed: the whole app was welded to the admin menu. When the plugin stopped painting its own window it handed the page back to WordPress — and handed back the gutter with it, so every card sat 2px from the menu and every row inside inherited the squeeze. The page has a margin again, and card headings, list rows and table cells share one left edge.
* The trend charts stop pretending. Two readings of the same number drew a perfectly flat bar across the card, which looks like a measurement and is the absence of one. A line now needs three readings and at least two different values; until then the card states the number and says when the line will appear.

= 0.1.3 =
* Fixed: the content list showed the same eight posts to anybody who did not scroll past every type group to find the pager. Each group now says "8 of 135" and offers "Show all 135", which opens that type as a paged list. The pager itself was written in English inside a translated screen.
* PageSpeed measures something. The screen named after a measurement could only store an API key — everything it configured appeared inside a single page's analysis panel, so you set it up, pressed Save, and it never showed a speed. It measures your front page on demand now and keeps the last reading: score, LCP, CLS, TBT, FCP.
* Fixed: the Pro strip on the dashboard printed near-black text on its violet ground. The headline is a div and the price a span, and the first pass had only coloured h3, strong and p.

= 0.1.2 =
* The GEO and SEO columns in the posts list write "73 / 100" instead of "73". The heading says what the column is, and a heading is at the top of a list you scroll — fifteen rows down the pill was two digits with no scale attached.

= 0.1.1 =
* Phase two of the visual upgrade — the controls. Fields have the same corner as everything around them instead of WordPress' 3px, a focus ring you can actually see, and one height across the product. Buttons have one height, one radius and a shadow in their own colour, so a filled button looks placed rather than pasted. Table rows lost the hard-coded blue-grey separator and gained a calm hover. The selected screen in the sidebar is unmistakable at a glance.
* Phase three — the screens people photograph. The score card carries a soft diagonal of the brand behind the ring. The Pro strip is the product's own dark register instead of the white-card-with-a-coloured-edge every plugin advertises with. Empty states are a surface with room to breathe rather than a dashed grey box.
* Still no behaviour change: 474 checks green, 114 routes live.

= 0.1.0 =
* The plugin has a face. Its typeface is DM Sans — the one the website uses, bundled with the plugin rather than fetched from a font host — its colour is one violet instead of the WordPress blue that every SEO plugin shares, and its surfaces are rounder with a canvas that carries a trace of that violet. Open it beside any other SEO plugin now and you can tell which is which.
* The editor sidebar, the post metabox and the block styles moved with it, so the product is one palette rather than four.
* This is phase one of a visual upgrade: identity first, components and signature screens next. No behaviour changed.

= 0.0.10 =
* Fixed: "Sign in with Google" in the Analytics panel asked for an OAuth URL built from your own Google Cloud credentials, so on any site that has not set those up — nearly all of them — it answered "no credentials" instead of signing you in. It takes the one-click path first now, the same one Search Console has always offered.
* Every GET route in the plugin is now called on a real install before a release, and anything that is not a 2xx has to name a reason we accept. 114 routes, 0 unexplained failures.

= 0.0.9 =
* Fixed: the site audit stopped where it stood the moment you closed the tab, and never continued. Both the audit and the broken-link scan registered themselves with the background resumer inside a REST hook — and a WP-Cron request never fires that hook, so the resumer had nothing to resume. Observed on a live site: 66 of 156 pages, frozen for three hours; one tick took it to 88.
* And the screen now continues an abandoned run as soon as you open it, instead of drawing a progress bar that never moves.
* The progress bar is taller and its stripes move while the run does — a bar that only grows every twenty seconds looks stopped, and a stopped bar looks like a crash.

= 0.0.8 =
* Fixed: "Allow & Continue" on the opt-in screen did nothing at all, on every install. Freemius returns from its sign-up to one page and only completes the sign-up on another, and our menu config told it to insist on the second — so the activation was dropped in silence every time. The config is corrected, and a second path completes a pending opt-in wherever the browser lands.
* Analytics connects the way Search Console does. The panel opened with two empty ID fields and a "Find mine" button beside them, so the automatic path looked like the fallback and the form looked like the job. It now states what it is connected to, or offers the one button that connects it — and finding the property saves it, because the server was already saving it and the screen was still asking for a second click.
* Not signed in to Google? The Analytics panel signs you in, instead of telling you to find another screen.

= 0.0.7 =
* Fixed: the Measure screen crashed the moment it loaded. A state hook sat below an early return, so the first render ran six hooks and the second ran seven — React refuses that and sent the whole screen to the error boundary. Analytics was unreachable on every install.
* Second-level tabs are a segmented control instead of WordPress' old pipe-separated link row. They were the one element on a screen full of cards that looked unfinished.
* The setup checklist sits on the card's own margin instead of flush against its edge, its steps read as titles rather than blue links, and each row is a hover target.
* "Upgrade to Pro" is a designed screen: what the product is for, the five-assistant picture that explains it, then the three plans.
* The asset cache buster is the file's own timestamp, so a rebuilt bundle is always a new URL and a browser cannot keep serving the previous build under the same version number.

= 0.0.6 =
* Fixed: every way of buying Pro ended on the WordPress dashboard. "Upgrade to Pro" in the menu, "Upgrade to Pro" on the dashboard, "See Pro" on every locked card and "See plans" on the licence screen all resolved to a Freemius page — Pricing or Account — that the SDK does not register until a customer has opted in and connected. WordPress answers an unregistered admin page by sending the visitor to the dashboard, so pressing buy appeared to do nothing at all.
* Fixed: the 0.0.5 fix for that was wrong in a second way. It redirected to the pricing page on the product's own site through `wp_safe_redirect()`, which refuses any host that is not this site and substitutes the dashboard.
* "Upgrade to Pro" is now a screen inside the plugin: the three plans, what each one includes, the price, and one button that goes to the checkout. Nothing about it can redirect, so nothing about it can dead-end.
* The buy button prefers Freemius' checkout inside WP-Admin when that page exists, because a purchase made there activates the licence on this site by itself; otherwise it opens the hosted checkout. Which one is true is checked while the page renders, not assumed.
* Fixed: the "Support" link handed to the admin screens pointed at Freemius' contact form, which this plugin also does not register.
* The licence screen no longer offers a "Licence account" button before there is an account to open.

= 0.0.5 =
* Fixed: seven tabs in the free edition led to a routing error instead of a Pro notice — URLs, Bulk, Checker, Internal, Structure, Keywords and Reports. They are hidden where the module is absent.
* Fixed: "Upgrade to Pro" led to "you are not allowed to access this page" — the menu pointed at a Freemius screen the plugin does not register.
* Fixed: after an import the dashboard kept reporting the old counts, so an import that brought 151 keywords across looked like it had done nothing.
* Fixed: the licence screen showed a raw PLAN_TITLE placeholder, an empty product row, and English status words in a translated admin.
* The three letters before each page title are now explained above the table instead of only in a tooltip.
* Fixed: the published free build was packaged with placeholder store credentials, so it treated itself as a development copy and "See Pro" led to a developer notice instead of the upgrade.
* The sitemap screen now shows how many URLs each content type actually contributes, instead of a published count that did not match the total.
* The AI search menu group is labelled "AI search (GEO)", so the menu and the GEO score column use the same word.
* Fixed: "Connected services" on the settings screen showed Search Console as not connected even while the Search Console screen was displaying live Google data — it was reading option names nothing writes.
* Fixed: the free edition showed four tabs whose modules it does not ship, and clicking them produced a routing error instead of a Pro notice.
* Fixed: finding your Analytics property gave up after twenty properties, which missed accounts with more. Candidates are now ranked by name against your domain first.
* Fixed: `o4-mini` was offered in the OpenAI model list but could never work — reasoning models reject the `max_tokens` parameter. The client now sends what they expect.
* Fixed: a bulk run's cost ceiling counted every AI call on the site, so an editor working in another tab could stop the run early. It counts the run's own spend now.
* Fixed: undoing an autopilot change restored at most 500 characters of the original value. The full value is kept, and an entry too large to keep is marked as not undoable rather than restoring a clipped copy.
* Fixed: a file import posted straight to the API bypassed the 8 MB ceiling and could silently import nothing.

= 0.0.4 =
* Fixed: the analyzer treated a meta description as missing when it came from a template or the excerpt — although that is exactly the description the page publishes. On template-driven sites this reported nearly every page as needing work. It now judges what the page actually publishes.
* Fixed: pages were not re-scored after an edit, so the dashboard kept listing the same pages however often you fixed them. Every published page is re-analyzed on save.
* Fixed: the AI autopilot's "at most X changes per run" ceiling did nothing — a run capped at one change made four and kept going.
* Fixed: cancelling a bulk or autopilot run mid-slice could be undone, and the resumer picked the job up again ten minutes later.
* Fixed: a bulk "fill in what is missing" run could loop forever when the model returned an empty answer.
* The setup wizard now detects all 14 import sources; it used to ask an older importer that knew nine.
* New: "Sign in with Google" connects Search Console and Analytics in one click, with no Google Cloud project on your side, and finds the matching property and GA4 account for your domain.
* New: agentic browsing as a single switch — allow or block the on-demand fetchers from ChatGPT, Claude and Perplexity together.
* Sitemap: exclude term IDs, and include the featured image independently of content images.
* The weak-area names on the dashboard are written for people who do not do SEO for a living.

= 0.0.2 =
* An installed but not yet activated Pro copy is now asked to connect its license — with a direct path to activation — instead of being shown the sales card again.
* The "See Pro" links say "Activate license" on a Pro install, and lead straight to Freemius activation.

= 0.0.1 =
* First public release on WordPress.org.
* Design: the whole admin now uses WordPress's own design language — native menu, nav-tabs, core buttons, notices and list tables — in light and dark, English and German.
* Licensing runs entirely through Freemius; the plugin never stores a key of its own.
* llms.txt and llms-full.txt pass Google's PageSpeed "Agentic Browsing" audit, and the file panel shows the three rules and the verdict before you run the report.
* The complete development history ships with the source repository.

== Upgrade Notice ==

= 0.2.43 =
Elementor saves no longer overwrite a title or description applied from the AI review, the sidebar or the content list.

= 0.2.42 =
The SEO score is a four-pillar formula that counts the keyword; llms.txt in one click; seven more audit rules and CSV export; a Measure overview; media library audit.

= 0.2.41 =
Restores the German translations missing from the 0.2.40 Pro package.

= 0.2.40 =
Scores in the editor toolbar, bulk keywords, WordPress styling for the meta box and sidebar, schema and link counts in the post list.

= 0.2.39 =
The AI search score is read once a day, and both dashboard trend cards state their last and next reading.

= 0.2.38 =
Fixes a translation notice on sites with WooCommerce and WP_DEBUG, and requires an administrator for running a plugin import.

= 0.2.37 =
The role editor, .htaccess, settings import and restore, maintenance tasks and webhooks now also require a WordPress administrator. Nothing changes for administrators.

= 0.2.36 =
Stored data is renamed from the `airs` prefix to `airanksuite` automatically on the first request; settings, scores and redirects are kept. Custom code using the old hooks, constants or REST paths must switch to the new prefix.

= 0.2.32 =
A plugin role could edit and read posts WordPress would not open for it; the dashboard kept a fixed audit's findings on screen; a large redirect import could stop half-applied.

= 0.2.31 =
The page drawer stayed inside the window again, and titles show their dash instead of an HTML entity.

= 0.2.30 =
The admin looks like WordPress again: system typeface, core blue, core radii. The dashboard verdict now considers audit findings.

= 0.2.29 =
Build housekeeping only. Identical to 0.2.28 for anyone using the plugin.

= 0.2.28 =
The dashboard reported "0 open findings" while the audit had hundreds. Recommended for everyone.

= 0.2.27 =
The AI can no longer write a meta description or title longer than the limit.

= 0.2.22 =
The AI can no longer write a meta description or title longer than the limit.

= 0.2.21 =
Translation fixes on the link structure and Local SEO screens.

= 0.2.20 =
Fixes "(undefined)" on the link structure filters.

= 0.2.19 =
The link graph can now finish. Required if you have ever seen it stall.

= 0.2.18 =
The link graph's progress bar now covers the whole run instead of filling up after the first phase.

= 0.2.17 =
The link graph now finishes instead of stalling after the first twenty seconds.

= 0.2.16 =
Fixes a crash when starting the first run on six screens. Recommended for everyone.

= 0.2.15 =
Fixes a crash on the Links and Bulk screens introduced in 0.2.14. Update straight away if you are on 0.2.14.

= 0.2.14 =
Disconnecting Search Console no longer depends on its report loading, the answer simulation shows a progress bar, and the link scan says it needs the tab open. No database changes.

= 0.2.13 =
A job that is running now looks like it, on every screen that reports one. No database changes.

= 0.2.12 =
The graph scan now shows how far it has got instead of a bare "Working…". No database changes.

= 0.2.11 =
Fixes "Build the graph" appearing to do nothing: it was building, silently, and a stale lock could block it for fifteen minutes. No database changes.

= 0.2.10 =
Fixes the editor panel sitting 24px off the right of the screen, and a rate-limit message that said "wait a moment" when the wait was an hour. No database changes.

= 0.2.7 =
Adds "Check for a new version" to the licence screen, lets the Indexing API key be pasted, and lists post types by name. No database changes.

= 0.2.6 =
The discount code is now GETNOW30; the old code stays valid. No database changes.

= 0.2.5 =
Adds "The AI at work" to the dashboard: what the agent is doing right now, how far along, and what it has cost today. No database changes.

= 0.2.4 =
Fixes the Measure screen, which could not be drawn on a site with no chart history yet, and stops one broken screen from hiding the rest of the plugin. No database changes.

= 0.2.3 =
Fixes the buttons that led out of the setup wizards and did nothing — "Open AI Engine", "Open Autopilot", the Search Console fallback — and the dead "Open developer tools" link. No database changes.

= 0.0.2 =
Pro installs without a connected license now see an activation card instead of a sales pitch. No database changes.

= 0.0.1 =
First public release. If you are upgrading from a pre-release build, your settings, scores and tables are kept; the database migrates itself on the first page load.
