TWT AEO Ultimate — Full Changelog
=================================

= 2.15.0 =

* **Fixed: every scanning screen silently stopped at the 200 most recent items.** The detectors cap each scan at 200 posts because scanning a whole large catalogue in one page load times out — but the screens presented that first block as the whole story, so on a 1,000-product store 800 products were simply invisible. Every affected screen now pages through the entire catalogue:
  * **WooCommerce Products tab** — pages 200 products at a time (`scan_all()` takes a page number), with a windowed pager, a "Products X–Y of Z (newest first)" header, and a Scan Again button with a rescan-complete notice.
  * **Social Graph** — the existing 25-row pager now spans the whole catalogue; deeper pages load deeper 200-item scan blocks (`TWTAEO_OG_Detector::SCAN_BLOCK`) instead of re-slicing the first one.
  * **Schema Detector FAQ / Service / Contact tabs** — these detectors *filter* what they scan (a block of 200 posts may yield three FAQ pages), so a new shared `TWTAEO_Scan_Pager` pages over scan blocks and labels each page with "Scanned items X–Y of Z", never presenting a filtered block as the site total.
  * All paged queries order by date then ID, both descending, so pagination is deterministic.
* **New: the commerce tab bar never hides a feature.** All five tabs (Products, Google Merchant Center, Bing Merchant Center, Smart Collections, Promotions) always render on the WooCommerce screen; a tab whose module is off shows dimmed with an "Off" chip and routes to the Modules page. Previously an off module's tab did not exist at all — and a merchant who never learns Promotions exists never publishes the data. Less data = fewer citations.
* **Fixed: the AEO header banner disappeared on the Smart Collections and Promotions tabs.** Both are separate admin pages that never rendered the shared header; they now carry the same "AEO WooCommerce" banner and page style as the rest of the tabbed screen.
* **New: Image SEO's "Add an image" opens the native WordPress media modal.** For a page with no images, the button now opens the media library in place and sets the chosen image as the featured image (the image og:image and schema fall back to), then swaps the row to the normal Generate/Edit actions. New `twtaeo_img_set_featured` AJAX endpoint; the button previously just linked to the editor.
* **Improved: bolder pill tabs and livelier cards.** The active tab is a filled blue gradient pill with white text (the Conflicts tab gets the matching red), hover states lift, icons are larger, and summary cards elevate on hover.
* **Changed: suppressing WooCommerce core's Product schema now requires the merchant's consent.** `dedupe_wc_product_schema()` used to blank WooCommerce's built-in Product block unconditionally whenever the detector module was on. Both dedupe filters are now gated on a new `twtaeo_wc_schema_takeover` option (`'aeo'` suppresses, anything else passes through untouched), and the Products tab carries a card that explains the conflict — classic themes publish WooCommerce's block on every product page, some block themes don't — with one-click "Make AEO the only Product schema (recommended)" and a "Restore WooCommerce's built-in block" path back. Existing installs updating to 2.15.0 revert to publish-both until the merchant chooses.
* **New: Social Graph summary row reworked.** A fifth card counts pages missing an og:title and/or og:description (the text half of the gap the image card never covered), the grid gains a compact variant so five cards fit one row, and Rescan All moved above the cards where the stale-number question actually arises. The Twitter Cards card stopped under-counting: the writer emits a card on every singular page once the site handle path is active, so the card now shows live coverage (with the customised count alongside) and, when Rank Math or Yoast manages Twitter Cards, says so instead of showing a dishonest zero.
* **New: catalogue-wide search on the big tables.** The WooCommerce Products tab and the Social Graph coverage table each carry a search box that queries the WHOLE catalogue server-side — title, content, and SKU (the `posts_search` SKU filter is ported from AEO Ultimate for WooCommerce, along with its reasoning: filtering the rows already on screen would look instant and find nothing, because the product being hunted is almost never on the page you are looking at). `scan_all()` on both detectors takes an optional search term; matches paginate through the same block machinery, `last_found()` supplies the pager total, and a page number that falls off the end of a narrowed result set lands on the last real page instead of an empty one.
* **Improved: pagers render above AND below every paginated table** (Products tab, Social Graph, and the Schema Detector's FAQ/Service/Contact tabs), so changing pages never requires scrolling past 200 rows.
* **New: the Social Graph cards read from a site-wide census.** A single 200-item block's counts on a 955-page site produced "200 scanned, 200 missing image, 200 Twitter cards" — the block size three times, read (correctly) as nonsense. Rescan All now drives a batched walk over every scan block (`twtaeo_og_census_block` AJAX, one block per request so no store size can time it out), counts accumulate server-side into the `twtaeo_og_census` option, and the cards render those site-wide totals with a caption naming when the census ran. Until the first census, the cards carry an explicit "newest block only — run Rescan All" caption instead of passing a block off as the site.
* **Improved: one image-fallback chain everywhere.** New `TWTAEO_OG_Writer::effective_image()` (saved og:image → featured image → first WooCommerce gallery image) now backs the OG output, the Twitter Card summary/large-image downgrade decision, and the Social Graph table — three surfaces that could previously disagree about whether a page "has an image". The Twitter Card column shows the effective default ("large image · default") rather than "auto", since the writer applies a card to every page once active; per-page overrides still win, and when Rank Math or Yoast manages cards the cell says so.
* **Fixed: products with only gallery images read as "missing og:image".** Both `TWTAEO_OG_Writer::output_meta()` and `TWTAEO_OG_Detector::scan()` fell back to the featured image alone; a product whose images all live in the WooCommerce gallery published no og:image and was counted missing. Both now fall back to the first gallery image, in the same order, so the scan keeps reporting exactly what the writer publishes. The duplicate Rescan All button above the coverage table was removed (the one above the summary cards stays). Note the scan runs live on every page load — Rescan is a labelled refresh, and unchanged numbers mean unchanged content, scoped to the 200-item block on screen.
* **New: the FAQ detector scans products.** `TWTAEO_FAQ_Detector::post_types()` includes `product` when WooCommerce is active, so the Schema Detector's FAQ tab covers product pages — sizing questions and care instructions are FAQ content an answer engine can quote. A product whose pairs are already published through `TWTAEO_Product_FAQ` (our assembler at `#product-faq`, or AEO Ultimate for WooCommerce when it claims the surface) reports schema present; pairs that exist while no commerce writer is active do NOT count as present, because nothing reaches the page.
* **Fixed: the Social Graph bulk description tools never touched products.** `get_posts_missing_og()` / `get_posts_missing_description()` queried only `post` and `page`, while the Social Graph screen scans products too — so on a store the AI bulk run queued a handful of pages, skipped them as thin, and reported "0 created. Reload to see them." to a merchant looking at hundreds of product rows missing og:description. Both queries now include `product` when WooCommerce is active (matching `TWTAEO_OG_Detector::post_types()`), the generator folds `post_excerpt` into the content it summarises (for products that is the short description — often the entire pitch), and every finish surface — the in-page status line, the admin notice — reports skipped counts by name instead of hiding them inside a bare created figure. Starting a run with nothing to do now says so instead of dead-ending at "Finding posts…".
* **Improved: page-scoped numbers say so.** On a paginated catalogue the Products tab summary cards are labelled "(this page)" with a caption naming exactly what was scanned — "0 Missing Schema" beside "949 Total Products" must never read as a site-wide verdict the scan never made. "Generate Schema for All" also stays visible whenever the catalogue is only partially scanned, since it walks the whole catalogue regardless of the page shown.

= 2.14.0 =

* **Compatibility: verified against WordPress 7.1 (RC2) and WooCommerce 11.0.0.** Every admin screen (Dashboard, Modules, Settings, Command Center, Social Graph, Schema Detector, Image SEO, E-E-A-T, WooCommerce + its Smart Collections and Promotions tabs, Knowledge Graph, Not Indexed, AI Ready) and the front-end surfaces (product pages with the full connected @graph, shop, FAQ pages, llms.txt) render with zero deprecations, warnings or fatals. WordPress 7.1's AI Client SDK still exposes usingModelPreference / usingMaxTokens / generateText, so the WordPress 7 AI generation path keeps working unchanged. Tested up to bumped to 7.1.

* **Feature: commerce schema suite ported back from AEO Ultimate for WooCommerce.** That plugin was forked from this one and its `@graph` and product schema kept evolving; this release brings the evolved versions home:
  * **Advanced Knowledge Graph (all pages, not just products):** the Organization is typed `OnlineStore` when WooCommerce is active; placeholder emails (dev-email@wpengine.local and friends) are never published as a contact point; WebPage titles are entity-decoded; `mainEntity` resolution understands `#product-group` and `#collection` (collection deliberately last so a shortcode collection never outranks the product on its own page); `isPartOf` back-references are restricted to CreativeWork types as the vocabulary requires; mapped product-category archives are typed `CollectionPage`; and the emitted JSON is compact rather than pretty-printed (~half the bytes on a large product graph).
  * **Product schema engine:** the WooCommerce detector is replaced with the evolved version — connected Product/ProductGroup `@graph` with per-variant offers, image nodes with captions, strikethrough (sale) price specifications, dimensions, GTIN/MPN identifiers, product relationships, review detection, shipping details derived from live shipping zones, return policy detection, speakable specs, breadcrumbs folded into the graph, and suppression of WooCommerce core's duplicate Product block. New `twtaeo_product_schema` filter for extensions.
  * **Extension-aware pricing (`TWTAEO_WC_Extensions`):** Bookings, Subscriptions, Bundles and the rest render "From: $X", not a definite price — offers now declare a range or a floor instead of publishing a starting figure as the price.
  * **Product FAQs:** every FAQ source on a product page (merchant-written, generated, filtered in via `twtaeo_product_faq_pairs`) is assembled into exactly one FAQPage node at `#product-faq` — deliberately not `#faqpage`, so it never outranks the Product as the page's mainEntity. PHP and AI generators included.
  * **Smart Collections (new module, off by default):** turns intent-shaped categories ("Starter Kits", "Under $500") into published Collections — CollectionPage + ItemList on the category archive, a written explanation, llms.txt entries, and a Search Console-driven suggester. Creates no pages or categories on its own; drafting returns a proposal and writes nothing until explicitly confirmed.
  * **Promotions & Price Rules (new module, off by default):** quantity breaks and member pricing published as `UnitPriceSpecification` entries carrying their own conditions (`eligibleQuantity`, member tier); coupon codes sent to Google Merchant Center (the channel built for them) and a downloadable Bing promotions feed; reads WooCommerce coupons directly so Advanced Coupons / Smart Coupons rules are covered; names any plugin that can change a price without a code. Never writes a discount into the offer price. Weekly re-derivation keeps detected dynamic pricing from outliving the rule that produced it.
* **Note:** both new modules require WooCommerce and are off until switched on. **When AEO Ultimate for WooCommerce is active, all ported commerce writers stay unregistered** — that plugin ships the same engines, and running both would publish every node twice. Detection and admin scanning still run; only front-end emission stands down.
* **Feature: the setup wizard's Autopilot now does real work.** Previously it enabled two modules and copied company info; now everything that can be done **without AI** also runs, **in the background** (one cron event, with the completion screen polling — and driving the run itself where WP-Cron is dead): FAQ + Service schema built for every detected page still missing it, Organization/ContactPoint schema for contact pages with gaps, and og:description filled from existing meta descriptions. **Every task checks whether another plugin already provides the surface first** — pages carrying schema from any source are never candidates, and the social-description step skips itself entirely when an SEO plugin owns Open Graph output. The completion screen reports exactly what was done and what was skipped and why. AI actions are never run by Autopilot; they stay behind their explicit, cost-labelled buttons.
* **Bug fix (important, WooCommerce):** "Generate Schema for All" only processed the 200 most recent products — it looped the display scan (capped at 200) instead of walking the catalogue. It now processes the whole catalogue in cursor-driven batches of 25 with live progress ("Generating… 175 of 1,400"), and a failed request reports how far it got and resumes on the next press instead of claiming nothing happened.
* **Bug fix (important, WooCommerce):** products could sit flagged "missing schema" that Generate for All refused to touch, with no explanation — schema detection checked stored scans, Rank Math, Yoast, SASWP, WooCommerce core and inline JSON-LD, but never this plugin's own saved schema, while the generator skipped exactly those products because the schema already existed. The writer's own schema now counts as schema present, and products the generator cannot build are listed by name with a pointer to what is missing.
* **Bug fix (Image SEO):** pages with no images at all were silently omitted, so a site whose pages lack images saw an empty screen and concluded the scanner was broken. They are now listed and flagged — no image means nothing for rich results, social shares, or AI answers to show, which is a worse gap than missing alt text — with an "Add an image" action (alt text starts with an image). The header counts them, and a note points WooCommerce stores to the product-image audit on the WooCommerce tab. The shared coverage badge also now shows an explicit "No image" warning instead of a muted dash (this fixes the same silent gap on the WooCommerce products table).
* **Bug fix (Social Graph):** on stores with more than 200 pages/products the header and summary presented the 200-post scan cap as the whole site ("200 missing image" on a 1,400-product store). Counts are now labelled honestly — "200 of 1,400 pages scanned (most recent first)" — with the true published total counted from the database.
* **Bug fix (important, Dashboard):** "Scan All Pages" scanned exactly the 200 most recent posts and silently stopped — the server-side background scanner that was designed to pick up the remainder was never triggered from the button. On completion the foreground pass now hands off to the background scanner, with progress shown; large catalogs get fully scanned.
* **Bug fix (Dashboard):** products listed with missing Product schema were a dead end — clicking + alerted "No form defined for schema type: Product". Products now generate their complete schema in one click using the same generator as the WooCommerce tab (a Product node cannot be typed into a form; offers, identifiers, shipping and returns come from the product itself).
* **Bug fix (WordPress 7):** the Setup Wizard was registered as an orphan (null-parent) admin page, which on WordPress 7 resolves no admin title and triggers a strip_tags() deprecation from admin-header.php on every wizard load. It is now a real (CSS-hidden) submenu entry with a proper title.
* **Improvement:** Smart Collections and Promotions live as tabs on the WooCommerce screen (Products | Google Merchant Center | Bing Merchant Center | Smart Collections | Promotions) instead of two more sidebar entries. Their pages keep their own URLs and sub-tabs; the sidebar items are hidden and the highlight stays on WooCommerce. ⚠️ WordPress 7 note recorded in class-admin-menu.php: every classic "hidden admin page" trick now fails — remove_submenu_page() revokes access, and a null/options.php parent gets its parent_file clobbered because get_admin_page_parent() recomputes AFTER the filter — so the pages stay genuinely registered and are hidden with CSS only.
* **Bug fix (PHP 8.4):** two ported methods declared implicitly nullable typed parameters (`array $children = null`, `WC_Product $product = null`), which PHP 8.4 deprecates loudly enough to break admin headers on debug installs. Both now use explicit `?` nullable types; all ported files swept for further cases (none).
* **Bug fix (found during the port):** with the detector's breadcrumb now folded into the unified graph, AI Ready's standalone semantic BreadcrumbList would have shipped a second copy on every product page. It now stands down whenever the graph actually publishes a BreadcrumbList — checked against `published_types()` (newly ported alongside `published_ids()`), not `is_folding()`, because a folding page whose graph carries no breadcrumb still needs the standalone one.

= 2.13.0 =

* **Feature: bulk actions everywhere a per-row button existed.** Every screen that previously required clicking pages one at a time now has a one-click bulk control at the top, so a whole site can be brought to full coverage in one pass:
  * **Schema Detector → FAQ tab:** new "Generate All (N missing)" button builds FAQPage schema for every listed page from its already-detected Q&A content. Free — no AI calls. (The button previously existed only on a legacy standalone page that was no longer linked in the menu; the merged Schema Detector tab that users actually see only had per-row buttons.)
  * **Schema Detector → Service tab:** new "Generate All (N missing)" button auto-fills Service schema for every page missing it, using the same prefill the one-click metabox action uses (page title, trimmed content, detected provider/business info). Free — no AI calls. Each page's schema can still be refined afterwards via "Edit Schema".
  * **Schema Detector → Contact tab:** new "Create for All (N need attention)" button in the Business Info panel applies the shared business profile to every contact page that still needs schema, instead of clicking "Create from this info" row by row. Free — no AI calls.
  * **Image SEO:** new "Generate all missing alt text (AI)" card fills alt text across every listed page instead of per-row clicks. States the exact image count up front, asks for confirmation before anything runs (one billed AI call per image), shows live progress, and has a Stop button — pages already processed keep their new alt text.
  * **WooCommerce → Products:** new "Fill All Missing Alt Text (AI)" button next to "Generate Schema for All" runs AI vision over every product with images missing alt text. Same safeguards: image-count confirmation with billing notice, live progress, Stop button.
* **Note:** all bulk actions that use AI say so explicitly and confirm the billed call count before running; all bulk actions that are free say "no AI calls, nothing is billed" in the UI. Nothing runs without a click, and nothing existing is overwritten — bulk actions only fill what is missing.
* **Bug fix (important, AI-Ready):** every plain-text/markdown endpoint served to AI crawlers — `llms.txt`, `llms-full.txt`, `ai.txt`, Markdown content negotiation (`Accept: text/markdown`), and the Agent Skills documents — was passed through `wp_kses_post()` before output. That function is an HTML sanitizer: on non-HTML bodies it entity-encodes the text, so the llms.txt spec's required `> description` blockquote reached crawlers as `&gt; description`, every `&` as `&amp;`, and code fences in skill documents could be mangled. These responses are `text/plain`/`text/markdown` and cannot execute HTML, so escaping bought no safety and corrupted exactly the output the plugin exists to provide. All five endpoints now emit the raw document (UTF-8 validated). Found while testing 2.13.0 on a live site: its `llms.txt` literally began `&gt; AEO Dashboard`.
* **Efficiency:** the WooCommerce products table computed each product's image-alt summary twice per row; it is now computed once and reused.
* **Bug fix (WordPress 7, follow-up to the AI Client fix):** bulk AI social descriptions aborted with "Method using_max_output_tokens does not exist on WordPress\AiClient\Builders\PromptBuilder" after generating nothing. Two compounding traps: the WP wrapper serves every fluent method through `__call`, so `is_callable()` is true for **any** name — and calling an unsupported one does not throw; the wrapper **poisons the builder** and the eventual `generate_text()` returns `prompt_builder_error`, so try/catch cannot save it either. The plugin was calling `using_max_output_tokens`, which has never existed in the SDK (the real method is `usingMaxTokens`). Optional tweaks (model preference, token cap) are now gated on `method_exists()` against the underlying SDK class `WordPress\AiClient\Builders\PromptBuilder` — the only probe that reflects what the installed AI Client actually supports — and the cap uses the correct `using_max_tokens` name. Applied to both AI Client call paths (`TWTAEO_AI_Description` and `TWTAEO_AI_Client`).

* **Bug fix (important, WordPress 7 only):** the WordPress 7.0 AI Client path was dead. `WP_AI_Client_Prompt_Builder` declares only `__construct`, `using_abilities` and `__call` — every fluent method, `generate_text()` included, is served by `__call`, so `method_exists()` is **false** for all of them while `is_callable()` is true. Both guards used `method_exists()`, so `generate_text()` was **never called**: `TWTAEO_AI_Client::call_via_ai_client()` short-circuited to `''` and returned "returned no content" without making a request, and `TWTAEO_AI_Description::call_via_ai_client()` could only ever return "This WordPress AI Client version cannot generate text". Leaving the key fields blank — which the settings screen recommends on WordPress 7 — therefore disabled AI generation entirely. Now uses `is_callable()`, actually invokes the builder, and passes WordPress's own `WP_Error` message through instead of flattening it to a generic one. `TWTAEO_Rate_Limiter` had the same defect on `get_ability_name()`, logging `unknown` always; that one was cosmetic.
* **Feature:** `TWTAEO_Output_Control` — a switch for every output that could collide with another plugin, on the Schema Conflict Detector page. Nine keys: `schema_identity`, `schema_article`, `schema_service`, `schema_contact`, `schema_breadcrumb`, `llms_txt`, `sitemap`, `meta_description`, `crawler_log`. **Seven of these had no gate anywhere** — the Knowledge Graph spine, Article, Service and Contact writers, the AI meta description, AI-Ready's `/sitemap.xml` endpoint and the crawler logger all registered outside every module check, so a site running Yoast or Rank Math had duplicates with no lever short of deactivation. Writers are gated at **registration**, not at output time.
* **Note:** every key defaults to **on**, so updating changes the output of no existing install. Conflicts are detected and reported; nothing is ever switched off automatically, because silently removing schema from a working site during an auto-update is worse than a duplicate.
* **Note:** `llms_txt` and `schema_breadcrumb` **delegate** to the settings that already own them in `twtaeo_ai_ready` — read, never copied. `set()` refuses them and the admin shows those rows disabled with a link to the AI Ready screen. A second flag that can disagree with the first is the bug this shape exists to prevent.
* **Note:** ⚠️ switching an output off strips every reference into the removed nodes; a hand-off does not. They are not the same operation — a hand-off is guaranteed a same-`@id` replacement, a switch is not (Yoast addresses its WebPage by the bare URL, so references into `{url}#webpage` would orphan).
* **Improvement:** Surface hand-off to AEO Ultimate for WooCommerce. That plugin is a commerce-focused fork of this one, so the two published the *same* `@id`s — `home_url( '/#organization' )`, `home_url( '/#website' )` and `{url}#webpage` — both on `wp_head` priority 1, plus two `/llms.txt` writers, two `/sitemap.xml` writers, two meta description writers and two BreadcrumbLists. Most of that came from writers registered outside every module check, so there was no setting a merchant could reach. `TWTAEO_Commerce_Handoff` now lets the merchant pick an owner per surface on that plugin's ownership screen, and this plugin stands down from whatever was handed over.
* **Note:** entirely inert without that plugin — the handshake is a `function_exists()` check on functions only it defines, so an install running this plugin alone cannot reach any of this code. There is no new setting, option or screen here.
* **Note:** schema stands down **per node**, never per graph. A node is dropped only when the other plugin republishes one at the same `@id` (or, for the Local Pack block which carries no `@id`, a node of the same type) on that same request. Article, News, Service, Author and PR nodes — which that plugin does not publish — are untouched, and references stay valid because the replacement carries the identical identifier.

= 2.10.0 =

* **Feature:** New **Smart 404 Rescue** module (default off). Turns 404s into fixes instead of a bloated log table — nothing is written per-404. On a genuine visitor 404 it: (1) ignores hacker/bot probe traffic outright via a bundled, admin-extendable pattern list (wp-login, .env, /.git, shells, traversal, and more) so attack noise never generates work or notifications; (2) matches the dead URL to the closest published page by typo-distance; (3) auto-corrects broken **internal** links at their source with a precise, reversible undo log; and (4) 302s the visitor to the right page.
* **Feature:** Smart 404 Lost Page Recovery. Using the plugin's existing Google Search Console connection, a weekly (and on-demand) scan pulls the URLs Google still ranks you for, keeps the ones that now 404 — renamed or moved content — and matches each to the closest live page by keyword overlap. Confident matches become **permanent 301s automatically** (these apply to crawlers too, so search equity transfers); ambiguous ones are listed for one-click approval. A "Not found (404)" GSC export can also be pasted in for the long tail. Reliable and free — no third-party backlink guessing.
* **Feature:** Smart 404 admin page — run the lost-page scan, approve suggested redirects, review and remove 301s, undo healed links, and extend the hostile-probe baseline. All state is held in bounded options (no custom tables). The scan cron is torn down cleanly when the module or plugin is deactivated.
* **Feature:** New **Access & Roles** page (administrator-only). Choose which WordPress roles — an Editor, or a custom "SEO Manager" role — can use the AEO tools without being full administrators. Ideal for agencies giving a client's marketing person controlled access. It grants a dedicated `manage_twtaeo` capability that unlocks the plugin's operational pages, AJAX actions, and the per-post schema controls, but only within the plugin itself — allowed roles gain no site-wide admin power, and Settings, Modules, Diagnostics and this page stay administrator-only so credentials and configuration are never exposed. Access is computed live from a single option (no roles are mutated), so it's fully reversible.
* **Minor:** A one-time, dismissible review request appears on the plugin's own admin screens after seven days of use ("leave a review" and "already did" hide it permanently; "maybe later" snoozes it a week). Shown only to administrators, never site-wide.

= 2.9.1 =

* **Feature:** WooCommerce Product schema now emits Google's newly documented merchant-listing fields. Offers carry `priceValidUntil` (so the price never reads as stale in Search), and scheduled sales add `validFrom`/`validThrough` from the product's sale dates — on both simple products and each variation of a variable product. Products also emit the recommended `category` property, mirrored from their WooCommerce product categories so on-page markup matches the Merchant Center feed.
* **Feature:** AI Google Product Category resolver. One click in the product schema modal maps a product to its exact entry in Google's official Product Taxonomy via web-grounded AI and adds it to the schema as a `CategoryCode` object (numeric ID + full path) alongside the plain-text categories — using the new array format Google now accepts. This makes the product page a single source of truth that Google Shopping and AI shopping agents read identically, without hand-mapping a 5,000-line taxonomy. The result is cached per product and flows automatically into generated schema.
* **Feature:** Category alignment audit in the Google and Bing Merchant Center sync. Each product's on-page resolved Google Product Category is now compared against the value in the feed; disagreements surface as a new "Category Mismatch" count, factor into the E-Commerce Integrity Score, and flag the product row as a Data Gap — so agencies can catch page↔feed category drift the same way they already catch price and availability drift.

= 2.9.0 =

* **Removed:** The built-in Content Generator module and its remote-generation REST endpoints. AI-assisted drafting is available in the standalone TWT Content Generator plugin instead. AI API keys and all other AI features are unchanged.
* **Removed:** Dormant bulk OG-image generation code. Per-post, on-demand OG image generation in the editor modal remains.
* **Removed:** The unused Agency Reporting module registration; the optional TWT Agency connection itself is unchanged.
* **Compliance:** The last inline admin script block (the metabox one-click schema button) now loads via `wp_enqueue_script()` with localized strings.
* **Compliance:** Restructured the readme within directory rendering limits. The complete external-services inventory now ships in EXTERNAL-SERVICES.md and the full version history in this file.
* **Compliance:** Verified every disclosed terms/privacy link resolves and corrected outdated ones; expanded disclosures to name each API endpoint and dashboard link.
* **i18n:** Regenerated the translation template and fixed translator-comment mismatches.

= 2.8.0 =

* **Compliance:** Output-escaping pass across the plugin. Inline SVG icons are now escaped through `wp_kses()` with an explicit SVG allow-list; JSON-LD schema output is hardened with the full `JSON_HEX_*` flag set so it is inert in HTML; and the WebMCP tool definitions are handed to the browser via `wp_add_inline_script()` instead of being echoed. Every remaining intentional escape exception is documented inline.
* **Compliance:** All physical-file operations for the `/.well-known/` discovery files (API Catalog, Agent Skills Index, MCP Server Card, OAuth/OIDC) and `robots.txt` now go through the WordPress `WP_Filesystem` API instead of direct PHP filesystem functions.
* **Compliance:** Unique-prefix pass — every element that used the short `twt_` prefix (the Reviews post type and its meta keys, the merchant-sync snapshot meta, nonces, AJAX/redirect parameters, and a transient) now uses the plugin's `twtaeo_` prefix. A one-time, automatic database migration renames existing data so nothing is lost on upgrade.
* **Compliance:** base64 transport-encoding calls (cryptography, AI image payloads, OAuth/JWT, HTTP Basic, the admin-menu data-URI icon) are documented as legitimate, non-obfuscation uses.
* **Security:** The AI Ready settings save now performs independent capability and nonce checks within the handler, and Markdown content negotiation reads its `aeo_format` request flag via a registered query var rather than the raw query string.
* **Feature:** When the server filesystem is not writable, the AI Ready page now shows step-by-step instructions plus the exact, copy-and-paste contents for creating the `/.well-known/` discovery files (and the `robots.txt` Content-Signal/Sitemap directives) by hand.
* **Compliance:** Expanded the External Services disclosures with the optional industry-directory links the plugin can surface, and corrected/removed several directory URLs.

= 2.7.0 =

* **Feature:** New **Image SEO** page — scans posts and pages for images missing alt text and fills it with AI vision per item, or via a manual edit modal when no AI provider is connected. Covers the featured image and images embedded in the content, and writes both the Media Library attachment alt and the empty inline `alt=""` in the post HTML (existing alt text is never overwritten). WooCommerce product images remain on the WooCommerce page.
* **Feature:** Rich WooCommerce Product schema — the per-product schema modal now writes nested Brand, color/material/size, GTIN/MPN, `additionalProperty` custom specs, `itemCondition`, variable products as `ProductGroup` with `variesBy`, cross-sell/related/similar/accessory relationships, `aggregateRating` + individual reviews, and site-wide `OfferShippingDetails` + `hasMerchantReturnPolicy` defaults.
* **Feature:** AI Product Enrichment engine (per-product, on-demand, non-destructive) — extract attributes from descriptions (with unit conversion to your store units), tag visual attributes from the product image (color/pattern/texture/shape), generate image alt text, enhance the description (fact-grounded, with optional write-back to the product), standardize custom attributes into schema.org fields, resolve brand `sameAs` authority links (Wikipedia/Wikidata/official), and flag competitor-expected fields you're missing.
* **Feature:** Shared multi-provider AI client (Claude, OpenAI, Google Gemini, Perplexity) with JSON, web-search grounding, and image (vision) input. New "AI Enrichment" settings let you choose an extraction provider and a live-retrieval provider independently.
* **Feature:** Products table now shows a per-product schema **Optimization score** (9 weighted signals, with the missing ones on hover) and an **image-alt coverage** column — both update live after you save or fill.
* **Improvement:** `/llms.txt` now includes a Products section for WooCommerce stores — links the shop and product categories and declares that the store publishes Schema.org product data for AI shopping agents.
* **Improvement:** Updated AI model IDs to current releases (OpenAI `gpt-5.4-mini`, with correct GPT-5 reasoning-model request parameters; image generation on `gpt-image-2`) and verified Claude/Gemini/Perplexity IDs.
* **Compliance:** Updated the External Services disclosures to cover the new AI uses — WooCommerce product enrichment and Image SEO — and to state that vision features send your own post/product image URLs/contents to the configured AI provider. Added a dedicated Google Gemini API entry. All AI requests remain opt-in, key-gated, and triggered only by an explicit click.

= 2.6.1 =

* **Feature:** AI traffic & citations across the Command Center — a GA4 "AI Assistant Traffic" section (ChatGPT, Gemini, DeepSeek, Copilot, Grok) with per-source sessions, users, and conversions, plus "AI Search" panels on the Search Console and Bing tabs that link out to Google's and Bing's AI performance reports (those stay UI-only until the providers expose APIs).
* **Feature:** Index Status now inspects every published page (not just the 50 most recently edited) and shows the pages that actually need work — sorted worst-first, with a "Needs work / All scanned" toggle and pagination. Google has no bulk index-status export, so each page is inspected individually via the URL Inspection API.
* **Feature:** Inline fixes on the Index Status tab — an "Issues & Fixes" column with a modal to fill Open Graph (title/description/image, with AI generation) and generate FAQ schema without leaving the page.
* **Feature:** Analytics "Top Pages" now includes Bounce Rate, Avg Time, Events, and Conversions plus a plain-language Signal (Converting / Needs work / Likely bots / Healthy) so non-analysts can tell what to act on.
* **Improvement:** Bing tab "Query Statistics" rebuilt as a proper Top Queries table (query, impressions, clicks, CTR, average position) — it previously discarded the query term and showed a bogus -0 position from click-position data.
* **Improvement:** Index Status content check now flags missing heading hierarchy (no H2/H3) instead of a missing in-content H1, since WordPress themes render the post title as the page H1.
* **Improvement:** Modules page descriptions reviewed for accuracy — Social Graph now notes its AI og:description/og:image generation and bulk fill.
* **Fix:** Setup wizard text corrected — sitemap URL now shows the real /aeo-sitemap.xml path, the completion screen links to the Command Center, and Basics step labels match what they configure.
* **Improvement:** Plugin Check pass — annotated the legitimate-but-flagged spots (read-only display filters, core-hook calls, custom merchant-queue table queries, guarded set_time_limit) with justified phpcs:ignore comments. No behavior change.

= 2.6.0 =

* **Feature:** AI Discovery File Hits — the Command Center now tracks how many AI crawlers fetch your machine-readable discovery files (robots.txt, llms.txt, llms-full.txt) over a rolling 30-day window, so you can see whether bots are actually using your structured signals. Per-site hits sync to the agency dashboard.
* **Feature:** "Remove physical robots.txt" button on the AI Ready page — deletes a stranded physical robots.txt (e.g. one left behind by Yoast) so WordPress serves it dynamically again, clearing stale content. No FTP required.
* **Improvement:** The robots.txt panel is now state-aware and recommends the right action per site — confirms when robots.txt is already dynamic and tracked, or guides you to remove/inject when a physical file is in the way. SEO-plugin detection now covers Yoast, Rank Math, All in One SEO, and SEOPress.
* **Improvement:** llms.txt, llms-full.txt, and the dynamic robots.txt now send no-cache headers so a full-page cache (host cache or CDN) doesn't serve them without running WordPress — which would hide AI-crawler hits. The dashboard also flags a physical robots.txt as "not tracked" instead of showing a misleading zero.
* **Fix:** The IndexNow key file now returns HTTP 200 (it was returning 200's body with a 404 status, which made search engines reject it).
* **Fix:** Saving IndexNow settings no longer fails with "The link you followed has expired" — a nested form was causing a nonce conflict.

= 2.5.0 =

* **Feature:** AI Open Graph images — generate a clean, on-brand social share banner (with the post title rendered on it) for a post on demand from the Social Graph editor modal: choose a style and what to base it on, and the image is added to the Media Library and set as the post's og:image. You can optionally reuse it as the post's featured image.
* **Feature:** Stop button for the bulk AI-description job, plus per-provider notes showing each model and its relative speed and limitations.
* **Improvement:** Bulk AI generation is more resilient — a single slow API call that times out is now skipped instead of stopping the whole run (it only gives up after repeated network failures), and the per-request timeout was raised.
* **Improvement:** Pages with too little body text are skipped during AI description generation, since thin pages only produce generic descriptions.
* **Improvement:** Added a "Rescan" button to the Social Graph and Schema Detector pages, with a confirmation when the scan refreshes.
* **Fix:** Google Gemini descriptions are no longer thin or empty — the gemini-2.5 reasoning model was spending its token budget on internal "thinking"; that is now disabled for these short summaries.
* **Fix:** The Social Graph coverage table now reflects the og:title and og:description saved through the AEO editor (previously only the saved image was detected), so saved values no longer show as "missing" after a rescan.
* **Fix:** The Schema Detector FAQ tab now recognises FAQPage schema created by its own "Add FAQ Schema" action, so generated schema no longer keeps showing as "Missing".

= 2.4.1 =

* **Feature:** Bulk AI meta-description generation now runs as a background job — it processes server-side in small batches, shows live progress, and continues if you navigate away (on hosts with working WP-Cron). It also advances directly from the dashboard while the page is open, so it works even where WP-Cron is disabled. The run stops cleanly and shows the provider's message on errors such as no remaining API credits.
* **Feature:** Social Graph — fill missing Open Graph (og:description) values in two ways: reuse the meta descriptions you already have (free and instant) or have AI write share-optimised social copy in the background. The per-post editor modal also gains "Use meta description" and "Generate with AI" buttons.
* **Fix:** Updated the default Google Gemini model to gemini-2.5-flash (gemini-2.0-flash was retired).

= 2.4.0 =

* **Feature:** AI Meta Descriptions — generate concise meta descriptions with Claude, OpenAI, or Google Gemini. Use the per-post "Generate with AI" button in the editor, the bulk "Generate all missing" action on the dashboard, or turn on automatic generation when a post is first saved (existing descriptions are never overwritten). The description is written to the active SEO plugin's field when one is present, and output as a `<meta name="description">` tag when none is.
* **Feature:** Added Google Gemini as an AI provider in Settings (key resolves via the `TWTAEO_GEMINI_KEY` constant/env var or the database, like the other providers).
* **Feature:** Article schema is now output automatically on blog posts that have no SEO plugin supplying it, so posts are no longer left without article-type structured data.
* **Feature:** New Diagnostics page (System Info + recent error log) with one-click copy for support. A centralized logger captures plugin errors — including a plugin-scoped fatal-error catcher — and redacts credentials from log output.
* **Improvement:** NewsArticle schema is now opt-in per post via the "Mark as Google News article" toggle instead of being applied to every post; the news sitemap follows the same flag. This stops evergreen content from being mislabelled as news.
* **Improvement:** E-E-A-T scan now reads your Company Profile and all supported SEO plugins (Yoast, Rank Math, AIOSEO, SEOPress, The SEO Framework) when checking for sameAs links and an Organization name, and the cached scan refreshes automatically when plugins are activated or deactivated.
* **Improvement:** Index-status results now flag `noindex` and mismatched canonical URLs (the real reasons a page stays unindexed), add an "Edit page" shortcut, and explain what actually drives indexing.
* **Fix:** The "Add Certification" button on the user profile now works (its script handle was not enqueued on the profile screen).
* **Fix:** sameAs links saved via the E-E-A-T "Add Links" modal now persist and are detected on rescan.

= 2.3.3 =

* **Compliance:** Plugin Check pass — rewrote database queries cleanly, fixed global-variable usage, and corrected hook-name flags.

= 2.3.2 =

* **Compliance:** Plugin Check compliance pass; declared WordPress 6.2 as the minimum supported version and made the WordPress 7.0 AI Client call conditional.

= 2.3.1 =

* **Security/Guidelines:** The Content Generator now sends Claude and OpenAI requests through the WordPress 7.0 AI Client (`wp_ai_client_prompt()`) when no plugin-owned key is set, so the plugin no longer reads the core Connectors API keys (`connectors_ai_*_api_key`) directly.
* **Security:** The MCP endpoint (`aeo/v1/mcp`) now runs the same permission checks as the Agent Search endpoint — honouring bot-block rules, rate limits, IP allow-list, user-agent verification, and OAuth bearer auth instead of bypassing them. It remains public by default when no restrictions are configured.
* **Performance/Guidelines:** Moved all remaining inline `<style>` and `<script>` blocks (admin pages, the Hub metabox, front-end hub shortcodes, the Last Updated badge) into properly enqueued CSS/JS via `wp_enqueue_*`. JSON-LD schema output remains inline as required.
* **Compliance:** Removed promotional upsell prompts from the AI Crawler Watch and Not Indexed screens; the Not Indexed report now lists every not-indexed page locally instead of capping the free view.
* **Fix:** Corrected the EasyPRWire service domain and its Terms/Privacy links, and updated the EIN Presswire legal links. All External Services disclosures reviewed and verified.

= 2.3.0 =

* **Feature:** Added Google service-account authentication as a no-OAuth alternative for Search Console and Analytics access on agency-managed sites.

= 2.2.2 =

* **Feature:** Social Graph now auto-fills the Open Graph title and image and adds an `og:image:alt` label.
* **Feature:** The Dashboard now scans pages beyond the initial 200-page cap in the background, with progress and a completion notice.

= 2.2.1 =

* **Fix:** Resolved a fatal error when author social/credential meta was stored as an array and passed to `json_decode()`.
* **Branding:** Updated the Modules promo card from "TWT AEO Pro" to "TWT Agency" with an accurate feature list.

= 2.2.0 =

* **Feature:** Content Generator — industry/subtopic-based AI content from Claude, OpenAI, and Perplexity.
* **Feature:** AEO Command Center — Google Analytics, Search Console, and Bing Webmaster integrations.
* **Feature:** Background WooCommerce merchant sync to Google Merchant Center and Bing Merchant Center.
* **Feature:** Connector REST API for the TWT Agency hub, automatic index-status scanning, and remediation endpoints.

= 2.1.1 =

* Renamed the connected dashboard product to **TWT Agency** across the admin UI, settings, and documentation. Sync behavior is unchanged, and existing connection settings are preserved.
* Updated the External Services disclosures and added Privacy Policy / Terms of Use links covering the optional TWT Agency sync and Token Telemetry features.
* Readme polish and WordPress.org formatting and compliance fixes.

= 2.1.0 =

* **Compatibility:** Validated against WordPress 7.0; now requires MySQL 8.0.
* **Feature:** Rebuilt the AI Crawler Watch interface on the WordPress DataViews framework (React) with live client-side filtering, sorting, and status — falling back to a classic table on older environments (< WP 6.5).
* **Integration:** Registered core operations with the WordPress 7.0 Command Palette and Client-Side Abilities API. Schema Conflict Scan and IndexNow Submit All are now available via `Ctrl+K` and core AI assistants.
* **Block Editor:** Added iframe-aware state tracking. The AEO Status metabox now reads block state via the `wp.data` store, avoiding direct DOM access in the iframed editor.
* **Developer:** Added the `TWT_AEO_Key_Resolver` for API credentials, resolved in priority order (PHP constant → environment variable → Connectors → database). Constants are never written to the database.
* **Server Safeguards:** Added environment checks on admin pages that flag low memory conditions (≤ 128MB) which can conflict with WP 7.0 AI features.

= 1.0.17 =

* Added: Company Profile module — Organization schema, `article:publisher` / `article:author` OG split, `fb:app_id` (Meta Business Sync), and Yoast / Rank Math one-click sync.
* Added: Per-bot controls — custom rate limits and Block toggles for 20 known AI crawlers.
* Added: `/llms-full.txt` generation mirrored against your sitemap configuration.

= 1.0.16 =

* Added: Repeatable `EducationalOccupationalCredential` fields on the author profile.
* Added: Configurable frontend author hover cards.

= 1.0.15 =

* Added: XML sitemap generator at `/aeo-sitemap.xml`.
* Added: Conflict detection against external SEO-suite sitemaps.
* Added: IndexNow publishing hooks.

= 1.0.14 =

* Added: AI Ready toolkit — `llms.txt`, agent endpoints, and discovery/authentication routes.
* Added: MCP server configuration.

= 1.0.13 =

* Added: Command Center overview with crawler analytics and a default 30-day log retention.

= 1.0.12 =

* Added: Open Graph and social metadata output with automatic conflict avoidance.

= 1.0.11 =

* Added: Author profile schema and a configurable frontend author box.

= 1.0.10 =

* Added: Schema Detector for FAQ and Service content.
* Added: Local Pack module and LocalBusiness schema.

= 1.0.0 =

* Initial release — module loader, dashboard, settings, and data transmitter.
