# Changelog

All notable changes to **Livada SEO** are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [1.14.1] — 2026-06-01

### Fixed — critical autofill bug on the new Wikidata field
- **`<input>` was being auto-filled by browsers with the admin email.** Chrome / Firefox / Safari (and password managers like LastPass / 1Password) aggressively pre-populate any `<input type="text">` with previously stored values — most commonly the admin email associated with the WordPress login. Observed in production on `hotel-du-roc04.fr` immediately after the 1.14.0 deploy: the Wikidata field arrived pre-filled with `livada@gmx.fr`, the server-side regex `^Q\d+$` correctly rejected it, but the previously stored Q-id was **silently wiped** on every Save click that didn't explicitly touch the field.
- **HTML-side fix** (`includes/admin/views/settings-general.php`): the Wikidata input now carries `autocomplete="off"`, `data-lpignore="true"` (LastPass), `data-1p-ignore="true"` (1Password) and `spellcheck="false"` — blocks all four major autofill sources at the source.
- **Server-side fix** (`includes/admin/class-admin.php`): the `general` save handler now distinguishes three cases for `aeo_wikidata_id`: empty input → clear the setting (admin explicitly removing the Q-id), valid `^Q\d+$` → save, **non-empty but malformed → preserve the previously stored value** (assume autofill). Belt-and-suspenders against any future autofill regression.

### Why critical
- Any site that took 1.14.0 (auto-update or manual) and where the admin opens Réglages → AEO once with a browser that has the admin email stored, then clicks Save without touching the Wikidata field, would have its previously saved Q-id wiped. The site goes back to red on the 11th AI Visibility check and the Wikidata URL disappears from the JSON-LD `sameAs`. Auto-updates propagate the fix on next cron tick.

### Files
- `livada-seo.php` — version bump (header + constant).
- `includes/admin/views/settings-general.php` — autocomplete-blocking attributes on the Wikidata input.
- `includes/admin/class-admin.php` — three-case sanitisation in the `general` save handler.
- `readme.txt` + `CHANGELOG.md` + `livada.io/site/functions/api/update/info.ts` — version + upgrade notice.

## [1.14.0] — 2026-06-01

### Added — Signal 11: Wikidata entity (off-site identity)
- **New AEO setting `aeo_wikidata_id`** in General Settings → AEO — Knowledge Graph. Accepts a Wikidata Q-id (format `Q` followed by digits, e.g. `Q140007358`). Validated server-side; malformed values are dropped silently to avoid emitting broken URLs.
- **Auto-injection into schema `sameAs`** of the primary `LocalBusiness` / `Organization` entity (`@id` = `#localbusiness-1`). When a valid Q-id is declared, the canonical URL `https://www.wikidata.org/entity/Qxxxx` is appended to the existing `sameAs` array — anchoring the business in the structured knowledge base that ChatGPT, Perplexity, Gemini, Claude and Google AI Overviews consult to identify and cite real-world entities. No duplicate if the URL is already present in `schema_same_as`.
- **AI Visibility — 11th check (`check_wikidata`, max 8 points).** Green when a valid Q-id is declared (and auto-linked); orange (0 pts) when the value is set but malformed (with fix instructions); red (0 pts) when no Wikidata entity is declared (with fix instructions pointing to the AEO setting and the Livada SEO guide). The check runs entirely from the stored setting — zero external HTTP call, preserving the module's "no per-client API cost" design constraint.

### Why — positioning
- Wikidata is the structured backbone AI engines use to identify entities. WordLift (799 €/mois) and Newsroom AI already do this for enterprise / e-commerce. The Livada SEO angle is *accessible to the local SMB professional* — a single Q-id field, zero external service, zero recurring cost, self-hosted.
- Score normalisation means sites already at 100/100 will temporarily drop ~8 points until they declare their Q-id, then return to 100. By design — surfaces the action lever.

### Files
- `livada-seo.php` — version bump (header + constant), default `aeo_wikidata_id => ''`.
- `includes/class-ai-visibility.php` — new `check_wikidata()`, added to `compute()` checks array.
- `includes/class-schema.php` — primary-location `sameAs` augmented with the Wikidata URL when the Q-id is valid.
- `includes/admin/class-admin.php` — sanitisation in the `general` save handler (regex `^Q\d+$`).
- `includes/admin/views/settings-general.php` — new Wikidata ID field in the AEO — Knowledge Graph section with contextual description and live link to the entity when set.
- `readme.txt` + `CHANGELOG.md` + `livada.io/site/functions/api/update/info.ts` — version + upgrade notice.

## [1.13.0] — 2026-05-31

### Added — Livada AI Assistant integration
- **Contextual link to the Livada AI Assistant** in the *AI Bots Access* admin page. A new button "💬 Ask the Livada AI Assistant" opens `livada.io/help-ai` in a new tab, pre-loaded with the current page context, the site URL and the licence key. The assistant runs entirely server-side on livada.io (Anthropic Claude Haiku via a Cloudflare Worker) — the plugin adds zero JavaScript and zero KB to the WordPress admin. Cost is absorbed by Livada (no setup, no API key needed by the client).
- This is the first integration of a broader "AI help everywhere" rollout — future versions will add the same contextual link to other settings pages (Schema, Sitemap, Robots.txt, Redirections, Multi-location, etc.).

### Why
- Onboarding new clients is the bottleneck. Documentation alone never gets read. A one-click contextual assistant that knows which page you're on dramatically cuts time-to-first-success — and adds a unique selling point vs Yoast / Rank Math / SEOPress.

## [1.12.6] — 2026-05-31

### Added
- **French-variant locale fallback**. Sites running with `fr_BE`, `fr_CA`, `fr_CH`, `fr_LU` (or any other `fr_*` locale) now load the canonical `fr_FR` translation automatically instead of silently falling back to English. Implemented via both `plugin_locale` and `load_textdomain_mofile` filters (belt-and-suspenders against WP 5.0+ just-in-time loader bypassing `plugin_locale` on certain paths). Belgian, Canadian, Swiss and Luxembourg French-speaking clients now see the admin in French out of the box.
- Same wiring scaffolded for Portuguese variants (`pt_BR` → `pt_PT`) for when a Portuguese translation ships.

## [1.12.5] — 2026-05-31

### Added
- **French (fr_FR) translation for the AI Bots Access feature** — 41 new strings translated (page title, descriptions, category labels, table columns, button labels, the "How to read the live diagnostic" legend, etc.). The admin page no longer mixes English and French.

## [1.12.4] — 2026-05-31

### Changed
- **Plugin icon updated** to the official Livada SEO product logo (stylized "L" in indigo→magenta gradient with embedded analytics nodes/lines — the visual identity of the SEO + AEO mission).

## [1.12.3] — 2026-05-31

### Fixed
- **AI Bots Access hero header now renders correctly** (was showing white text on white background). The `_partials/header.php` include was placed outside the `.livada-seo-wrap` div, so the hero couldn't inherit the brand CSS variables (--ls-indigo, --ls-charcoal, etc.) — the gradient background never rendered. Moved the include inside the wrap.

### Changed
- **Plugin icon in WordPress Plugins page** is now the actual Livada.io brand mark (the stylized "L" in the indigo→magenta gradient) instead of the SEO loupe. Pulls from `https://livada.io/logo-icon.png` (1024×1024 RGBA) with an SVG fallback at `/favicon.svg`.

## [1.12.2] — 2026-05-31

### Fixed
- **OPcache auto-reset on plugin update**. Hostinger LiteSpeed (and other managed PHP hosts) ship with OPcache in production mode — when WordPress replaces plugin files during an update, new methods/hooks/admin views can take 10–30 minutes to appear because OPcache keeps serving the previous bytecode. Livada SEO now calls `opcache_reset()` automatically on `upgrader_process_complete` (when our plugin is part of the upgrade batch) and on activation. No-op when OPcache is disabled or `opcache_reset` is not allowed — safe everywhere.

### Why
- We discovered the issue while shipping v1.12.1 to hotel-du-roc04.fr: the new `enforce_hard_block` method and the "Enforcement mode" admin section didn't activate for 10+ minutes after install because Hostinger's OPcache cached the v1.12.0 bytecode. This release ensures every future update activates instantly.

## [1.12.1] — 2026-05-31

### Added
- **Hard block enforcement** (default ON). When enabled, blocked bots receive a real HTTP 403 from PHP at `parse_request` priority 1 — scrapers that ignore robots.txt are stopped at the server. Configurable from the AI Bots Access page (toggle at the top: "Enforcement mode"). Performance: a single user-agent string match per public request, no theme/DB load beyond the settings cache.
- **LiteSpeed Cache integration**: saving the AI Bots Access page now triggers `do_action('litespeed_purge_all')` automatically when LiteSpeed is active, so robots.txt updates without manual cache purge.

### Fixed
- **Plugin icon in WordPress Plugins page**: updated `info.ts` manifest to use `livada-seo-mark.png` (mark loupe, no "Livada" wordmark) instead of the brand logo with text.

## [1.12.0] — 2026-05-31

### Added — AI Bots Access Control

- **New admin page: Livada SEO → AI Bots Access.** Per-bot allow/block toggles for the 33 most important AI crawlers, classic search engines, archive bots and competitor SEO scrapers — grouped into 6 categories (Search Engines, AI Citation, AI Training, Archives, SEO Scrapers, Opaque). End clients can now grant or revoke AI access without touching Cloudflare or any CDN dashboard. No other WordPress SEO plugin gives this control today.
- **robots.txt enrichment.** Each bot in the registry now gets an explicit `User-agent: <slug>` block (Allow or Disallow based on the toggle), appended to the existing robots.txt content. The block is generated server-side, idempotent on re-saves, and prefixed by a `# Livada SEO — AI bot access control` marker so it's easy to identify.
- **Live diagnostic.** A "Run live diagnostic" button issues an HTTP GET against the home URL with each bot's real user-agent and shows the resulting status code (colour-coded green/yellow/red). When the bot returns 403 despite being allowed in WordPress, the admin immediately knows the block lives at the edge (Cloudflare WAF, host firewall) — not in the plugin. Results cached 60s per bot to avoid origin hammering.
- New module `class-ai-bots.php` (registry + diagnostic + AJAX) and admin view `settings-ai-bots.php`.

### Why

- The «And by AI.» promise of Livada SEO only holds when the AI crawlers can actually reach the site. spa-castellane.fr scored 100/100 AI Visibility yet GPTBot/PerplexityBot/ClaudeBot all returned 403 because Cloudflare's AI Crawl Control was blocking them by default on the Free plan. This release exposes the right toggles inside WordPress so every client can verify and fix this themselves — a major commercial differentiator vs Yoast SEO / Rank Math / SEOPress, none of which expose AI bot control.
- Default policy: legitimate search + citation + training bots are ALLOWED out of the box (visibility-first), SEO scrapers from Semrush/Ahrefs/Moz/Majestic/etc. and opaque crawlers are BLOCKED out of the box (they consume bandwidth without sending visitors).

## [1.11.2] — 2026-05-30

### Added
- **French (fr_FR) translation — 558 strings.** The plugin admin UI now appears in French on any WordPress site whose locale starts with `fr_*`. Covers the Dashboard, AI Visibility, Sitemap, Schema, Locations, Social, Redirects, Robots, Audit, Tools, Licence and SEO metabox pages. Generated from a clean i18n extraction (43 PHP files, 558 priority strings = ~95% of admin UI text). The remaining ~340 technical strings (rare audit messages, bulk editor tooltips) will follow in v1.12.0.
- `languages/livada-seo-fr_FR.po` (canonical source) + `languages/livada-seo-fr_FR.mo` (compiled, ~49 KB).

### Why
- Five production client sites are French-speaking and one of them (spa-castellane.fr) just hit **100/100 AI Visibility GRADE A** — the admin should match the user's language. Yoast SEO and Rank Math both ship French translations; Livada SEO now does too.

## [1.11.1] — 2026-05-30

### Added
- **FAQ schema detection — Pattern 4 (`<strong>Question ?</strong>` + answer).** Tourism/local pages routinely hand-write FAQs as a bold question inside a paragraph, with the answer in the same `<p>` (after the bold) or the next `<p>`. Livada SEO now turns this common author-written format into `FAQPage` JSON-LD automatically — no other SEO plugin reads it, and no page redesign is required. Works alongside the existing Elementor Toggle/Accordion and heading→paragraph patterns. Sibling-question guard prevents grabbing the next question as an answer.

### Why
- Dozens of existing tourism pages already contained real FAQs that were invisible to search/AI engines because they used bold text instead of an accordion widget. This release exposes them as structured data with zero content changes.

## [1.11.0] — 2026-05-29

### Added — AI Visibility Score

- **New "AI Visibility" admin page** with a 0–100 readiness score quantifying how well the site is prepared to be discovered and cited by AI engines (ChatGPT, Perplexity, Gemini, Claude, Copilot, Google AI Overviews). The concrete number behind the "And by AI." promise — a zero-API-cost, on-site alternative to SaaS citation trackers.
- Score is computed from 10 weighted on-site signals: llms.txt, Knowledge Graph entity, business schema completeness, declared expertise, E-E-A-T signals, FAQ structured data, question-format headings, meta description coverage, XML sitemap, and snippet directives.
- Letter grade (A–E), human headline, per-signal breakdown with actionable fixes, circular gauge in the Livada Indigo palette, and a one-click recompute. Result cached 6 h; auto-invalidated on settings save.

### Fixed

- **llms.txt heading no longer empty** when the Site Name setting is blank: the title now cascades Site Name → Schema Business Name → WordPress blog name. The organization description falls back to the WP tagline the same way.

---

## [1.10.2] — 2026-05-26

### Changed

- **Tested up to WordPress 7.0.** Confirmed full compatibility with WordPress 7.0 (zero deprecated API usage). No functional changes.

---

## [1.10.1] — 2026-05-25

### Fixed — Auto-update reliability

- **Updater hardened against host quirks.** The `wp_remote_get` call to the
  manifest server now uses a 30-second timeout (up from 10s), pins HTTP/1.1 to
  avoid HTTP/2 negotiation issues seen on older PHP curl builds, and falls back
  to a relaxed-SSL retry if the strict-verify attempt fails (broken CA bundles
  on some shared hosts).
- **WP_HTTP_BLOCK_EXTERNAL whitelist.** If the host enabled the
  `WP_HTTP_BLOCK_EXTERNAL` constant in `wp-config.php`, the plugin now
  transparently whitelists `livada.io` for the duration of the update check via
  the standard `http_request_host_is_external` filter. Sites no longer need to
  manually add the domain to `WP_ACCESSIBLE_HOSTS`.
- **Short failure cache.** A failed manifest fetch is now cached for 15 minutes
  (instead of being silently retried on every admin page-load), so we no longer
  hammer the update server when something is wrong upstream.
- **Visible error notice.** When the update check fails, an admin notice now
  appears on the Plugins / Updates / Livada SEO pages showing the error code,
  message, time and SSL mode. No more silent failures — you immediately see
  *why* an update wasn't detected.
- **Force-check resets the failure state.** Clicking "Check for updates" from
  the Plugins page now also clears the short-failure cache and stored error,
  so a retry truly starts from a clean slate.

### Compatibility

- No breaking changes. v1.10.0 → v1.10.1 is a drop-in upgrade.

---

## [1.10.0] — 2026-05-25

### Changed — Admin UI refresh

- **Brand-new admin interface.** The entire Livada SEO admin has been rebuilt
  around the official Livada.io design system: indigo `#4F46E5` primary,
  charcoal `#2C2A29` for text, ambre `#F59E0B` for premium CTAs. Every plugin
  page now opens with a hero header (gradient banner + version badge + Made-in-France
  badge) followed by a unified tabs navigation that lets you jump between
  Dashboard, Titles & Metas, Schema, Locations, Social, Sitemap, Redirections,
  Robots.txt, Site Audit, General, Tools and Licence without going back through
  the WordPress submenu.

- **Modernised dashboard.** The stat cards now use a quiet white surface with
  a 3-px coloured top-bar that signals status at a glance (green = OK, amber =
  warning, red = needs action), 30-px metric typography and inline "click to
  fix" call-outs. Hover lifts the card with a soft indigo shadow.

- **Section panels.** Plugin footprint, sitemap inventory, redirects, audit
  and every settings page are now contained in proper `.ls-section` cards with
  rounded corners, soft shadows, and a consistent header (icon + title + pill
  badges).

- **Refined widefat tables.** WordPress's default tables are restyled inside
  Livada SEO pages: pill badges replace ad-hoc spans, hover row highlight uses
  the indigo tint, headers are upper-cased small caps. Status indicators are
  now coloured dots (`.ls-dot--ok`, `.ls-dot--warning`, `.ls-dot--danger`).

- **Form controls.** Inputs, selects and textareas inside the plugin now use
  the design-system border, radius and focus ring (3-px indigo halo). Buttons
  follow the same scale: `button-primary` is filled indigo with a soft shadow,
  default buttons turn indigo on hover with a 1-px lift.

### Added

- `assets/css/admin.css` — full design-token rewrite (~600 lines) scoped under
  `.livada-seo-wrap` so the styles never leak to the rest of the WP admin.
- `includes/admin/views/_partials/header.php` — reusable hero + tabs partial
  included by every settings view.

### Notes

- Zero frontend impact. All changes are admin-side only.
- Zero new dependencies, zero new HTTP requests, single CSS file.
- The native WordPress `<h1>` in each view is hidden by CSS (the hero header
  replaces it). Pages that want to keep the legacy heading can add the
  `ls-h1-keep` class to their `<h1>`.

---

## [1.9.6] — 2026-05-16

### Fixed

- **Auto-update manifest now reachable from client sites.** The Updater's
  manifest URL was pointing directly at `https://livada.fr/livada-seo-updates/info.json`,
  which is shielded by Hostinger HCDN's JavaScript challenge — WordPress's
  `wp_remote_get()` cannot execute JS and was getting 403 responses,
  silently failing the update check. The manifest is now fetched through the
  Cloudflare Worker (`https://license.livada.fr/?action=info`), the same path
  the licence endpoints already use. The Worker runs on a Cloudflare IP
  whitelisted by HCDN, so the proxied fetch succeeds and the JSON is returned
  unchanged to the client site.

### Changed

- `Livada_SEO_Updater::DEFAULT_MANIFEST_URL` rewritten from
  `https://livada.fr/livada-seo-updates/info.json` to
  `https://license.livada.fr/?action=info`. Cache TTL and licence-gated
  download behaviour unchanged.

---

## [1.9.5] — 2026-05-16

### Fixed

- **Title/description template placeholders preserved on save.** Saving title or
  description templates (e.g. `title_category`, `title_post`, `desc_home`, and
  any `title_cpt_*` / `title_tax_*` dynamic template) no longer corrupts the
  `%category%`, `%tag%`, `%term%`, `%date%` and similar `%XX...%` placeholders.
  WordPress's `sanitize_text_field()` strips percent-encoded sequences whose
  next two characters are valid hex (`%ca`, `%ta`, `%da`, etc.), which silently
  truncated tokens such as `%category%` → `tegory%`. Both the REST API
  (`includes/class-rest-api.php`) and the admin form save handler
  (`includes/admin/class-admin.php`) now use `wp_strip_all_tags()` on template
  fields — HTML is still removed, but placeholders survive intact.

---

## [1.9.0] — 2026-05-08

### Added

- **Licence system.** `Livada_SEO_License` (new `class-license.php`) validates a
  licence key against the livada.fr licence server before allowing auto-updates.
  The key is stored encrypted (AES-256-CBC, keyed from `AUTH_SALT`) in its own
  WP option (`livada_seo_license_key`), separate from `livada_seo_settings`.
- **Licence admin page** (`Réglages → Livada SEO → Licence`). Shows current
  status (plan, email, expiry), masked key, Activate / Deactivate / Check again
  buttons — all via AJAX, no page reload.
- **Gated auto-update.** `Livada_SEO_Updater` now builds the download URL by
  appending `?license_key=&site_url=` via `Livada_SEO_License::get_download_url()`.
  Without a valid licence the `package` field is empty — WordPress shows the
  update notice but cannot download.
- **Licence server plugin** (`livada-license-server/livada-license-server.php`).
  Standalone WP plugin to install on livada.fr. REST endpoints:
  `/activate`, `/validate`, `/deactivate`, `/download`. Admin page at
  `wp-admin → Livada Licences` to generate / enable / disable keys, track
  per-site activations, set expiry dates and plans.

### Changed

- Version bumped to 1.9.0 (minor — new licence feature).

---

## [1.8.5] — 2026-05-05

### Added

- **AEO Knowledge Graph admin UI.** New section in General Settings
  with 4 fields previously DB-only: organization description (textarea),
  founding year (4-digit text), founder name (text), and areas of
  expertise (textarea, one topic per line → array). Sanitised in
  `Livada_SEO_Admin::sanitize_section('general', …)`.
- **/llms-full.txt now lists Pages before Posts.** A new `## Pages`
  section is emitted (max 50, ordered by `menu_order ASC` then title)
  ahead of `## Posts`. Better fits service / hospitality sites where
  static pages carry the high-value information.

### Fixed

- **Knowledge Graph @type was hardcoded to `Organization`.** A hotel
  used to identify as a generic Organization to AI engines; now the
  type follows the Schema → Business Type setting. Allow-list:
  Hotel, Campground, BedAndBreakfast, LodgingBusiness, Resort,
  Hostel, Motel, VacationRental, Restaurant, LocalBusiness, Store,
  TouristAttraction, MedicalBusiness, HealthAndBeautyBusiness,
  SportsActivityLocation, GovernmentOrganization, NGO. The `@id` is
  derived from the type (`/#hotel`, `/#campground`, etc.). Anything
  outside the allow-list falls back to `Organization`.
- **/llms.txt "Content" section listed WordPress categories instead
  of curated expertise topics.** When `aeo_org_knows_about` has
  entries, /llms.txt uses those. Falls back to WP categories only
  when knowsAbout is empty.

---

## [1.8.4] — 2026-05-01

### Fixed

- **Dashboard "Sitemap refresh" and "IndexNow" cron events stayed
  "not scheduled" forever.** They are not recurring crons by design —
  sitemap is a 12 h transient cache invalidated on `save_post`, and
  IndexNow uses one-shot `wp_schedule_single_event` per URL change.
  The dashboard now shows the freshest sitemap cache expiry
  ("fresh — refreshes in 11 hours" or "on demand") and the next
  pending IndexNow ping. The `wp livada-seo doctor` command was
  updated to match.
- **Sitemap cache freshness query targeted a non-existent transient
  key** (`livada_seo_sitemap_index`). The sitemap module actually
  stores per-post-type / per-taxonomy transients prefixed
  `livada_seo_sitemap_*`. The dashboard and doctor command now
  query the maximum expiry across all of them.

---

## [1.8.3] — 2026-05-01

### Fixed

- **Score not recalculated when meta is updated via REST API.** The
  REST flow doesn't trigger `save_post`, so `PUT /livada-seo/v1/meta/{id}`
  and `/meta/bulk` left `_livada_seo_score` stale. Both endpoints now
  call `Livada_SEO_Analysis::analyze()` after writing. Standard WP
  REST updates (`/wp/v2/posts/{id}` with `featured_media`, `title`,
  etc.) also trigger re-analysis via `rest_after_insert_post`. Direct
  `_thumbnail_id` changes from Quick Edit or custom plugins are
  caught by an `updated_post_meta` watchdog.
- **"No images found in content" was scored RED.** Many service /
  listing pages legitimately have no inline images; this distorted
  the total. Now scored ORANGE (informational). Featured image
  stays a separate RED-on-missing signal.

---

## [1.8.2] — 2026-05-01

### Fixed

- **Focus keyword matching ignored accents and required contiguous
  phrase.** "réparation PC Castellane" now correctly matches
  "Réparation et maintenance PC à Castellane" in titles, descriptions,
  H1, first paragraph, URL slug, and density. Implementation: new
  `normalize_for_matching()` helper (NFD via intl with fallback table),
  plus `keyword_matches()` that tries exact accent-insensitive substring
  first then falls back to all-words-present matching.
- **Featured image not detected when set via REST `featured_media`.**
  Some integrations write `_thumbnail_id` without flushing the object
  cache that `has_post_thumbnail()` reads from inside the same request.
  Now triple-checks: WP helper, direct `_thumbnail_id` meta, and
  `get_post_thumbnail_id()` getter.
- **Elementor HTML widgets were not analysed.** When the Elementor
  frontend renderer returns empty (common during save_post in REST
  contexts), the analyser now walks the raw `_elementor_data` JSON
  and extracts text from `html`, `editor`, `heading`, `button`,
  `image`, and generic textual settings. Pages built primarily with
  HTML widgets no longer score 0.
- **URL slug match is now word-by-word.** Slug
  `reparation-maintenance-pc-castellane` matches keyword
  "réparation PC Castellane" because every significant keyword word
  is found, even when intermediate slug segments interleave.

---

## [1.8.1] — 2026-04-30

### Fixed

- **"Check for updates" link returned "Le lien suivi est expiré"** —
  the `admin_init` handler ran on every page load including the
  post-redirect URL (`?livada_seo_check_update=done`), which re-triggered
  `check_admin_referer()` against a query string that no longer carried
  the nonce. The handler now matches the parameter exactly against `1`
  and ignores any other value, so the success redirect completes cleanly.

---

## [1.8.0] — 2026-04-30

Self-hosted updater so future versions appear under "Mettre à jour
maintenant" on the WordPress Extensions page exactly like a plugin from
the wp.org repository.

### Added

- **`includes/class-updater.php`** (`Livada_SEO_Updater`).
  - Hooks `pre_set_site_transient_update_plugins` to inject our entry
    when a newer version exists in the remote manifest.
  - Hooks `plugins_api` so the "Voir les détails de la version X.Y.Z"
    modal shows our changelog + screenshots.
  - Hooks `upgrader_process_complete` to bust the local cache after a
    successful upgrade and log a `plugin_upgraded` event in the activity
    log.
  - Adds a "Check for updates" link on the plugin row (`plugin_row_meta`)
    that force-refreshes the manifest + WP's `update_plugins` transient.
  - Manifest URL is `https://livada.fr/livada-seo-updates/info.json` by
    default. Override via filter `livada_seo_update_url`.
  - Cache TTL is 12 h, override via `livada_seo_update_cache_ttl`.
  - Kill-switch via `livada_seo_update_disabled`.
- **`Update URI:` plugin header** pointing to the manifest URL so
  WordPress core knows the plugin manages its own updates and never
  attempts a wp.org lookup that would fail.
- **`info.example.json`** documenting the manifest schema with sane
  defaults (icons, banners, sections.changelog, sections.upgrade_notice).
- **`UPDATES.md`** documenting the release workflow: bump version, run
  `build-zip.ps1`, edit `info.json`, upload both files to
  `/public_html/livada-seo-updates/`. Total ≈ 1 minute per release.
- **`build-zip.ps1`** PowerShell helper that produces the standard
  WordPress plugin ZIP via Python (Unix forward-slash paths, single
  root folder named `livada-seo`).

### Notes

- The first install on a new site still uses the wp-admin upload route
  (Extensions → Ajouter → Téléverser une extension). After that, all
  subsequent updates flow through the standard WordPress upgrade UI.
- The activity log records `plugin_upgraded` events with the post-upgrade
  version so you can audit auto-update activity across sites.

---

## [1.7.1] — 2026-04-30

### Fixed

- **`Livada_SEO_Robots::filter_robots_txt`** now normalises line endings,
  defensively splits glued directives, and ensures a single trailing LF.
  Previously, when the `livada_seo_settings.robots_txt` option lost its
  newlines (DB export/import quirk, paste from a flattened source), the
  served `/robots.txt` rendered all directives concatenated on a single
  line. Still valid for crawlers but visually ugly. The fix injects a
  newline before every well-known directive keyword (`User-agent:`,
  `Allow:`, `Disallow:`, `Sitemap:`, `Crawl-delay:`, `Host:`) when no
  newline is present in the source content.

---

## [1.7.0] — 2026-04-30

Five new features: onboarding wizard + AI rewrite + GSC import + internal
link graph + OG image generator.

### Added

- **Onboarding wizard** (`includes/class-onboarding.php`).
  - Hidden admin page `livada-seo-welcome` rendered as 4 progressive steps:
    business basics (name + city + country + schema type, also seeds
    `livada_seo_locations[0]`), modules selector with three tiers
    (recommended / modern / optional), Anthropic API key (optional, encrypted
    on save via `Livada_SEO_Image_Bulk::encrypt_key_for_storage`), final
    audit run + redirect to dashboard.
  - Activation hook in `livada-seo.php` flags first-activation only when
    `livada_seo_settings` does not exist; the next admin load redirects to
    the welcome page via a 60-second transient. Subsequent activations or
    reactivations after a settings reset do **not** trigger the wizard.
  - Skip button on every step persists `livada_seo_onboarding_done` so the
    flow is never shown again.
- **AI rewrite mode** in `class-ai.php`.
  - Adds `'rewrite'` to `Livada_SEO_AI::ALLOWED_TYPES`.
  - `suggest( $post_id, 'rewrite', $extra )` accepts arbitrary text
    (defaults to the post content excerpt when `$extra` is empty), returns a
    rewritten version that preserves language, facts, and approximate length.
    Newlines preserved (unlike the title/desc/keyword paths which collapse
    whitespace).
  - REST endpoint `POST /ai/suggest/{id}` now accepts an `extra` body field;
    the MCP tool `livada_ai_suggest` likewise.
- **Google Search Console keywords import** (`includes/class-gsc-import.php`).
  - Tools-page CSV uploader; auto-detects delimiter (comma / semicolon /
    tab); maps GSC's English + French header variants for Query / Clicks /
    Impressions / Position / Page columns.
  - Per-URL: matches via `url_to_postid`, stores the top 25 entries in
    `_livada_seo_gsc_keywords` post meta (sorted by clicks DESC then
    impressions DESC).
  - Site-wide overflow: when the export has no Page column, the rows land
    in a single `livada_seo_gsc_global_keywords` option (top 200).
  - Logged via `Livada_SEO_Activity_Log::log( 'gsc_import', … )`.
  - Surfaces in the SEO metabox as a collapsible "Search Console — top
    queries" table.
- **Internal Link Graph** (`includes/class-link-graph.php`).
  - New admin sub-menu `livada-seo-links`.
  - Crawls up to 1 000 posts (post types from `sitemap_post_types` minus
    template deny-list), counts inbound / outbound internal edges via
    canonical-path matching (handles relative + absolute, http vs https,
    trailing-slash variants).
  - Output: per-post inbound / outbound counts, top 10 hubs, orphans
    (zero inbound), lonely cornerstones (cornerstone with zero inbound).
  - 12-hour transient cache, invalidated on `save_post` / `delete_post`,
    AJAX rebuild button.
- **Programmatic OG image generator** (`includes/class-og-image.php`,
  module slug `og_image`, opt-in via the `og_image_enabled` setting).
  - Renders 1200×630 PNG on-demand at
    `/wp-json/livada-seo/v1/og-image/{post_id}.png` when no custom
    `_livada_seo_og_image` meta is set on the post.
  - GD-based: tries TTF fonts in `og_image_font_path` first, then a list
    of common Linux / macOS / Windows system font paths, falls back to
    GD's built-in font 5 if none found.
  - Settings: `og_image_enabled`, `og_image_brand_color`,
    `og_image_accent_color`, `og_image_font_path`.
  - Filter `livada_seo_og_image_url` is checked from `class-opengraph.php`
    as the *last* image-resolution step, so any explicit override still
    wins.
  - 24-hour transient cache keyed by post id + 10-char hash of (title +
    brand + accent + site name).

### Changed

- `class-opengraph.php::get_og_image()` now applies the
  `livada_seo_og_image_url` filter as a final fallback.
- `livada-seo.php` activation now distinguishes first activation
  (`livada_seo_settings` absent) from re-activation. Only the first triggers
  the onboarding redirect.
- `uninstall.php` clears the new options
  `livada_seo_onboarding_done` and `livada_seo_gsc_global_keywords`.

---

## [1.6.0] — 2026-04-30

Devops + observability pass.

### Added

- **Settings JSON export / import** under Livada SEO → Tools.
  - `Livada_SEO_Admin::export_settings` streams a JSON document with the
    full `livada_seo_settings` option + the `livada_seo_locations` option,
    intentionally stripping the encrypted Anthropic API key.
  - `Livada_SEO_Admin::import_settings` accepts a previously exported file,
    merges over the current settings (additive — does not clear unmentioned
    keys), full-replaces locations, and preserves the existing API key
    even if the import payload tries to inject one.
- **Activity Log** module (`includes/class-activity-log.php`,
  `Livada_SEO_Activity_Log`).
  - New table `{prefix}livada_seo_activity_log` with id / created_at /
    user_id / actor / event / object_type / object_id / summary / data
    columns and indexes on (event, created_at).
  - Hard cap at 1 000 rows, auto-trimmed on insert, so the table stays
    bounded even on busy sites with chatty MCP / WP-CLI traffic.
  - Actor detection: `cli` / `rest` / `cron` / `ajax` / `web` so operator
    actions can be told apart from automated agent activity.
  - Admin viewer at **Livada SEO → Activity Log** — last 200 entries with
    a "Clear log" AJAX button. Persists itself an entry recording the clear.
  - Wired listeners: `update_option_livada_seo_settings`,
    `update_option_livada_seo_locations`, `Livada_SEO_Redirects::add_redirect`
    / `delete_redirect`, `Livada_SEO_AI::suggest`, `Livada_SEO_Site_Audit::run_audit`.

### Changed

- `uninstall.php` drops the new `livada_seo_activity_log` table and the
  `livada_seo_activity_log_version` option.

---

## [1.5.1] — 2026-04-30

E-A-T pass on the schema graph + a small sitemap dashboard.

### Added

- **Person / Author schema** in `class-schema.php`:
  - `Livada_SEO_Schema::author_anchor( $user_id )` — stable `@id` URL keyed
    on the author archive permalink.
  - `Livada_SEO_Schema::build_author_person()` — assembles a Person node
    from the WP user profile (display_name, description, user_url, avatar)
    plus optional user meta (`livada_seo_job_title`, `livada_seo_same_as`,
    standard `twitter` / `facebook` / `linkedin` / `instagram` / `youtube`
    / `github` fields when populated by Yoast / Edit Author Slug / etc.).
    `worksFor` references the primary business location's `@id`.
  - The Article builder now references the author by `@id` instead of
    inlining a flat `{ name }` Person, and the @graph emits the Person node
    on singular posts and author archives so the reference resolves.
  - The schema preview in the metabox includes the Person too.
- **Sitemap inventory widget** on the dashboard: per-post-type +
  per-taxonomy URL counts (read via `wp_count_posts` / `wp_count_terms`),
  total URL count, and direct links to every sub-sitemap.

---

## [1.5.0] — 2026-04-30

Observability + portability.

### Added

- **Live JSON-LD schema preview** in the SEO metabox. New
  `Livada_SEO_Schema::build_preview_graph( $post_id )` builds the exact
  `@graph` the front-end would emit, with a temporary `setup_postdata` +
  synthetic `WP_Query` shim so `is_singular` / `get_the_ID` behave as if
  the post were being rendered, and globals are restored before returning.
  REST endpoint `GET /schema/preview/{id}` plus a collapsible viewer panel
  inside the metabox (lazy-loaded via `fetch` with the WP REST nonce).
- **Full SEO-data CSV export** under Livada SEO → Tools.
  `Livada_SEO_Admin::export_seo_csv` streams one row per published / draft /
  pending / private / future post across the configured `sitemap_post_types`
  (template CPTs auto-stripped) with: id, post_type, status, title,
  permalink, SEO title, meta description, focus keyword, SEO score,
  readability, noindex, nofollow, cornerstone, canonical, OG image,
  schema_type, location_id, sitemap_excluded. UTF-8 BOM for Excel.
- **Redirect chain / loop detector** as a new Site Audit site-wide check
  (`redirect_chains`). Walks every redirect target up to 5 hops and reports
  any cycle (red) or chain where the target is itself a redirect source
  (orange). Pure DB introspection — no outbound HTTP, scales linearly to
  the redirect count.

---

## [1.4.0] — 2026-04-30

Workflow + observability pass.

### Added

- **Bulk AI alt-text fill** on the Image SEO admin page. New "AI ✨ fill empty
  alts on this page" button runs the existing `livada_seo_ai_generate_alt`
  endpoint sequentially on every visible row whose alt is empty, with a
  250 ms throttle and a confirmation prompt before billing the API.
- **Bulk schema-type assigner** in the Bulk Editor. Per-row checkboxes plus a
  toolbar dropdown (HowTo / Product / Event / Recipe / Review / Offer / clear)
  let editors apply a `_livada_seo_schema_type` to N posts at once. New AJAX
  endpoint `wp_ajax_livada_seo_bulk_set_schema` with whitelist validation.
- **Plugin footprint widget** on the dashboard. Reports transient cache size,
  redirects table row count, 404 log row count, time since the last audit run,
  and a status table for the three Livada cron events with relative
  "next run" timing (`human_time_diff`).
- **Live keyword density meter** in the metabox check panel. Computes the
  density of the focus keyword inside the meta description as the user types,
  using a global escaped-regex match. Surfaced as a green/orange/red check
  that complements the on-save full-content density score from
  `class-analysis.php`.

---

## [1.3.0] — 2026-04-30

Workflow polish on top of the AI module shipped in 1.2.0.

### Added

- **Bulk AI Fill** in the Bulk Editor (`includes/class-bulk-editor.php`). New
  toolbar at the top of the Bulk Editor lets the operator pick a target field
  (description / title / focus_keyword), opt into overwrite mode, and run
  AI suggestions across the visible posts. Sequential client-side loop with
  250 ms throttle between requests; rows refresh in place with the new
  value + score. AJAX endpoint `wp_ajax_livada_seo_bulk_ai_fill` reuses the
  existing `Livada_SEO_AI::suggest()` pipeline and re-runs the analyser
  after each save so the on-screen score stays current.
- **Cornerstone reverse counter** in the SEO metabox. One bounded SQL
  query (`postmeta` JOIN with `LIKE` on the post permalink) counts how many
  cornerstone posts already link to the current page. Surfaces under the
  cornerstone checkbox so editors can spot under-linked flagship content.
- **Quick-wins admin notice** (`Livada_SEO_Admin::render_quick_wins`)
  shown on Livada SEO admin pages and the WP Dashboard. Surfaces three
  unfinished setup states with direct links: LocalBusiness data missing,
  Anthropic API key missing, > 50 % of posts without a focus keyword.
  Per-user dismissable via `?livada_seo_dismiss_qw=1`, persisted as
  `livada_seo_quick_wins_dismissed` user meta.

---

## [1.2.0] — 2026-04-30

Brings Livada to ~100 % feature parity with Rank Math PRO by adding AI
content suggestions on top of the existing AI alt-text. Single-tier, GPL,
no premium upgrade required.

### Added

- **AI suggestions module** (`includes/class-ai.php`, `Livada_SEO_AI`).
  Calls Claude (`claude-sonnet-4-5-20250929`) via the Anthropic Messages API
  with the post's title, focus keyword, current meta, content excerpt (1 500
  chars max) and WordPress locale. Returns one length-capped string:
  - `title`       → ≤ 60 chars
  - `description` → ≤ 160 chars
  - `keyword`     → ≤ 30 chars (1–4 words)
  Reuses `Livada_SEO_Image_Bulk::get_anthropic_key()` so a single API key
  powers both alt-text and content suggestions.
- **Metabox UI** — small "AI ✨" button next to each of the three fields,
  visible only when the API key is configured. Confirms before overwriting
  a non-empty value, fires the same input event so the live SERP preview
  and live checks update immediately.
- **REST endpoint** `POST /wp-json/livada-seo/v1/ai/suggest/{id}` with body
  `{ type: title|description|keyword, save: bool }`.
- **MCP tool** `livada_ai_suggest` for driving suggestions from Claude Code.

### Notes

- Cleaning step strips wrapping quotes / markdown / leading bullet markers
  the model sometimes emits despite the prompt — output is always a plain
  string ready to paste.
- The model is instructed to match the source language, never translate.
  Tested against FR / EN / NL / DE / IT post content.

---

## [1.1.2] — 2026-04-30

Quality-of-life pass focused on per-post multilingual control and devops.

### Added

- **Hreflang manager UI** in the SEO metabox. New repeater under the Advanced panel lets editors maintain a `[ lang => url ]` map without touching post meta directly. The save handler normalises BCP-47 codes (language lowercase, region uppercase, `_` → `-`) and strips invalid characters before persisting. `x-default` is kept verbatim.
- **`wp livada-seo doctor` WP-CLI command**. Single-pass self-test that prints a coloured table covering: WP/PHP versions, required extensions (DOM, libxml, mbstring, openssl), custom-table presence, cron events scheduled, sitemap rewrite-rules freshness, robots.txt sitemap directive, LocalBusiness completeness, Anthropic API key roundtrip, and active SEO plugin conflicts. `--ping` adds an end-to-end probe of the REST namespace.

---

## [1.1.1] — 2026-04-30

Quick post-1.1.0 quality-of-life pass. No breaking changes, no schema changes.

### Added

- **Schema validation buttons** in the SEO metabox: one-click "Test in Google Rich Results" and "Validate on Schema.org" links that open the current post permalink in the corresponding tester. Visible only when the post has a permalink (skipped on auto-draft).
- **Live Flesch reading-ease** in the metabox check panel. Mirrors the PHP heuristic in `class-analysis.php` (vowel-group syllable counter, silent-trailing-e correction, clamp 0–100) so the live indicator and the on-save analyser agree.
- **Broken internal-link detector** as a new per-post check in Site Audit (`broken_internal_links`). Walks every `<a href>` in the post content, filters internal links, resolves them with `url_to_postid()` and falls back to a public-taxonomy term lookup. Status: green when 0 broken, orange at 1, red at 2+. Pure DB introspection — no outbound HTTP.

---

## [1.1.0] — 2026-04-30

Major release. Brings the plugin to ~95 % feature parity with Rank Math PRO and
above SEOPress PRO. Closes the four-day improvement plan (Site Audit,
Multi-location, Image SEO bulk + AI, WooCommerce + News sitemap).

### Added

#### Modules

- **Site Audit** (`includes/class-site-audit.php`) — opt-in module, daily
  WP-Cron crawl (`livada_seo_cron_audit`), 8 per-post checks + 5 site-wide
  checks, 6 h transient cache, sortable admin table, CSV export, REST endpoint
  (`GET /audit`, `POST /audit`), WP-CLI subcommand (`wp livada-seo audit`),
  dashboard card.
- **Multi-location LocalBusiness** — new `livada_seo_locations` option, new
  admin page **Locations** with collapsible repeater, per-post
  `_livada_seo_location_id` post meta, metabox dropdown (only visible when
  ≥ 2 locations exist). Location 0 keeps the legacy `#localbusiness` `@id`
  anchor for backward compat; subsequent locations use `#localbusiness-2`,
  `#localbusiness-3`, … REST CRUD under `/locations`.
- **Cornerstone marker** — `_livada_seo_cornerstone` post meta + metabox
  checkbox + admin star column + dashboard card. Cornerstone posts get a +5
  score boost in internal-link suggestions.
- **Image SEO bulk editor** (`includes/class-image-bulk.php`) — admin sub-menu
  listing every attachment missing `_wp_attachment_image_alt`, inline AJAX
  edit, bulk-apply same alt to selection. Optional **one-click AI alt-text
  generation** through the Anthropic Claude vision API.
- **WooCommerce module** (`includes/class-woocommerce.php`) — auto-emits
  Product + Offer + AggregateRating + Review JSON-LD on product pages, full
  support for variable products with one Offer per visible variation, brand
  detection from `_brand` / `pwb-brand` / `product_brand` taxonomies, GTIN +
  MPN from common Woo extension meta keys. Auto-augments `sitemap_post_types`
  / `sitemap_taxonomies` defaults when Woo is active.
- **Google News sitemap** at `/sitemap-news.xml` — Google News namespace,
  48 h window, 1 h transient cache, listed in the sitemap index only when
  matching posts exist.
- **Yoast SEO importer** (`includes/class-import-yoast.php`) — batched
  migration of post meta, title/desc templates (with `%%var%%` → `%var%`
  conversion), social profiles, and Yoast Premium redirects (301/302).
- **TranslatePress hreflang adapter** — pulls translated URLs from
  `TRP_Url_Converter`, silences TranslatePress's own hreflang output via
  `trp_disable_hreflang` to avoid duplicates.
- **404 Monitor** (`includes/class-404-monitor.php`) — dedicated table,
  object-cache throttle (60 s per path), top-200 admin view, **one-click
  "convert to redirect"**.
- **Bulk Editor** (`includes/class-bulk-editor.php`) — admin sub-menu, edit
  titles / meta descriptions / focus keywords / noindex flags across many
  posts at once with AJAX-per-cell save. Re-runs the analyzer after each
  change so the score updates live.
- **Admin Filters** (`includes/admin/class-admin-filters.php`) — query var
  `livada_seo_filter` filters the post-list to `low_score`, `missing_desc`,
  `noindex`, `no_keyword`, or `cornerstone`. Used by the dashboard cards.
- **Internal Link Suggestions** (`includes/class-link-suggestions.php`) —
  live in the metabox, debounced 500 ms, scored by token match + cornerstone
  boost.
- **MCP server for Claude Code** (`mcp-server/`) — TypeScript stdio
  implementation, 28 tools, Application Password auth. Lets Claude Code drive
  every plugin action from a chat conversation.

#### Schema enrichments

- New top-level types: HowTo, Product, Event, Recipe, Review, Offer.
- New LocalBusiness sub-types: Campground, LodgingBusiness, Hotel,
  BedAndBreakfast, Resort, Hostel, Motel, Restaurant, Store,
  TouristAttraction.
- Tourism / lodging fields: star rating, room count, pets allowed, check-in /
  check-out times, amenities (`amenityFeature`), aggregate rating.

#### Endpoints

- **REST**: `/audit`, `/audit (POST)`, `/locations` (full CRUD),
  `/locations/{i}`, `/404`, `/404/{id}`, `/404/{id}/redirect`,
  `/images/missing-alt`, `/images/{id}/alt`, `/images/{id}/ai-alt`,
  `/sitemap/diagnose`, `/import/yoast`, `/woo/product/{id}/schema`.
- **WP-CLI**: `audit`, `sitemap diagnose`, `sitemap flush --type=<slug>`.
- **MCP**: 28 tools — settings/meta/redirects CRUD plus all of the above.

#### UX

- **Mobile SERP preview** in the metabox alongside the desktop variant.
- **Flesch reading-ease score** stored separately (`_livada_seo_readability_score`)
  and surfaced as an admin column.
- **Dashboard cards now clickable** — every count routes to a filtered
  post-list view via the admin-filters module.
- **CSV import/export** for redirects.
- **Sitemap auto-strip** of known template CPTs (`elementor_library`,
  `jkit-header`, `jkit-footer`, `e-landing-page`, `oceanwp_library`,
  `astra-advanced-hook`, `cartflows_step`, `cartflows_flow`,
  `fl-builder-template`, `wpbakery_layout`, `tcb_lightbox`, `brizy-template`,
  `jet-*`).
- **Anthropic API key** stored encrypted at rest with AES-256-CBC using a key
  derived from `wp_salt('auth')`.

### Changed

- `class-schema.php::build_local_business()` → `build_local_businesses()`
  (plural). Returns an array of nodes for the @graph. `add_lodging_fields()`
  now takes a `$loc` array. `Service`, `Article`, and `Offer` builders use
  `post_location_anchor()` so the per-post `_livada_seo_location_id` retargets
  `provider` / `publisher` / `seller` references.
- General settings page now lists the 7 modules previously hidden
  (sitemap_html, toc, image_seo, analytics, geo, aeo, site_audit) plus the
  new `woocommerce` module.
- Sitemap rewrite rule pattern relaxed to `[a-z0-9_-]+` to allow hyphens in
  sub-sitemap slugs (notably `news`).
- SEOPress importer now shares the same `livada_seo_admin_nonce` as the rest
  of the admin AJAX surface.

### Fixed

- `LIVADA_SEO_DEBUG` was enabled by default in 1.0.3 with verbose error
  logging; now defaults to `false`. Define `LIVADA_SEO_DEBUG = true` in
  `wp-config.php` to opt back in.
- Sitemap transient cache now invalidates when `_livada_seo_sitemap_exclude`
  or `_livada_seo_noindex` post meta changes outside `save_post` (REST / AJAX
  / CLI).
- SEOPress import nonce mismatch (was using
  `livada_seo_import_nonce` while the JS sent `livada_seo_admin_nonce`).
- Removed dead `Livada_SEO_Admin::ajax_import_seopress` handler that called
  a non-existent `import_from_seopress` method (would have produced a fatal
  error if the broken legacy AJAX path had been triggered).
- Hex color validation in the analysis admin column to keep the inline
  `style` attribute safe.

### Schema / data structures

- New option: `livada_seo_locations` (array of location arrays).
- New post meta: `_livada_seo_location_id`, `_livada_seo_cornerstone`,
  `_livada_seo_readability_score`, `_livada_seo_offer_*`, `_livada_seo_schema_type`,
  `_livada_seo_faq_schema`, `_livada_seo_howto_steps`, `_livada_seo_product_*`,
  `_livada_seo_event_*`, `_livada_seo_recipe_*`, `_livada_seo_review_*`,
  `_livada_seo_hreflang`.
- New custom table: `{prefix}livada_seo_404_log`.
- New transients: `livada_seo_audit_results`, `livada_seo_sitemap_news`,
  `livada_seo_redirects_map`.

### Notes for migrators

- The legacy single-location `schema_*` settings remain as a fallback. When
  `livada_seo_locations` is empty, the plugin synthesises a single-location
  array from those settings so existing JSON-LD output is unchanged.
- After upgrading, open **Livada SEO → Locations** once and save: this
  promotes the legacy data into the multi-location store, after which the
  Locations page is the canonical edit surface.
- The bundled MCP server is **not** auto-built. Run `npm install &&
  npm run build` inside `mcp-server/` if you intend to use it from Claude
  Code.

---

## [1.0.3] — 2026-03-15

Initial public release.

### Added

- Meta module (title, description, canonical, robots).
- XML sitemap with image / video tags and 12 h transient cache.
- HTML sitemap, robots.txt manager.
- Schema module: WebSite, LocalBusiness, Article, FAQPage, Service,
  BreadcrumbList.
- Open Graph + Twitter Cards.
- Breadcrumbs (shortcode + template tag + Elementor dynamic tag).
- Redirect manager (301/302) with hit counter.
- On-page SEO analysis with 16 checks.
- IndexNow async ping module.
- AEO / GEO modules — `/llms.txt`, AI markers, speakable schema.
- SEOPress importer.
- REST API namespace `livada-seo/v1` (settings, meta, redirects, analysis,
  dashboard, sitemap flush, IndexNow ping).
- WP-CLI subcommands: `settings`, `meta`, `redirects`, `import`, `sitemap`,
  `analyze`.
