# Changelog

## 5.0.9 - 2026-06-09
### Added
- **AI Files manager** — create & edit root text documents (`llms.txt`, `llms-full.txt`, `ads.txt`, `app-ads.txt`, `security.txt`, `humans.txt`) from WP admin (IM-Press &rsaquo; AI Files) **or** via REST (`GET/POST/DELETE /wp-json/im-press/v1/files (name+content params)`, `POST /files-generate-llms`). Files are written to the site root by PHP, so AI agents can manage them with an application password — no SSH/FTP needed. Includes a one-click "Generate llms.txt from pages".


## 5.0.7 - 2026-06-09
### Changed
- **Friendlier free tier.** Removed the per-feature upgrade banners: previously each of ~17 gated modules printed its own "Upgrade" notice on every wp-admin page, stacking into a wall of nags on free installs. Now a single, gentle, dismissible notice (shown only on IM-Press screens) covers it, with a direct "Activate license" link. No change for licensed sites.


## 5.0.6 - 2026-06-09
### Fixed
- **Duplicate SEO head tags (fleet-wide).** IM-Press loaded two SEO emitters (legacy `SEO` and `SEO_V2`) that each output canonical/description/OG/JSON-LD, plus WordPress core's `rel_canonical` — producing up to 3 `<link rel="canonical">` and 2-3 JSON-LD blocks per page. Consolidated to a single source: legacy `SEO` is now the sole `wp_head` emitter, it honors SEO Workspace (`_imp_seo_*`) overrides so workspace-configured SEO still renders, WordPress core `rel_canonical` is removed, and `SEO_V2`'s duplicate `output_head_tags`/`output_canonical` hooks are disabled (its REST/analysis/meta backend is unchanged). Result: exactly one canonical + one JSON-LD per page.

# IM-Press Changelog

All notable changes to IM-Press are documented in this file.

Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

---

## [5.0.5] — 2026-05-24 — **Patch — WP 7.0 Abilities API hook compliance + MCP Adapter detection fix**

### Fixed

#### `class-abilities.php::init()` — wrong hook for category registration
WP 7.0 final ships an Abilities API that enforces `_doing_it_wrong` on registration: `wp_register_ability_category()` MUST be called inside the `wp_abilities_api_categories_init` action, and `wp_register_ability()` MUST be called inside `wp_abilities_api_init` — verified directly against WP 7.0 core at `wp-includes/abilities-api.php`. If either function is called outside its required action, WP returns `null` and emits a doing-it-wrong notice.

Our previous code registered BOTH categories and abilities under the single `wp_abilities_api_init` hook. Effect: categories silently returned `null` and never registered, then ability registrations referencing those categories failed downstream → "Abilities endpoint returns 0 abilities" on WP 7.0 sites with IMPRESS_AGENT_LAYER enabled.

**Patch:** split the hook registration into two:

```php
add_action( 'wp_abilities_api_categories_init', array( __CLASS__, 'register_categories' ) );
add_action( 'wp_abilities_api_init',            array( __CLASS__, 'register_abilities' ) );
```

`register_categories()` changed from `private static` to `public static` (must be reachable as a WP hook callback). `register_abilities()` no longer self-calls `register_categories()` — categories are guaranteed registered earlier because their hook fires first.

#### `class-mcp-bridge.php::is_mcp_adapter_installed()` — wrong class name in primary detection
The first detection check used `class_exists( 'McpAdapter' )` (bare class), but the WordPress MCP Adapter plugin uses the namespaced class `\WP\MCP\Core\McpAdapter`. Effect: the primary detection branch never fired, even when the adapter was installed and active. Falling back to the secondary checks (action-hook and active_plugins string match) worked unevenly.

**Patch:** corrected to `class_exists( '\\WP\\MCP\\Core\\McpAdapter' )` — matches the namespace already used by `register_mcp_server()` elsewhere in the same file. No behavior change to the secondary detection branches.

### Scope

- Two files changed: `includes/class-abilities.php`, `includes/class-mcp-bridge.php`.
- Agent layer behavior on WP < 7.0 sites: unchanged (function_exists guard at `class-abilities.php::init()` bails early).
- Agent layer behavior on WP 7.0+ sites with `IMPRESS_AGENT_LAYER=true`: abilities now register correctly (27 abilities across 5 categories), MCP Adapter integration now detects the namespaced class on the first check.
- Sites without `IMPRESS_AGENT_LAYER` defined: zero behavior change (agent classes are not loaded).

### Credits

Bug surfaced by Aims during the v5.0.4 agent-layer canary test on servicetechohio.com, 2026-05-24. Canary did exactly what canaries are for — exposed an architecture mismatch before fleet-wide enablement. WP 7.0 hook names verified via direct source grep on STO's `wp-includes/abilities-api/` rather than guessed from documentation.

### Anti-pattern lesson for Dev Skill

Add to the IM-Press Dev Skill: **when integrating with a WordPress API that uses `_doing_it_wrong` enforcement, never assume the hook your code currently registers on is the right one — verify against core source.** Functions guarded by `_doing_it_wrong` will return null silently in production (the notice goes to debug.log, not the response), so misbehavior looks like "feature not working" rather than a crash. Always grep `wp-includes/` for `doing_action()` calls inside the API functions you're calling.

---

## [5.0.4] — 2026-05-24 — **Patch — PHPMailer require fix for `/imp-email/v1/test-smtp`**

### Fixed

#### `class-email.php::test_smtp_connection()` — fatal on diagnostic endpoint
Calling `GET /wp-json/imp-email/v1/test-smtp` threw:

```
PHP Fatal error: Uncaught Error: Class "PHPMailer\PHPMailer\SMTP" not found
in includes/class-email.php on line 144
```

Root cause: the method tried to load PHPMailer via the pre-WP-5.5 monolith path:

```php
require_once ABSPATH . WPINC . '/class-phpmailer.php';
```

WP 5.5 (Aug 2020) removed that file and split PHPMailer into three separate class files under `wp-includes/PHPMailer/`. The require call silently failed (file not found) but the immediately-following `new \PHPMailer\PHPMailer\PHPMailer( true )` succeeded because the PHPMailer base class had already been autoloaded elsewhere. The fatal only fired later inside `smtpConnect()`, which internally instantiates `\PHPMailer\PHPMailer\SMTP` — that class was never loaded, hence the missing-class fatal.

**Patch:** replaced the single-file require with a guarded triple-require of the canonical WP 5.5+ paths:

```php
if ( ! class_exists( '\PHPMailer\PHPMailer\PHPMailer' ) ) {
    require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
    require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
    require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
}
```

The `class_exists` guard prevents re-loading if `wp_mail()` already primed PHPMailer earlier in the request.

### Scope

- **One file changed:** `includes/class-email.php` (3 lines removed, 9 lines added including comment).
- **No behavior change on send paths** — `wp_mail()`-based delivery (the actual user-facing email path) was unaffected and continues to work as before. Only the `/imp-email/v1/test-smtp` diagnostic endpoint was broken.
- **No other instances** of the deprecated require pattern exist elsewhere in the codebase (verified by grep sweep).

### Credits

Bug surfaced by an AI agent (Aims-affiliated agent) during the SMTP-wiring task on airobotbuilds.com, 2026-05-24. The v2 diagnostic routes (`/imp-email/v2/test-smtp`) and the `/admin/test` endpoint were unaffected and confirmed working — clean diagnostic isolated the broken route precisely.

### Anti-pattern lesson for Dev Skill

Add to the IM-Press Dev Skill: **never `require_once` `class-phpmailer.php`** — that file does not exist in any supported WP version. When you need PHPMailer in a context where `wp_mail()` hasn't already loaded it (e.g., inside a custom SMTP connection test), always load the three split files explicitly (`PHPMailer.php`, `SMTP.php`, `Exception.php`) behind a `class_exists` guard.

---

## [5.0.3] — 2026-05-10 — **Patch — Build out orphan-enqueue feature implementations**

Latent issue surfaced during airobotbuilds.com migration debug: `class-popup-builder.php`, `class-theme-layer.php`, and `class-plugin.php`/`class-renderer.php` were enqueueing CSS/JS assets that were never built (`assets/css/popup.css`, `assets/css/theme-layer.css`, `assets/js/popup.js`, `assets/js/nav-toggle.js`, `build/style.css`). Plus an asset directory permission bug (`assets/` was `700`, blocking Apache from reading any file in it). Together these produced 5–7 console errors per page load fleet-wide on every IM-Press v5.0.x install.

### Built (real implementations, not stubs)

#### `assets/css/popup.css` (~140 lines)
Frontend popup builder styles — wraps the markup `class-popup-builder.php` emits via `render_popup_html()`. Implements:
- 8 position variants: center, top, bottom, top-left/right, bottom-left/right
- 6 entrance animations: fadeIn, slideUp, slideDown, zoomIn, slideRight, slideLeft (via `data-animation` attribute on `.imp-popup-wrapper`)
- Close-button styling with hover/focus states + keyboard outline preservation
- Mobile responsive — corner popups become full-width below 600px
- Closing animation (`.imp-popup-closing` → fadeOut)
- Body scroll prevention while popup open via `body.imp-popup-open` class

#### `assets/js/popup.js` (~190 lines)
Vanilla JS popup behavior — reads configuration from `data-*` attributes, schedules trigger, manages frequency caps, tracks events. Supports:
- 5 trigger types: `immediate`, `delay` (seconds), `scroll` (% threshold), `exit-intent` (mouseleave from top), `click` (CSS selector)
- 4 frequency caps: `always`, `once-session` (sessionStorage), `once-day` (localStorage + 24h ts), `once-ever` (localStorage)
- Event tracking via existing AJAX endpoint (`window.impPopupData.ajax_url`): impression on show, close on dismiss, conversion on link/button click in popup body
- Keyboard accessibility: Escape closes popup
- Overlay click closes popup
- Fail-silent on storage unavailability (private browsing) and invalid CSS selectors
- No external dependencies

#### `assets/css/theme-layer.css` (~130 lines)
Layout/structure baseline for header/nav/footer rendered by `class-theme-layer.php`. Per-site brand colors stay generated inline by the PHP class (specificity wins). This file provides:
- Sticky header support (`.impress-header-sticky` with backdrop-filter blur)
- Flexbox nav bar with brand + menu + CTA layout
- Mobile breakpoint (≤768px): nav menu collapses, mobile toggle button shows
- Footer 3-column grid with widget styling, mobile collapses to single column
- Footer-bottom row with subtle border separator

#### `assets/js/nav-toggle.js` (~80 lines)
Mobile nav toggle for `.impress-nav-bar`. Auto-injects a toggle button if the rendered nav doesn't include one (so themes that skipped the button in their template still get mobile nav). Behaviors:
- Toggle `.is-open` class on nav, swap button glyph (☰ ↔ ✕)
- Update `aria-expanded` for accessibility
- Close on outside click
- Close on Escape key
- Close when viewport widens past mobile breakpoint (matchMedia)

### Fixed

- **`class-plugin.php` line 184-191** — added `file_exists()` + `filesize() > 0` guard around the frontend block style enqueue (`build/style.css`). Previously enqueued unconditionally → 404 if build artifact missing. The other build/style.css enqueue paths (in `enqueue_editor_assets()` and `class-renderer.php`) already had this guard.
- **`assets/` directory permissions** — was `700` on the source dir (Mac filesystem default), inherited via `cp -R` during deploy. Apache on Hostgator runs as a different uid → can't read the dir → 403 on every file inside (even ones that exist). Fixed source dir to `755`; v5.0.3 ZIP ships with correct perms.
- **`build/style.css` orphan enqueue** — file was a webpack build artifact that never got produced. Stub file ships in v5.0.3 (real build deferred to a separate npm-build pass).

### Why it matters
These features were scaffolded in PHP but never finished on the frontend side. Sites had latent broken popup builder + nav functionality without anyone noticing because no one had used those features yet. v5.0.3 makes the popup builder + mobile nav + theme layer actually work end-to-end on every install.

### No breaking changes
Sites without popups in `wp-content` see no behavior change (`output_popups()` runs but emits nothing). Sites without `.impress-nav-bar` in DOM see no JS activity. Sites without sticky headers see no theme-layer.css activity. All-additive.

---

## [5.0.2] — 2026-05-09 — **Patch — Deconstructor render correctness + head-asset preservation**

Driven by Aims's Phase 1 migration report on `airobotbuilds.com` (the first real-world stress test of the deconstructor on non-skill-tagged content from a different author). Five concrete bugs were caught at the canary stage before bulk migration scaled them. Every fix in this release maps to a `failures:` line in that report.

### Fixed
- **Imported blocks rendered invisibly** — root cause: `classify_features()` and `classify_testimonials()` emitted attribute names (`features`, `testimonials`) that didn't match the block schema's canonical attribute names (`items`). Render callbacks saw empty arrays via `wp_parse_args` defaults and returned `''`. Now both classifiers emit canonical `items` matching the schema.
- **`classify_footer()` underextracted** — previously emitted only `copyright`. Footer block schema accepts `columns` (with link lists per column) + `socialLinks` + `logo` + `copyright`. Now the classifier:
  - Detects column structure via two strategies (Pattern A: nested div with heading + ul; Pattern B: heading-followed-by-sibling-ul fallback)
  - Extracts social links via known social-domain href patterns (Facebook, Twitter/X, LinkedIn, Instagram, YouTube, GitHub, TikTok)
  - Extracts logo from first `<img>` in footer
  - Detects copyright via © symbol or "copyright"/"all rights reserved" text, falls back to last `<p>`
- **Deprecated namespace emission** — classifiers hardcoded `'imp/X'` block names. Now emit canonical `'im-press/X'` per Skill v0.1 spec. `Block_Namespace_Aliases` already provides the alias mirror, so render path unchanged.
- **`<head>` content silently dropped** — root cause: `deconstruct_html_to_blocks()` extracted only `<body>`, never inspecting `<head>`. Source pages lost CSS (`<link rel="stylesheet">`), JSON-LD schema (`<script type="application/ld+json">`), OG/Twitter meta, etc.
  - Now adds a head-extraction pass before body deconstruction
  - Filters: preserves `<link rel="stylesheet|preconnect|dns-prefetch|prefetch|preload|icon|apple-touch-icon">`, `<style>`, `<script type="application/ld+json|application/json">`, `<meta property|name="og:*|twitter:*|description|keywords|robots|author|theme-color">`
  - Skips: charset/viewport meta (WP injects its own), executable JS (analytics/trackers — user re-adds deliberately), arbitrary http-equiv
  - Emits preserved content as a `core/html` block at the **top** of page output with marker comment: `<!-- imp:preserved-head-assets — review or remove if not needed -->`
  - Bytes-preserving by design — visual fidelity restored, user can edit/remove later
- **Report payload** — `deconstruct_html_to_blocks()` return now includes `preserved_head_assets: bool` flag for diagnostic visibility.

### Why it matters (product framing)
The deconstructor is the IM-Press AI-Site Adoption product surface. v5.0.2 is the first release where it's been validated against real-world AI-generated content from a different author (Aims's airobotbuilds Phase 1 telemetry → fix → re-validate loop). The "every miss is data" feedback flow is now operational.

### No new features. No public API changes.
v5.0.1 → v5.0.2 is pure bug fix. Deconstructor output now matches block schema expectations; head assets preserved as `core/html` rather than dropped. Existing fleet content unaffected.

---

## [5.0.1] — 2026-05-05 — **Patch — Bookings_Legacy schema self-heal**

Sites that originally installed the standalone `imp-bookings` plugin (now merged into IM-Press core as Bookings_Legacy) carry assorted versions of the `imp_blocked_dates` table. Some have a `service_id` column for per-service date blocks; older installs only have `blocked_date` + `reason`. The legacy availability code unconditionally assumed `service_id` existed, producing `Unknown column 'service_id' in 'where clause'` on every availability lookup on legacy sites. The error was swallowed by the API layer (so the booking widget kept working), but it polluted error logs and meant per-service blocked dates couldn't be filtered correctly.

### Fixed
- **Schema self-heal in `Bookings_Legacy`** — new `maybe_upgrade_schema()` method runs on `init` priority 20, gated by `imp_bookings_legacy_schema_version` option (single get_option lookup after first run). On version mismatch:
  - Creates `imp_blocked_dates` from canonical schema if missing entirely (`dbDelta`)
  - On existing tables, runs `INFORMATION_SCHEMA.COLUMNS` checks and `ALTER TABLE ADD COLUMN` for any missing columns (`service_id`, `reason`, `created_at`)
  - Idempotent — runs at most once per `SCHEMA_VERSION` per site
- **Defensive queries in `api_availability` / `check_availability` / `admin_block_date` / `admin_unblock_date`** — every query that touches `service_id` now calls `column_exists()` first and falls back to a service-id-agnostic query if the column isn't present. Belt-and-suspenders for the very-first-pageload-after-upgrade window before `init` has a chance to fire.

### Why it matters (product framing)
This is the IM-Press way: **detect drift, self-heal, fail-safe**. Plugins that ship across many sites with different histories will always have schema drift. The right answer isn't "manually run migrations on every site" — it's "the plugin notices and fixes itself on its own next page load." This pattern is the template for future schema migrations across all IM-Press tables.

### No new features. No behavior changes for sites already on the canonical schema.
v5.0.0 → v5.0.1 is a pure fix: error log goes silent on availability calls for legacy schemas, per-service blocked-date filtering starts working again where it had been silently degraded.

---

## [5.0.0] — 2026-05-01 — **Foundation — Reliability & AI-Friendliness**

The first release explicitly scoped to *make IM-Press a more reliable tool for AI to build beautiful WordPress websites that users can edit*. Born from John's reframe in the v4.7.x → v5.0.0 conversation: stop optimizing for release-cadence economics, optimize for the four product dimensions — **reliability, AI usefulness, user usefulness, UI polish**.

> **Product north star, locked:** "🛠️ AI builds beautiful sites → 👤 users edit them."

### Cleanup foundations (foundation work that unlocks all future cleanup safely)

#### Added — `Blocks_Attr_Migration` helper class (`includes/class-blocks-attr-migration.php`)
Render-time, **additive** translation of legacy attribute names to canonical names. When `render_block_data` fires for any `imp/*` or `im-press/*` block, the helper:
- Reads block-specific + universal alias rules
- For each rule where canonical isn't set but legacy is, copies legacy → canonical
- **Preserves both names** (additive, not destructive) — render callbacks reading legacy *or* canonical attrs both work
- Type-aware via optional `condition` callbacks (`icon: bool → showIcon`, `icon: string → iconName`, `columns: array → items` while scalar `columns` stays integer)
- Filterable via `imp_blocks_attr_migration_rules`

This unblocks per-block render renames as a *gradual* cleanup track instead of a big-bang rewrite. Legacy stored markup keeps rendering identically; new canonical-named markup also works; both can coexist indefinitely.

#### Added — `Block_Namespace_Aliases` (`includes/class-block-namespace-aliases.php`)
On `init` priority 30, walks the WP block registry and registers an `im-press/*` mirror for every `imp/*` block found, sharing the same render_callback and attribute schema. Net effect:
- AI/agent markup using `im-press/heading` works identically to `imp/heading`
- Brand-correct CSS classes (`wp-block-im-press-{name}`) emit for new content
- Old fleet markup with `imp/*` keeps rendering unchanged (no break)
- Idempotent — running the alias pass twice doesn't double-register
- Zero per-block code churn (no rename of 100+ `register_block_type` calls needed)

This ships the brand + SEO + AI-training-data continuity wins without any of the breakage risk of a renamed canonical.

#### Added — Schema consistency audit + decisions (`docs/research/block-schema-consistency-audit.md`)
Programmatic catalogue of all 159 registered blocks across the 4 registries, 731 attribute declarations. Found:
- 8 concept-naming inconsistencies (heading/title/text/label/headline split across 33 blocks; bgColor/backgroundColor split across 20; etc.)
- 26 select-style attrs missing `enum` arrays (silent failure path for AI)
- 6 type conflicts (same attr name with different types across blocks: `icon`, `columns`, `separator`, etc.)
- 37 attrs missing `type`, 39 missing `default`

Canonical names locked: `heading`, `description`, `backgroundColor`, `textColor`, `imageUrl`, `imageId`, `iconName`, `showIcon`, `iconColor`, `linkUrl`, `linkTarget`, `linkRel`, `level`, `items`, `columns`, `enableSeparator`, `separatorStyle`. Type-conflicts resolved by name-splits (`icon: bool → showIcon`, `columns: array → items`, etc.).

Raw catalog persisted at `docs/research/data/block-catalog.json` + `block-schema-audit-findings.json` for re-running future-version sanity checks.

#### Sample blocks deepened to canonical names — `imp/heading` + `imp/button`
- `imp/heading`: `text → heading` (declared as canonical in schema; legacy `text` retained for editor back-compat)
- `imp/button`: `text/url/target → heading/linkUrl/linkTarget` with resolver pattern at top of render_callback
- Both verified with end-to-end identity tests (legacy attrs ≡ canonical attrs ≡ migration-pipeline output) — byte-identical HTML across all paths

Remaining ~28 attribute renames deferred to v5.0.x patches — additive migration helper means each one ships as silent cleanup.

### AI-reliability pivot (the actual product wedge)

#### Added — IM-Press Skill v0.1 spec (`docs/SKILL.md`)
**The first published vendor-agnostic AI authoring spec for a WordPress page builder.** Tells any AI agent (Claude / ChatGPT / Gemini / custom OpenClaw) which canonical block names + attribute names to use, plus the block catalogue mapping content intent → block. Versioned alongside IM-Press releases.

#### Added — 3 REST endpoints under `im-press/v1/`

| Endpoint | Purpose |
|---|---|
| `GET /skill` | Structured Skill JSON (canonical attrs map, cross-cutting attr docs, 12-category block catalogue with 59 blocks, common patterns, fallback rules) — for programmatic AI consumption at session start |
| `GET /skill-prompt` | Paste-ready ~3.5KB system-prompt addendum (vendor-agnostic) — primes any LLM in one prompt |
| `GET /blocks/reference` | Live block catalogue dump from `WP_Block_Type_Registry` — actual schemas, types, defaults, enums for the blocks registered on THIS site (per-site licence gating reflected) |

All public endpoints (no auth) — Skill is meant to be openly fetchable by any AI agent without credentials. Cache-Control: 1 hour. Filterable via `imp_skill_payload` / `imp_skill_prompt`.

The Skill prompt is the headline AI artifact. Drops into any LLM's system prompt, primes it on canonical names + block selection rules + cross-cutting attrs + fallback patterns. ~600 words, version-stamped, vendor-agnostic.

### Mobile reliability layer

#### Added — `Block_Mobile_Safety` (`includes/class-block-mobile-safety.php`)
Defensive `<style>` injected at `wp_head` priority 1 (before any block-specific CSS). Eliminates the entire class of mobile bugs that AI-generated pages hit:

- **Universal viewport overflow guard** — `html, body { overflow-x: hidden; max-width: 100vw }`. AI emitting `width: 100vw` inside a padded parent can no longer push the page wider than viewport.
- **Universal media defensiveness** — images / videos / iframes / canvas / svg / picture / table / pre / code never overflow on mobile. `box-sizing: border-box` everywhere.
- **Fixed-nav clearance** — `:has()` selector detects fixed/sticky navs (filterable list — defaults include `.imp-fixed-nav`, `.imp-sticky-nav`, `.imp-nav`, `.ledw-nav`, plus inline-style position-fixed pattern). Auto-pads body and emits `--imp-nav-height` CSS variable. `scroll-padding-top: var(--imp-nav-height)` so anchor navigation never lands behind nav.
- **Mobile-only safeguards** (`@media (max-width: 767px)`) — kills `min-width: Npx` constraints that block grid items from shrinking; auto-centres uncentered `max-width: Npx` boxes (the right-margin asymmetry symptom); hides decorative `.bg-orb` / `[class*="orb"]` / `.imp-bg-decoration` absolute elements that escape viewport on small screens.

12/12 defensive rules verified emitting in render-time test. ~2.4 KB inline CSS. Filterable via `imp_mobile_safety_*` filters (master switch, nav-height values, nav selectors, decoration selectors, full CSS string).

This module would have prevented the ohioledwall.com hero-hidden-by-nav bug we patched in v4.7.x without a per-page hotfix. From v5.0.0 forward, the same class of bug self-fixes on every site.

### Hotfix shipped during v5.0.0 build

#### Fixed — ohioledwall.com mobile nav clearance
While building v5.0.0, the recurring "nav covers hero on mobile" symptom surfaced. Patched via `imp_global_css` option (Custom Code Injection module) — appended marker-tagged CSS that respects Aims's existing column-layout mobile nav redesign while bumping hero `padding-top` to clear the actual 222px-tall mobile nav. Idempotent (won't re-apply on subsequent runs). Visually verified at 375×812 viewport via Playwright.

The same class of bug is now defensively prevented across the fleet by `Block_Mobile_Safety`.

### Files added/modified

| Type | Files |
|---|---|
| New PHP classes | `class-blocks-attr-migration.php`, `class-block-namespace-aliases.php`, `class-skill.php`, `class-block-mobile-safety.php` |
| New docs | `docs/SKILL.md` (16 KB human-readable spec), `docs/research/block-schema-consistency-audit.md`, `docs/research/data/block-catalog.json`, `docs/research/data/block-schema-audit-findings.json` |
| Modified PHP | `im-press.php` (4 new requires + 4 new init calls; version 4.7.0 → 5.0.0), `class-blocks-v2.php` (`imp/heading` + `imp/button` schema + render canonicalised) |
| New REST namespaces | `im-press/v1/skill`, `im-press/v1/skill-prompt`, `im-press/v1/blocks/reference` |

### Back-compat
Every existing fleet page renders identically to v4.7.0. Migration helper is additive (preserves legacy attrs alongside canonical). Namespace aliases are additive (preserves `imp/*` registrations alongside `im-press/*`). Block_Mobile_Safety is filterable to off if it conflicts with bespoke design. Zero breaking changes to deployed sites.

### What's deferred to v5.0.x patches

- Remaining ~28 attribute renames per render_callback (silent cleanup; migration helper handles user-visible behaviour)
- 26 select-style attrs missing `enum` arrays
- 37 attrs missing `type` / 39 missing `default`
- Validation feedback API endpoint (`POST /imp-deploy/v1/validate-page`)
- Error message rewrites for top 10 REST endpoints
- Per-block `migrate_attributes()` convention for evolving schemas
- Eventual swap of canonical to `im-press/*` in registration files (drop `imp/*` registrations after fleet migration script runs)

### Next major release direction (tracked, not committed)
v5.1.0 — Site Theme Mode (global header/footer template with conditional display rules) — the feature John originally requested. Foundation for "AI builds full sites" (not just one-off pages).

---

## [4.7.0] — 2026-04-29 — **Depth — BB+UABB Capability Parity**

The first feature-parity release. Closes the depth gap surfaced by the [BB+UABB Parity Audit](docs/research/bb-uabb-parity-audit.md) — 6,168 BB module instances analyzed across the 4-hitech.com production database showed IM-Press blocks were 3-10× shallower than their BB counterparts in the dimensions BB users actually configure in production. v4.7.0 closes that gap on the highest-frequency 8 blocks plus the cross-cutting infrastructure that lifts every block.

> Product north star: **Build a tool that helps AI build beautiful WP websites and lets users tweak the data — SEO optimized, user-friendly, agent-friendly.**

### Track A — Cross-cutting infrastructure (every block benefits)

#### Added — Universal Effects & Animation Panel (`assets/js/effects-panel.js` + `class-effects-panel.php`)
Surfaces in the Gutenberg sidebar of **every `imp/*` and `im-press/*` block**:
- ✨ **Effects & Animation panel** — 24 entrance animations, 12 hover effects, delay + duration + offset + trigger-once controls
- 🎨 **Hover State panel** (visual blocks) — color, background, border-color, opacity, scale, transition-duration on hover. Pure CSS `:hover` selectors — no JS.
- 🔗 **Link Controls panel** (link-bearing blocks) — `linkTarget` (`_self` / `_blank` / `_parent` / `_top`) + `linkRel` (None / nofollow / sponsored / ugc / combinations). WP core handles `noopener noreferrer` automatically on `_blank`.
- 📱 **Responsive panel** (text-bearing blocks) — mobile alignment override (`alignmentMobile`)
- ⚙️ **Advanced Effects panel** — parallax, scroll-transform, text animation, stagger for repeating children

The animation/hover/parallax/scroll attrs were already added to every IM-Press block by `Block_Animations` since v3+ but had no editor UI — the system worked only via REST/block-markup. v4.7.0 adds the UI so human editors can toggle effects from the sidebar without writing block markup.

#### Added — `Block_Link_Controls` trait
Auto-adds `linkTarget` + `linkRel` to 19 link-bearing blocks via `register_block_type_args`. Render-side hook patches the first `<a>` tag with the configured target/rel only if the block's render_callback didn't already set them. Filterable list via `imp_block_link_controls_blocks`.

#### Added — `Block_Hover_State` trait
`hoverColor`, `hoverBackground`, `hoverBorderColor`, `hoverOpacity`, `hoverScale`, `hoverTransitionDuration` on 21 visual blocks. Generates a scoped class on the block's first wrapper element + injects `<style>` with `.class:hover { … }` rules. Deterministic class names so identical configs share CSS.

#### Added — `Block_Responsive_Alignment` trait
`alignmentMobile` on 22 text-bearing blocks. Emits `@media (max-width: 767px) { .class { text-align: …; } }` so mobile alignment can differ from desktop — closes the BB+UABB `r_alignment` / `r_custom_alignment` gap which was populated on **100% of headings, buttons, and icons** in production BB pages.

#### Added — `Blocks_Helpers::fluid_font_size()` (`includes/class-blocks-helpers.php`)
Converts a CSS size value (`32px`, `2rem`, `1.5em`) to a `clamp(min, vw-scaled, max)` expression that scales smoothly between 320px and 1280px viewport widths. `clamp()` / `calc()` / `var()` / unsupported-unit inputs pass through unchanged. Adopt-as-you-touch-it — Track B blocks opt in via a `fluidTypography` attribute (off by default to keep existing pages identical).

#### Fixed — `Block_Animations` no longer overwrites block-declared attrs
Pre-v4.7.0, `Block_Animations::add_animation_attributes()` blindly overwrote 18 attribute schemas on every IM-Press block, clobbering narrowly-typed enums like `imp/callout`'s `hoverEffect: ['none','shadow','scale','border']`. Now the filter only adds attrs the block hasn't already declared — same defensive pattern Block_Link_Controls uses.

#### Fixed — `Block_Animations` namespace expanded to `im-press/*`
Pre-v4.7.0 only `imp/*` blocks got animation rendering. v1 blocks (`im-press/*` namespace) silently bypassed the system. Now both prefixes match.

#### Added — Click-action runtime (`assets/js/click-actions.js` + `class-click-actions.php`)
Vanilla-JS frontend handler for `data-imp-click` attributes emitted by deepened blocks (notably `imp/button`'s `clickAction` variants):
- `data-imp-click="copy"` → Clipboard API copy with `execCommand` fallback
- `data-imp-click="lightbox"` → Self-rendering overlay with YouTube (privacy-mode `youtube-nocookie.com`), Vimeo, generic-iframe, or inline-element content. ESC + overlay-click + close-button dismiss. Body-scroll lock.

Zero dependencies. Frontend-only.

### Track B — Per-block depth (8 blocks deepened)

Each deepening adds the missing settings BB+UABB users actually configured in production while keeping every pre-v4.7.0 default visually identical. SSR_Editor's auto-generated InspectorControls picks up the new `enum`-tagged attributes and renders SelectControls / ToggleControls / RangeControls — no per-block JS edit components needed.

| Block | Attrs before → after | Highlights |
|---|---:|---|
| `imp/info-box` | 6 → 25 | `mediaType` icon/image toggle, `mediaPosition`, `enableSeparator`, `titleTag` h2-h6, `verticalAlign`, `linkScope` whole_box/title/icon |
| `imp/callout` | 12 → 32 | `titleTag`, `iconSize`, `iconColor`, `verticalAlign`, separator, `ctaType` button/link/none, `buttonStyle` solid/outline/ghost. Removed dangerous inline `onmouseover`. |
| `imp/heading` | 8 → 24 | `linkUrl` + target/rel, subheading + position + typography, separator with above_heading/between/below_subheading positions, `fluidTypography` opt-in. **Bare-heading markup unchanged from v4.6.5.** |
| `imp/button` | 11 → 24 | `clickAction` enum: `link` / `lightbox_video` / `lightbox_iframe` / `lightbox_inline` / `email` (auto-mailto:) / `phone` (auto-tel:) / `sms` / `scroll_to_anchor` / `copy_to_clipboard`. `customWidth`, `customHeight`, `iconAnimation`, `transitionEffect`. |
| `imp/numbers` | 15 → 27 | `numberType` enum: `digits` (default) / `circle` (SVG progress) / `bar` / `pie` (conic-gradient). `targetValue`, `circleSize`, `barHeight`, `triggerOnView`. Singleton runtime per page. **Legacy `circleBar:true` still maps to `numberType:circle`.** |
| `imp/photo` | 13 → 28 | **Removed dangerous inline `onmouseover` / `onmouseout`** — replaced with scoped class + `<style>` + `:hover`. `photoSource` media_library/external_url/unsplash. Image filters (`opacity`, `grayscale`, `imageBrightness` + hover variants). Overlay caption-on-hover with 5 reveal styles. |
| `imp/modal` | 5 → 30 | `contentType` enum: `inline` / `video` (YouTube/Vimeo auto-detect, privacy-mode embed) / `iframe` / `image` / `shortcode` (`do_shortcode()`) / `page_id` (loads WP page content). Trigger types: `click` / `page_load` / `time_on_page` / `scroll_depth` / `exit_intent`. `showOncePerSession` via sessionStorage. 5 modal sizes. |
| `imp/divider` | 5 → 12 | `alignment` left/center/right, `opacity`, glyph mode (icon/text between flanking lines), `double` and `zigzag` (inline-SVG) styles. |

### Track C — New blocks (closing genuine BB+UABB gaps)

#### Added — `imp/image-hover` (UABB ihover replacement)
CSS-driven photo overlay block with caption-on-hover. **Zero JS.** 9 reveal effects: `fade` / `slide_up` / `slide_down` / `slide_left` / `slide_right` / `zoom_in` / `zoom_out` / `rotate` / `curtain` / `flip`. 6 aspect ratios. Title + description with independent typography. Optional whole-block link wrap.

#### Added — `imp/audio` (HTML5 audio block)
Native `<audio>` with cover art + metadata. Three visual themes:
- **minimal** — bare audio control
- **card** — cover image + title + artist + control in a light card
- **banner** — gradient hero (accentColor → backgroundColor) with cover + meta + control

Standard HTML5 attrs: controls / autoplay / loop / muted / preload.

### Documentation

- **Added** [`docs/research/bb-uabb-parity-audit.md`](docs/research/bb-uabb-parity-audit.md) — full audit methodology, BB module frequency ranking, capability deltas per block, scope rationale for v4.7.0 / v4.8.0+
- **Added** [`docs/research/v470-plan.md`](docs/research/v470-plan.md) — implementation plan with attr-name-by-attr-name spec for every deepening
- **Added** [`docs/research/data/*.json`](docs/research/data/) — raw audit data: `bb-module-frequency.json`, `bb-module-settings-samples.json`, `imp-block-attrs.json`, `bb-vs-imp-depth-audit.json` for future re-runs

### Files

| Type | Files |
|---|---|
| New PHP classes | `class-blocks-helpers.php`, `class-block-link-controls.php`, `class-block-hover-state.php`, `class-block-responsive-alignment.php`, `class-effects-panel.php`, `class-click-actions.php` |
| New JS | `assets/js/effects-panel.js` (~600 lines), `assets/js/click-actions.js` (~200 lines) |
| Modified | `im-press.php` (bootstrap wiring + version bump), `class-block-animations.php`, `class-blocks-v2.php` (heading / button / numbers / photo / modal / divider deepenings), `class-blocks-extra.php` (callout / numbers / photo deepenings, image-hover + audio new blocks) |
| Docs | `CHANGELOG.md`, `docs/research/bb-uabb-parity-audit.md`, `docs/research/v470-plan.md`, `docs/research/data/*.json` |

### Back-compat
Every deepened block was smoke-tested with both pre-v4.7.0 default-only attributes (must render byte-equivalent to v4.6.5) and post-v4.7.0 new-feature attributes. **Zero breaking changes.** Existing pages render identically until a user/agent opts in to a new attribute.

### Anti-patterns avoided
- All `register_rest_route()` calls inside `rest_api_init` callbacks ✓
- No `__CLASS__` at file scope ✓
- No `{$self::$prop}` in double-quoted strings ✓
- No `wp_update_post()` on inline `<style>` content ✓
- No JS arrow functions in WP-injected `<script>` blocks ✓
- All hook callbacks public ✓

### What's next
- **v4.8.0 — Site Theme Mode**: global header/footer template (where the nav bar lives), Site Config toggle, per-page override for one-off landing pages. Closes the recurring "every page has its own copy of the nav and they drift out of sync" pain.

---

## [4.6.5] — 2026-04-28 — **Durable Contact Submissions (Lead-Loss Fix)**

Closes a real production data-loss bug surfaced by Aims during Ohio LED Wall testing: contact forms returned `success` based on `wp_mail()`'s boolean return, but if email delivery silently failed (transient SMTP issue, recipient bounce, ModSec block, anything) the lead vanished — no DB row, no admin inbox, no recovery. Per-site issue, fleet-wide risk.

### Fixed — `/imp-email/v1/contact` is now lead-safe
**Save first, email second.** Every valid submission writes a row to `{$prefix}imp_contact_submissions` BEFORE `wp_mail()` is even attempted. Email failure no longer means lead loss.

Response shape now matches Aims's spec:
```json
{
  "success": true,
  "saved": true,
  "submission_id": 123,
  "email_sent": true,
  "message": "Message received! We'll get back to you within 24 hours."
}
```

If email fails after save:
```json
{
  "success": true,
  "saved": true,
  "submission_id": 123,
  "email_sent": false,
  "message": "Message received! We'll get back to you within 24 hours."
}
```

**Frontend message stays user-friendly regardless** — visitors see "Message received!" whether email worked or not, because the lead is durably captured. Admin sees the failure in the inbox and can resend.

### Added — Durable storage table
`{$prefix}imp_contact_submissions` schema (per Aims's spec — 18 columns):
- `id`, `site_url`, `source_url`, `referrer_url`
- `name`, `email`, `phone`, `subject`, `message`, `raw_payload` (JSON of full POST)
- `ip_address` (proxy-aware via `HTTP_CF_CONNECTING_IP` / `X-Forwarded-For` / `X-Real-IP`), `user_agent`
- `status` (default `new`), `email_sent` (1/0/null), `email_error`, `email_attempted_at`
- `created_at`, `read_at`, `archived_at`
- Indexes on `status`, `email_sent`, `created_at`, `email`

`Email::VERSION` bumped 2.0.0 → 2.1.0; `dbDelta` adds the table on next plugin load.

### Added — `wp_mail_failed` hook integration
When `wp_mail()` fails inside `handle_contact()` (or `resend_submission()`), the `wp_mail_failed` action fires synchronously with a `WP_Error`. We pin the failure to the active submission via a private `$current_submission_id` property and write the error message + data to the row's `email_error` column. Single-request-scoped (WP doesn't run concurrent requests in one process — no locking needed).

### Added — REST endpoints
- `GET /wp-json/imp-email/v1/contact-submissions` — list w/ status filter (`all`/`unread`/`email_failed`/`archived`) + per-status counts
- `GET /wp-json/imp-email/v1/contact-submissions/get?id=N` — single submission
- `POST /wp-json/imp-email/v1/contact-submissions/action` — single endpoint, `{ id, action }` body, supports `resend` / `mark-read` / `mark-unread` / `archive` / `unarchive` / `delete`. Single-endpoint pattern keeps URLs ModSec-safe (no POST to `/path/{numeric}`).
- `GET /wp-json/imp-email/v1/contact-diagnostics` — recipient, SMTP status, last successful send, last failure, totals

All admin-gated (`manage_options`).

### Added — `class-admin-contact-submissions.php` (~340 lines)
**WP Admin → IM-Press → Contact Submissions.**
- Diagnostics panel at top: recipient, SMTP status, last successful/failed send (relative time), totals
- Filter tabs: All / Unread / Email failed (red highlighted) / Archived with live counts
- Sortable table: date, name (• marker for unread), email, subject, email status pill (Sent / Failed / Pending), per-row View action
- Click row → drawer with full message, source URL, email status with `email_attempted_at`, error block when `email_error` populated, IP + UA
- Drawer actions: **Resend notification** (returns updated `email_sent`), Mark unread, Archive, Delete
- Auto-mark-read when drawer opens
- Auto-refresh every 30 s while tab visible (paused on `visibilitychange`)
- Mobile-friendly under 720 px
- Dark IM-Press admin theme matching Static Reviews + Prompt Studio

### Live verification on ohioledwall.com
- Test contact submission: `success:true`, `saved:true`, `submission_id:1`, `email_sent:true` ✓
- DB row inserted (verified via `/contact-submissions` list endpoint) ✓
- Diagnostics endpoint shows `last_successful_send` populated, no `last_failed_send` ✓
- Actions tested: mark-read ✓, resend ✓, delete ✓
- Email arrived at `you@example.com` (notification recipient) — actual delivery confirmed
- Auto-reply also fired to the test address (best-effort, doesn't gate success)

### Updated
- `uninstall.php` drops `imp_contact_submissions` when opt-in delete is enabled.

### Anti-patterns audit (clean)
- All hooks in `init()`. All REST routes inside `rest_api_init`. All callbacks public on instance.
- Action endpoint uses body params (no POST to `/path/{numeric}` — ModSec-safe).
- No `{$self::}`, no `IMPress_Constants::`, no JS arrow functions in inline JS.

---

## [4.6.4] — 2026-04-28 — **AI HTML → Native Blocks Deconstructor**

Completes the original IM-Press promise: AI generates HTML, IM-Press lifts it into editable WordPress blocks the human can polish visually. Built on the section-detection scaffolding hinted in `class-blocks-v2.php` but never wired up.

### Added — Deconstructor in `class-html-importer.php` (~570 lines)
Walks DOM with `DOMDocument`, classifies top-level body sections, emits real Gutenberg block markup (not just JSON in private meta).

Section heuristics (class-name + structural):
- `<header>` / `<nav>` → wp:html fallback (site-specific, no native imp/* equivalent)
- `<footer>` → `imp/footer`
- `.hero` / `.banner` / first section with H1 + button → `imp/hero`
- `.features` / 3+ sibling cards with title+description → `imp/features-grid`
- `.pricing` / pricing tiers → `imp/pricing-table`
- `.testimonial` / `.review` / `.quote` → `imp/testimonials`
- `.faq` / `.accordion` / `<details>` blocks → `imp/accordion-faq`
- `.cta` / `.call-to-action` → `imp/cta-section`
- Anything else → `<!-- wp:html -->...<!-- /wp:html -->` fallback (lossless — never lose content)

Each classification carries a confidence score (0-100) so dry-run reports can flag uncertain matches.

### Added — `mode=deconstruct` + `dry_run=true` on `/im-press/v1/import-html`
- `dry_run=true`: returns parser report (block list, attributes, confidence, fallback count, warnings) without writing to the DB. Perfect for safe parser testing.
- `dry_run=false`: writes the deconstructed block markup to a new (or specified existing) page. Stamps `_impress_deconstructed`, `_impress_deconstruction_date`, `_impress_deconstruction_stats` post meta.

Output: real Gutenberg block comment-marker syntax — `<!-- wp:imp/hero {"heading":"…"} /-->` self-closing markers per [WP block grammar docs](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#blocks). Server `render_callback` produces the visible HTML at render time. Editor displays the block UI.

### Added — `POST /im-press/v1/migrate-bb-page`
Migrates an existing Beaver Builder page to IM-Press blocks **without modifying the original**.

- Reads target page, confirms `_fl_builder_data` is present
- Renders BB layout via `FLBuilder::render_content_by_id($post_id)` — verified against [BB's official displaying-content docs](https://docs.wpbeaverbuilder.com/beaver-builder/developer/tutorials-guides/displaying-beaver-builder-content/)
- Falls back to `wp_remote_get` of the published permalink if FLBuilder class isn't loaded
- Pipes rendered HTML through the same deconstructor (one parser, two input sources)
- Creates a NEW IM-Press page tagged `(IM-Press)` with `-impress` slug suffix, status `draft`
- Stamps `_impress_migrated_from_bb` post meta linking the new page to the BB original
- Supports `dry_run=true` for inspecting the parser output before writing

### Added — thejedai.com to deploy fleet
Was on v4.1.0 + free tier. Now Agency-licensed, on v4.6.4. Added to `deploy-impress-update.sh` + `im-press-smoke-test.sh`. Will be cleared and reset after BB-migration tests complete.

### Fleet count: 9 → 10 sites.

---

## [4.6.3] — 2026-04-28 — **Permalink Diagnostic + Passthrough Auto-Updater + Fleet Expansion**

Three additive operational improvements. None changes existing behavior.

### Added — Permalink diagnostic endpoint
`GET /wp-json/imp-health/v1/permalink-check` (public). Two modes:

- **`?page_id=N`** — verify N's expected permalink actually serves N. Returns `expected_url` + `expected_title` + an `ok` boolean indicating both HTTP 200 AND no WP fallback content AND title appears in rendered output.
- **`?url=https://…`** — verify any URL renders cleanly with no "Latest Posts" / "No posts found" / "404" sentinel.

Specifically prevents the "I sent you a link" / "I see Latest Posts" failure mode an AI agent + the site owner hit. Returns structured `warnings[]` + `checks{}` + a `hint` directing agents to `imp-preview/v1/static-review` when WP routing is unreliable.

Pairs with the v4.6.1 agent-smoke-test as the diagnostic toolkit's second member.

### Added — Passthrough theme auto-updater
**Operational infrastructure**, not user-facing. Until now every Passthrough theme update required SSH push to all sites manually. From v4.6.3 it flows through the standard WP themes-update mechanism.

- **New module:** `class-theme-updater.php`. Hooks `pre_set_site_transient_update_themes` to inject Passthrough updates into the standard "Update available" flow in WP admin → Themes. Same caching pattern as `class-updater.php` (12 h site transient). No-ops on sites without Passthrough installed.
- **New server endpoints** in `class-rest-api.php`:
  - `POST /wp-json/im-press/v1/theme-updates` — license-validated update check
  - `GET /wp-json/im-press/v1/theme-updates/info` — public info (no download URL)
- **Config** stored in the existing `im_press_update_server` WP option, new `theme` sub-section: `{ latest_version, download_url, changelog, requires_wp, tested_wp, requires_php }`. Seeded on airobotbuild.com pointing at the existing `passthrough.zip` upload.
- **License gating:** uses the same key list as the plugin updater. Customers with an IM-Press Pro/Agency license get the companion theme too — same key, same site allowance, no separate purchase.
- **`themes_api` filter** registers a "View details" popup so admins can see the changelog before updating.
- **`upgrader_process_complete`** clears the cache after a successful theme upgrade so admin doesn't keep showing "update available" for an already-applied version.

After this release, shipping a Passthrough update = upload the new ZIP + bump `theme.latest_version` in `im_press_update_server` on airobotbuild.com. Every site sees "update available" within 12 hours.

### Added — `hi-techsecuritysolutions.com` to deploy fleet
- Was on v4.3.1 (4 versions stale). Now on v4.6.3.
- Added to `~/deploy-impress-update.sh` (`SITE_PATHS`/`SITE_DOMAINS` arrays).
- Added to `~/im-press-smoke-test.sh` (`SITES` array).
- Verified Agency-licensed via the v4.6.1 capabilities endpoint.
- Fleet count: **8 → 9 sites**.

### Fixed
- **servicetechohio.com WP rewrite rules** — pre-existing missing `# BEGIN WordPress` block in `.htaccess` was causing `/wp-json/` to return 404 (Apache served a static "page doesn't exist" page instead of routing to `index.php`). Restored the standard WP rewrite block. Backup saved as `.htaccess.bak.{timestamp}`. Note: caused by an external page-cache plugin's overzealous rewriting; not v4.6.3-introduced.
- **Smoke test** now correctly handles arbitrary expected HTTP codes (e.g. `400` for endpoints that legitimately reject empty params), not just 200/401. Was producing false "warn" results for the new permalink-check endpoint.

### Live verification
- 9/9 sites on v4.6.3 health ping.
- Permalink diagnostic verified both modes (page_id ✓, custom URL ✓ — bad URL produces full warnings + hint).
- Theme update endpoints verified — invalid license gets metadata only, valid license (`IMP-AGY-XXXX-XXXX-XXXX`) returns `package` URL.
- `im_press_update_server` `theme` sub-section persisted to airobotbuild.com DB and confirmed flowing through to public info endpoint.
- Smoke test **351/351** across 9 sites (was 296 in v4.6.2 — added permalink + theme-info endpoints + the new site).

### Anti-patterns audit (clean)
All hooks in `init()`. All REST routes inside `rest_api_init`. All callbacks public static. No `{$self::}`, no `IMPress_Constants::`. All PHP built-ins prefixed (`\stdClass` in theme-updater).

### Still deferred (v4.6.4+)
- the agent's amendments **D** (versioned reviews `{slug}-v{n}.html`) and **E** (readability checklist).
- **Backend Media Review Queue** — designed in BACKLOG, not yet built.
- **OpenAPI ping endpoint** — tiny pre-check before fetching the full spec.
- **Lifted styles editor** in promotion meta box (CodeMirror panel — view shipped in v4.6.2, edit pending).

---

## [4.6.2] — 2026-04-28 — **Admin UI for v4.6.0 Features**

Polished WP-admin surfaces around the v4.6.0 endpoints so humans (the site owner) can use them without curl. REST endpoints stay authoritative — these admin pages are pure presentation.

### Added — `class-admin-preview-pages.php` (Static Reviews admin page)
WP Admin → IM-Press → **Static Reviews**.

- Hero header + "+ New Review" button
- Filter row: All / Active / Expired with live counts
- Sortable table: title, status pill (Active/Expiring/Expired), recommended URL (truncated, click to open), created (relative), expires (relative w/ warning when <24h)
- Per-row actions: **Copy** (clipboard), **View** (new tab), **Delete** (confirm modal)
- Empty state with onboarding CTA when no reviews exist
- **Create modal** — title, video URL, image URL, caption, style picker (Flat / 📱 Phone / Minimal with descriptions), watermark text (default "DRAFT PREVIEW — NOT FINAL"), expires-in-days. Submits via `POST /imp-preview/v1/static-review`.
- **Success modal** — surfaces the canonical `recommended_review_url` plus a `health` pill (✓ Self-verified / ⚠ N warnings) + HTTP status + size. Two action buttons: **Copy URL** + **Open in new tab**.
- **Delete confirmation** — shows the title, warns the link will 404 immediately
- Auto-refreshes the list every 30 s while the tab is in focus, pauses on `visibilitychange`
- Toast notifications for create/delete/copy
- Mobile-friendly: collapses to single-column under 720 px, larger tap targets
- Dark IM-Press admin theme matching Prompt Studio + Admin Dashboard V2

### Added — `class-admin-promotion.php` (Promotion meta box)
Right-sidebar meta box on every public-post-type edit screen. Only renders meaningful content for pages with `_impress_promoted` set; otherwise shows a quiet hint about how to promote.

For promoted pages:
- **🎨 Auto-promoted** status badge with relative timestamp ("Promoted 2h ago")
- **2×2 stat grid** — counts of lifted style blocks, stylesheets, head scripts, body scripts
- **View Source** button — opens read-only modal with the preserved original HTML in a monospace textarea + Copy-to-clipboard
- **Restore Original** button — confirms with a warning ("Any edits since promotion will be lost"), calls `POST /im-press/v1/import-html/restore`, reloads the editor on success
- Footer hint explaining the contract: body is editable in the main editor, lifted assets render via `wp_head`/`wp_footer`, restore rolls everything back

### Changed — `promotion-info` endpoint accepts `include_source=1`
`GET /im-press/v1/import-html/promotion-info?page_id=N&include_source=1` now returns the preserved blob in `source_html` (with `source_size_bytes`). Opt-in because the blob can be 50KB+. Drives the meta box's View Source modal. The endpoint is `edit_pages`-gated so blob exposure stays admin-only.

### Live verified on 4-hitech.info
- v4.6.2 health ping green on all 8 sites
- Admin page registers (returns expected 302 to login when unauth'd, which proves the page slug is correct)
- `promotion-info?include_source=1` returns the blob with `source_size_bytes` ✓
- `promotion-info` without the flag does NOT include the blob ✓ (default-safe)
- Smoke test 296/296

### Anti-patterns audit (clean)
All hooks in `init()`, all REST routes inside `rest_api_init`, all callbacks public static, no `{$self::}`, no `IMPress_Constants::`, no JS arrow functions in inline JS.

### Still deferred (v4.6.3+)
- Lifted styles editor (CodeMirror panel for `_imp_promoted_head_styles`) — view is shipped, edit is next
- Body-class field on page sidebar
- the agent's amendments D (versioned reviews `{slug}-v{n}.html`) and E (readability checklist)
- Permalink diagnostic, OpenAPI ping endpoint
- **Backend Media Review Queue** — designed in BACKLOG, not yet built
- Session 62: Passthrough theme auto-updater, hi-techsecuritysolutions fleet addition

---

## [4.6.1] — 2026-04-28 — **Agent Reliability Pass**

Addresses the agent's second-amendment feedback (post-v4.6.0 production testing).
Universal fixes that benefit any agent integration, not just the agent's workflow.

### Added — Capabilities endpoint (`GET /imp-health/v1/capabilities`, public)
Single-shot discovery JSON describing what an IM-Press install can do **right now**:
- `im_press_version`, `wp_version`, `php_version`
- `license` (tier, key_present)
- `modules` — reflected by `class_exists()` checks across core / SEO / content / commerce / forms+email / agent_layer
- `important_routes` — curated list of the 8 endpoints agents care about most, with method + purpose
- `openapi` (available + URL)
- `host_hints` — recommended headers, ModSec-recent-block detection
- `checked_at`

Solves the agent's "is this site really on the version it claims?" problem in one call. Public on purpose so agents can pre-check before making auth-required requests.

### Added — Agent smoke-test (`GET /imp-health/v1/agent-smoke-test`, auth required)
Battery of 8 sanity checks in one call: auth, pretty permalinks, uploads writable, Safe Deploy registered, OpenAPI reachable (self-fetched), SEO Workspace registered, media upload registered, static review registered. Returns per-check `status: ok | warn | fail` + summary counts + overall verdict.

Replaces the previous "agent has to test 6 endpoints to know if a site is healthy" pattern.

### Changed — `imp-preview/v1/static-review` (an AI agent amendments B + C)

**Fail-fast validation (amendment C)** — pre-flight `HEAD` check on `video_url` and `image_url` BEFORE generating the page. Falls back to `GET` with `Range: bytes=0-0` for hosts that block HEAD (some CDNs). Skips the check for same-host URLs (just-uploaded files may not be edge-cached yet).

If the URL doesn't return a 2xx/3xx, the endpoint refuses with a structured error:
```json
{
  "success": false,
  "error_code": "media_unreachable",
  "message": "video_url did not return 200 OK (got 404). HTTP 404",
  "url": "https://...",
  "http_status": 404
}
```

**Structured error codes (amendment C)** — every error response now has `success: false` + `error_code` + `message`. Codes: `missing_content`, `media_unreachable`, `verification_failed`. Replaces the previous freeform `error` strings.

**Health block in response (amendment B)** — extended self-verification covers:
- HTTP 200 ✓ (was already there)
- Token round-trip in body ✓ (was already there)
- No WP fallback sentinels ("Latest Posts", "No posts found", "Page not found", "404") ✓ (was already there)
- **NEW: expected title appears in rendered output**
- **NEW: expected media URL appears in rendered output**
- **NEW: no empty `<video src="">` tags**

Returned as a top-level `health` block:
```json
{
  "success": true,
  "recommended_review_url": "...",
  "health": {
    "ok": true,
    "status": 200,
    "size": 2134,
    "warnings": [],
    "checks": {
      "http_200": true,
      "token_present": true,
      "no_wp_fallback": true,
      "title_present": true,
      "media_url_present": true,
      "video_src_non_empty": true
    },
    "checked_at": "2026-04-28T08:59:38Z"
  },
  ...
}
```

Hard failures (HTTP/token/sentinel) clean up the file and return `error_code: verification_failed` with the diagnostic. Soft failures (title not found, etc.) keep the file but populate `warnings[]`.

### Changed — Response shape contract (an AI agent amendment F)

**Confirmed: no IM-Press response includes rendered HTML by default.** Audited all v4.6.0 endpoints (preview pages, html-importer, safe-deploy, media, openapi, ai-endpoints, health-monitor). Going forward this is a contract: any future endpoint that wants to return rendered output must gate it behind `?include_rendered=true`.

### Live verification on 4-hitech.info

- `/capabilities` returns full discovery JSON (8 core modules, 7 SEO modules, 8 important routes, agency tier).
- `/agent-smoke-test` returns 8/8 ok.
- Empty input → `error_code: missing_content` ✓
- Unreachable URL → `error_code: media_unreachable` with `http_status: 0` for DNS failure ✓
- Healthy create → `health: { ok: true }` with all 6 checks passing ✓

Smoke test fleet-wide: **296/296** (was 245 in v4.6.0 — added 2 new endpoints × 8 sites = 16 + bug-fix path coverage).

### Anti-patterns audit (clean)
All hooks in `init()` callbacks. All REST routes inside `rest_api_init`. All callbacks public. No `{$self::}`, no `IMPress_Constants::`. All PHP built-ins prefixed with `\`.

### Remaining backlog (v4.6.2+)
- an AI agent amendments D (versioned reviews `{slug}-v{n}.html`) and E (readability/contrast checklist) — deferred.
- Permalink diagnostic, OpenAPI ping endpoint — deferred.
- **Backend Media Review Queue** — bigger feature, designed but not built. See `BACKLOG-v4.6.1.md`.
- Admin UI for v4.6.0 features (Static Reviews page, Promotion meta box).
- Session 62: Passthrough theme auto-updater, hi-techsecuritysolutions fleet addition.

---

## [4.6.0] — 2026-04-28 — **Make AI Pages Just Work**

This release closes the gap between "the agent generated a beautiful HTML page" and "the human sees a beautiful page in the browser." Driven by real agent-in-production feedback — black-bar/white-margin footer bug, ModSecurity 406 video uploads, "Latest Posts / No posts found" instead of the expected page, no canonical link to send a human for review. All six pain points addressed.

### Added — `class-page-render-shim.php`
Foundation runtime helper that hooks `body_class`, `wp_head`, `wp_footer`, and `the_content`. Targeted CSS reset injected on `blank-page.php` template kills the 8px browser default `body` margin (the source of the white-frame-around-black-footer bug). Renders lifted `<style>` / `<link>` / `<script>` assets on auto-promoted pages. Adds `.imp-fullbleed`, `.imp-landing`, `.imp-promoted` body classes for safe per-template CSS targeting. Hides the WP admin bar on landing pages so the 32px chrome doesn't break full-bleed designs.

### Added — HTML importer auto-promotion (`mode=promoted`)
`POST /wp-json/im-press/v1/import-html` with `mode: 'promoted'` parses a full HTML document with `DOMDocument` and:
- Wraps body content in `<div class="imp-page-root">…</div>` and stores it in `post_content`
- Lifts `<style>` blocks to `_imp_promoted_head_styles` post meta, **rewriting `body` and `html` selectors to `.imp-page-root`** so the AI's body-level styles apply correctly to the wrapper
- Lifts `<link rel="stylesheet">` and `<link rel="preload">` tags to `_imp_promoted_head_links`
- Lifts `<script>` from `<head>` to `_imp_promoted_head_scripts_head`
- Lifts trailing-body `<script>` tags to `_imp_promoted_head_scripts_body`
- Preserves the original blob in `_imp_html_original` post meta (blob preservation)
- Auto-sets `blank-page.php` template + `imp-fullbleed` body class
- Marks page with `_impress_promoted` flag

Result: visual fidelity preserved, every piece editable independently, body/html CSS no longer orphaned.

### Added — Restore endpoint
- `POST /wp-json/im-press/v1/import-html/restore` body `{"page_id": N}` — rebuilds the page from `_imp_html_original` and clears all promotion artifacts
- `GET /wp-json/im-press/v1/import-html/promotion-info?page_id=N` — inspect promotion state (lifted asset counts, restore URL)
- Both routes use body/query params (not path-positional `/restore/{id}`) to avoid ModSecurity rules that flag POST-to-numeric-path

### Added — Safe Deploy `template_mode` parameter
`POST /wp-json/imp-deploy/v1/safe-update` now accepts `template_mode`:
- `blank_canvas` → `blank-page.php` + `imp-fullbleed`
- `landing_page` → `blank-page.php` + `imp-fullbleed imp-landing` (also hides admin bar)
- `theme_page` / `post_content_only` → default theme template
Resolved via `IMPress\HTML_Importer::resolve_template_mode()` so the same logic powers importer, Safe Deploy, and prompt-to-page. Response now includes `page_url`, `edit_url`, `body_class`, and the resolved `template_mode`.

### Added — `class-preview-pages.php` (theme-bypass static review pages)
**The most-requested fix from the agent feedback.** When a human (the site owner) opened a Safe-Deploy preview URL in the browser, WordPress's permalink fallback sometimes served "Latest Posts / No posts found" instead of the page — even though the API returned 200. Cause: theme/permalink fragility on shared hosts.

`POST /wp-json/imp-preview/v1/static-review` writes a **flat HTML file directly to `/wp-content/uploads/imp-previews/{token}.html`** — bypasses WP routing entirely. No theme, no permalinks, no rewrites, no fallback.

- ONE canonical `recommended_review_url` in the response (the link to send to the human)
- Other URLs (direct asset, admin edit) labeled in a `metadata` sub-object so agents pick the right one
- Self-verifies after write — fetches the URL itself and checks for HTTP 200 + token presence + sentinel words ("Latest Posts", "No posts found", "Page not found", "404"). If verification fails, the file is deleted and the endpoint returns the diagnostic rather than a broken URL
- Three built-in styles: `flat` (centered), `phone` (phone-frame around video for reels), `minimal` (no chrome)
- Optional draft watermark banner (default: "DRAFT PREVIEW — NOT FINAL")
- Optional auto-expiration via daily WP-cron sweep
- Token-based unguessable URLs (`wp_generate_password(16, false)`)
- Atomic file write (temp file + rename), path-traversal-safe deletion
- `POST /imp-preview/v1/static-review/delete` body `{"token": "…"}` — case-sensitive token validated by regex, not `sanitize_key` (which lowercases)
- `GET /imp-preview/v1/static-review/list` — admin inspection

### Added — `imp-media/v1/upload` (multipart wrapper)
ModSecurity on shared hosts (Hostgator, Bluehost) rejects raw-binary uploads to `/wp/v2/media` with a 406. New endpoint always uses `multipart/form-data` internally and returns a richer response:
```json
{
  "media_id": 123,
  "direct_file_url":      "https://site/.../video.mp4",
  "attachment_page_url":  "https://site/?attachment_id=123",
  "recommended_share_url": "https://site/.../video.mp4",
  "mime_type": "video/mp4",
  "filesize_bytes": 12345678,
  "is_image": false, "is_video": true,
  "width": null, "height": null
}
```
Decision logic: `recommended_share_url` is the attachment page for images (gives caption + alt + lightbox), the direct file URL for everything else (videos, PDFs share better as direct links).

### Added — OpenAPI `x-im-press-agent-notes`
The `imp-openapi/v1/spec` endpoint now annotates each path with host/security gotchas before an agent makes the request. Examples surfaced:
- `/wp/v2/media`: "Some shared hosts reject raw-binary uploads with a 406. Use `imp-media/v1/upload` instead."
- `/imp-deploy/v1/safe-update`: "Use this for any HTML containing inline `<style>`/`<script>`/`<svg>`. The standard `/wp/v2/pages` endpoint runs `wp_kses_post` and silently strips these."
- `/imp-preview/v1/static-review`: "Generates a self-contained HTML file in `/wp-content/uploads/imp-previews/` — bypasses theme/permalink/template fragility entirely. Returns ONE `recommended_review_url`."
8 routes annotated initially; the lookup table is centralized in `class-openapi.php::AGENT_NOTES` and easy to extend.

### Added — Prompt-to-page `template_mode` parameter
`POST /wp-json/im-press/v1/ai/prompt-to-page` now accepts `template_mode` (default `landing_page`). Generated pages immediately render full-bleed without theme chrome fighting the design.

### Fixed
- The black-bar / white-margin footer bug on AI-imported pages (root cause: 8px browser default body margin + theme CSS leak through `wp_head()`).
- ModSec 406 on POST to `/import-html/restore/{id}` — moved page_id from URL to body.
- `count((array)'')` returning 1 on empty meta — promotion-info now uses an `is_array` guard.
- `sanitize_key` lowercasing case-sensitive tokens — preview-pages delete now validates with `preg_match( '/^[a-zA-Z0-9]{8,32}$/', … )`.

### Anti-patterns audit (clean)
- All hooks in `init()` callbacks — zero file-scope `add_action`/`add_filter`.
- All `register_rest_route()` calls inside `rest_api_init` callback.
- All hook callbacks public static.
- No `{$self::}` interpolation, no `IMPress_Constants::` references, no JS arrow functions in inline JS.
- All PHP built-ins (`\DOMDocument`, `\WP_REST_Request`, `\WP_Error`) properly namespaced inside `namespace IMPress;` files.

### Live verification on production fleet
- Deployed to all 8 sites (airobotbuild, ohioledwall, aimsclawz, nonursinghome, servicetechohio, answerneo, airobotbuilds, **4-hitech.info**).
- All sites HTTP 200 on health ping, version 4.6.0.
- Smoke test 245/245 endpoints across 7 fleet sites (35/35 on 4-hitech.info).
- Promotion + restore + render shim verified end-to-end on nonursinghome.com (10/10 markers present in rendered HTML).
- Static-review create + verify + delete tested on 4-hitech.info (11/11 content checks pass, file 200 → 404 after delete).
- OpenAPI agent-notes confirmed live on 4 paths per site.

### Deferred to v4.6.1 (admin UI)
- IM-Press → Static Reviews admin page (browse / view / delete from WP admin instead of REST)
- Promotion meta box on page edit screen with "Restore Original" button
- Lifted styles editor (CodeMirror panel for `_imp_promoted_head_styles`)
- Body class field in page sidebar

### Sites added to fleet
- `4-hitech.info` (cPanel main domain at `~/public_html`, added to deploy script + smoke test)

---

## [4.5.0] — 2026-04-17 — **Architecture Cleanup (Refactoring Only)**

Zero behavior changes. Every endpoint, shortcode, and admin page works identically. Smoke test: 210/210 before and after.

### Changed
- **Unified namespace convention** — every IM-Press class now lives in `namespace IMPress;`. Migrated 36 global-scope `IMPress_FooBar` classes to `\IMPress\Foo_Bar` and rewrote 206+ cross-references across the codebase (barewords, strings in hook callbacks, leading-backslash forms). Side effect: `$wpdb`, `\WP_Error`, `\WP_REST_Request`, `\Exception`, etc. now correctly prefixed with `\` everywhere a namespaced class references global symbols.
- **Unified init pattern** — every class now exposes a `public static function init()` as its single entry point. Singletons that previously required `ClassName::instance()->init()` / `::get_instance()->init_hooks()` / `new ClassName()->init()` now delegate internally via a static wrapper; the bootstrap block in `im-press.php` is 100% `ClassName::init()` calls with no mixed patterns. Seven classes got the static wrapper: `Blocks_V2`, `Blocks_Extra`, `Block_Animations`, `Block_Data_Bindings`, `Bookings_V2`, `Performance`, `Admin_Dashboard_V2`. `Forms_V2` also got a static `::init()` that defers to its existing constructor-based init.
- **Deduplicated constants** — `IM_PRESS_VERSION`, `IM_PRESS_FILE`, `IM_PRESS_URL` usages across the codebase replaced with `IMPRESS_VERSION`, `IMPRESS_PLUGIN_FILE`, `IMPRESS_PLUGIN_URL` (canonical). The `IM_PRESS_*` names remain defined as backward-compat aliases for any third-party code.
- **Logical file ordering** — `im-press.php` bootstrap replaced chronological "version diary" ordering (`// v3.0.0 — ...`, `// v4.1.3 — ...`) with nine function-group sections: **Core**, **Blocks**, **Content & Media**, **SEO**, **Email & Forms**, **Commerce**, **AI & Prompt**, **Agent Layer** (feature-flagged), **Admin UI**. Both the `require_once` block and the `init` callback mirror the same grouping with matching section comments.

### Added
- **`uninstall.php`** — new plugin root file. Fires when a user deletes IM-Press from Plugins → Delete (not on deactivate). **Opt-in only** — honours a new "Delete all IM-Press data on uninstall" toggle in IM-Press → Settings → General → Data & Privacy (default: off). When enabled, drops 18 custom tables, deletes 17 option keys, clears all `imp_*` / `impress_*` transients, removes `imp_product` + `imp_form` CPT posts, deletes `imp_product_cat` terms, and scrubs 8 post-meta keys. Existing customers who uninstall lose nothing unless they explicitly flip the switch.

### Fixed (incidentally, via namespace migration)
- Pre-existing dead-code references in the feature-flagged agent layer (e.g. `class_exists('IMPress_Products')`, `class_exists('IMPress_Forms')`, `\IMPress_Agent_Log::log(...)`) that would have always returned false / fatal'd if the agent layer were ever enabled, now resolve correctly to the real namespaced classes.

### Developer notes
- New `skills/im-press-dev/SKILL.md` section: **Coding Standards** now prescribes `namespace IMPress;` + `public static function init()` on every new class, and the `IMPRESS_*` constant prefix.
- Anti-pattern grep commands in `README.md` updated — searching for `class IMPress_` in `includes/*.php` should now return zero results (the `IMPress_Nav_Walker` inside `class-theme-layer.php` is a deliberate exception — it's a nested helper already inside `namespace IMPress`).
- Migration was shipped in two deploys (constants+reorder first, namespace rewrite second) so the high-risk rename wasn't coupled to structural changes. Rollback from either phase was always possible via the retained phase-1 ZIP.

---

## [4.4.3] — 2026-04-17 — **OpenAPI Spec + AI Agent Guide**

### Added
- **New module** `includes/class-openapi.php` (~370 lines) — public OpenAPI 3.0.0 spec endpoint at `GET /wp-json/imp-openapi/v1/spec`. Walks every route that `rest_get_server()->get_routes()` returns, filters to IM-Press namespaces (`imp-*`, `impress/*`), and emits a valid OpenAPI document with `info`, `servers`, `components/securitySchemes` (HTTP Basic Auth via Application Passwords), `tags` (one per namespace), and `paths` with methods, parameters, request bodies, and tier hints.
- **`x-im-press-tier` extension** on every path — marks minimum tier (`starter` / `pro` / `agency`). Agents can read this before making a request to avoid 403s.
- **`x-im-press` info block** — plugin version, total namespace + endpoint counts, authentication instructions, tier pricing model, documentation links.
- **Public endpoint by design** — the spec is the menu, not the food. Individual endpoints still enforce their own capability + tier checks when hit with real requests. Free users see the full spec (including Pro/Agency features) — this is the upsell moment.
- **1-hour transient cache** on the spec. Invalidated automatically on `activated_plugin` and `upgrader_process_complete` hooks, or manually via `POST /wp-json/imp-openapi/v1/refresh` (admin only).
- **`docs/AI-AGENT-GUIDE.md`** (~170 lines) — universal AI agent guide covering authentication, endpoint discovery via `/wp-json/`, `OPTIONS` for per-endpoint schemas, the OpenAPI spec endpoint, a one-line namespace map, workflow patterns (SEO Workspace task queue, Safe Deploy, content pipeline), anti-patterns that will break sites, and the tier model. Deliberately does NOT duplicate what `/wp-json/` already self-documents.
- **Claude/Cowork dev skill** — `skills/im-press-dev/SKILL.md` — triggered by any IM-Press development context. Covers the 4 class init patterns, the anti-pattern pre-release scan (5 grep commands), new-module procedure, build→deploy→verify workflow, GitHub release ritual, site mapping table. Points to `AI-AGENT-GUIDE.md` for REST API patterns and to `CLAUDE.md` for site-specific context.
- **OpenClaw agent skill** — `skills/openclaw-im-press/SKILL.md` — triggered when the agent is operating IM-Press sites in production. Covers authentication, self-discovery via the OpenAPI spec, task-queue workflow, content-pipeline workflow, Safe-Deploy vs standard WP API, common endpoints, and agent-specific anti-patterns.

### Integration
- OpenAPI output reflects the live state of each site: routes that are disabled (agent layer off, module tier-gated out) simply don't appear, so the spec always matches the site's actual capabilities.
- Smoke test updated to hit `GET /imp-openapi/v1/spec` and expect 200.

### Documentation
- `README.md` — bumped to v4.4.3, updated class count (76+ → 79+), endpoint count (179+ → 215+), added SEO Workspace / Breadcrumbs / OpenAPI to the architecture tree and REST API tables, added links to the AI Agent Guide and OpenAPI spec in the Documentation section.
- `docs/docs-*.html` — updated version numbers, class/endpoint counts, and added SEO Workspace, Internal Linking, Breadcrumbs, and OpenAPI Spec to the feature lists. Added an "AI Agent Setup" section to `docs-getting-started.html`.

### Tier gating
- `imp-openapi/v1/spec` — **public on all tiers** (including free).
- `imp-openapi/v1/refresh` — admin-only.

### Anti-patterns audit (clean)
- All hooks inside `init()` — zero file-scope `add_action`.
- Both REST routes nested inside `rest_api_init` callback.
- All hook callbacks public static.
- No `{$self::}`, no `IMPress_Constants::`, no JS arrow functions (no inline JS at all).

---

## [4.4.2] — 2026-04-17 — **Breadcrumbs Module**

### Added
- **New module** `includes/class-breadcrumbs.php` (~580 lines) — site-wide breadcrumb navigation. Extends the blog-post-only breadcrumb implementation that lived inside `class-article-templates.php` to every WP context, while keeping full backwards compatibility with the old behaviour.
- **Context coverage**:
  - **Pages** — walks `post_parent` hierarchy (Home → Parent → Child → Current).
  - **Blog posts** — Home → Blog Page → Category (with parent chain) → Post.
  - **Products** (`imp_product` CPT) — Home → Products archive → Taxonomy term (with parent chain) → Product.
  - **Custom post types** — auto-detects archive slug (when `has_archive`) and the first public taxonomy.
  - **Taxonomy archives** — walks term parents.
  - **Post-type archives** — Home → Archive name.
  - **Date archives** — Home → Year / Month / Day label.
  - **Search results** — Home → `Search: "query"`.
  - **Author archives** — Home → Author display name.
  - **404** — Home → Page Not Found.
- **`[imp_breadcrumbs]` shortcode** — works on `blank-page.php` templates (critical for the Passthrough deploy method). Supports `post_id`, `separator`, `class` attributes.
- **JSON-LD BreadcrumbList** emitted in `wp_head` (priority 50) — Google's preferred format. Toggle-able via settings.
- **Inline HTML microdata** (`schema.org/BreadcrumbList` + `ListItem` + `aria-current="page"` on the final crumb) — keeps the visible markup crawler-friendly alongside the JSON-LD.
- **REST endpoints** under `imp-breadcrumbs/v1`:
  - `GET /trail/{id}` — public endpoint returning the breadcrumb array for any post/page. Agents can pull trails without WP-admin auth.
  - `GET/POST /settings` — admin-only settings round-trip.
- **Admin settings page** at IM-Press → Breadcrumbs: toggle enable, home label, separator (6 presets), hide-on-front, auto-prepend to `the_content` (per-post-type opt-in), JSON-LD toggle, Site Config primary-color integration. Includes a live-rendered preview of the current trail.
- **Per-post meta box** on every public post type — hide breadcrumbs on this page, or override the final-crumb label.
- **Filter hooks**: `imp_breadcrumb_items`, `imp_breadcrumb_separator`.
- **Inline CSS** shipped on demand in `wp_head` — respects Site Config `primary_color` for link styling. `.imp-breadcrumbs` class matches Article Templates for CSS compatibility.

### Changed
- **`class-article-templates.php::render_breadcrumbs()`** now defers to the Breadcrumbs module when it is active. Pattern:
  `if ( class_exists( '\IMPress\Breadcrumbs' ) && \IMPress\Breadcrumbs::is_active() ) { return do_shortcode( '[imp_breadcrumbs post_id="…"]' ); }`
  Article Templates' own breadcrumbs remain as a fallback when the Breadcrumbs module is disabled, so nothing regresses for users who rely on the old behaviour.

### Tier gating
- **Pro** — same tier as Article Templates and the rest of SEO.

### Anti-patterns audit (clean)
- All hooks inside `init()` — zero file-scope `add_action`/`add_filter`.
- `register_rest_route()` calls nested in the `rest_api_init` callback.
- Hook callbacks (`save_meta_box`, `output_json_ld`, `maybe_auto_prepend`, `save_post`) are all public static.
- No `{$self::}` interpolation, no `IMPress_Constants::` refs, no arrow functions.

---

## [4.4.1] — 2026-04-16 — **Internal Linking Suggestions**

### Added
- **Internal linking analysis** inside `class-seo-analysis.php` — extends the existing focus-keyphrase + readability pipeline with a new `internal_links` section in the analysis response. No new DB tables, no new REST namespace — piggybacks on the existing `imp-seo-analysis/v1/analyze-content` endpoint.
- **Outbound suggestions** — scans published `post`, `page`, and `imp_product` content for keyphrase overlap, ranks by relevance (exact phrase in title = 10, phrase in content = 3/hit capped at 15, individual word matches scored lower, current-page-mentions-this-title = +5 bonus), returns top 10 with `already_linked` flag so the editor can highlight which suggestions are already satisfied.
- **Inbound opportunities** — surfaces other pages that mention the current focus keyphrase but don't yet link to this page. Perfect for finding "fix this in 30 seconds" internal linking wins.
- **Orphan detection** — `is_orphan` flag on every analyzed page, plus `inbound_link_count` showing how many other published pages already link to this one. Published pages with zero inbound links are flagged red in the SEO check.
- **Two new SEO checks** added to the keyphrase panel:
  - `kp_internal_links_out` — "Links to other pages" — green at 3+ outbound links, orange at 1-2, red at 0 with suggestions available.
  - `kp_internal_links_in` — "Links from other pages" — green at 3+ inbound links, orange at 1-2, red if orphaned.
- **New "Links" tab** in the Gutenberg sidebar panel (`PluginDocumentSettingPanel`) alongside SEO and Readability tabs:
  - Two KPI tiles (outbound count, inbound count) with red/yellow/green color coding.
  - Orphan banner when applicable.
  - Ranked outbound list with clickable permalinks, relevance score badges, match reason, and "✓ linked" indicator.
  - Ranked inbound opportunity list showing pages that could link here.
- **Field shape** per suggestion: `{ target_post_id, target_title, target_url, target_post_type, reason, relevance_score, match_count, already_linked }`.

### Performance
- Suggestions cached per `(keyphrase, post_id)` in a 1-hour transient (`imp_internal_links_{hash}`).
- Inbound link count cached per-post in `imp_inbound_count_{post_id}` transient.
- Caches invalidated on `save_post` and `delete_post` via a bulk `$wpdb->options` delete — no stale suggestions after content changes.
- SQL query capped at 100 candidate posts, results trimmed to top 10 per direction. Query uses indexed `post_status` + `post_type` filters.

### Tier gating
- **Pro** — same tier as the rest of SEO Analysis. Free tier users see no analysis panel at all.

### Anti-patterns audit (clean)
- All hooks inside `init()`; zero file-scope `add_action`/`add_filter`.
- `save_post` / `delete_post` callbacks (`invalidate_link_caches`, `count_inbound_links`) are both `public static`.
- Zero arrow functions in the inline Gutenberg JS — continued `function()` syntax.
- No `{$self::}` interpolation, no `IMPress_Constants::` references.

---

## [4.4.0] — 2026-04-16 — **SEO Workspace**

### Added
- **SEO Workspace module** (`class-seo-workspace.php`) — strategic SEO workbench that sits on top of the existing SEO engine (`class-seo.php`) and per-page scorer (`class-seo-analysis.php`). IM-Press becomes the workspace: you input business intent, AI agents pick up work via REST API using their own native tools, and post results back for review.
- **Four new database tables** (dbDelta, idempotent, auto-healed on load):
  - `{prefix}imp_seo_keywords` — tracked keywords with priority, current/target rank, search volume, difficulty, assigned WP page.
  - `{prefix}imp_seo_competitors` — competitor domains + notes.
  - `{prefix}imp_seo_tasks` — agent task queue with 5 task types (`analyze_gaps`, `generate_content`, `audit_page`, `research_competitor`, `keyword_research`), status machine (queued → claimed → running → completed/failed), atomic claim, input/output JSON.
  - `{prefix}imp_seo_content` — agent-produced content drafts with auto-scored SEO/readability, review workflow (draft → approved → deployed/rejected).
- **~22 REST endpoints** under `imp-seo-workspace/v1` — full CRUD for keywords/competitors/tasks/content, pending queue poll, atomic claim, approve/deploy/reject workflow, overview dashboard summary.
- **Admin page** at IM-Press → SEO Workspace — 5 tabs (Overview, Keywords, Competitors, Tasks, Content), KPI grid, kanban task board, content review table with inline approve/deploy/reject, slide-out task detail drawer, bulk keyword paste, dark/light theme, native PHP rendering (no React dependency).
- **Integration with existing modules**:
  - Auto-scores new content drafts via `IMPress_SEO_Analysis::run_analysis()` (reflection call — scoring is fail-silent).
  - Deploys approved content via direct `$wpdb->posts` write to preserve inline `<style>`/`<script>`/`<svg>` (same pattern as `class-safe-deploy.php`).
  - Records deploy history in `_imp_deploy_history` post meta so `class-revision-history.php` surfaces SEO Workspace deploys alongside Safe Deploy revisions.
  - Prefers `blank-page.php` template automatically when Passthrough theme is active.
  - Auto-assigns deployed page back to the source keyword (`assigned_page_id`).
  - Sideloads provided featured image URLs via `download_url` + `media_handle_sideload` (handles extensionless URLs like Unsplash).
  - `Image_Fetcher` picks up posts without a featured image naturally through its `wp_after_insert_post` hook — no explicit call needed.
- **Two WP action hooks** for external modules: `imp_seo_workspace_task_completed`, `imp_seo_workspace_content_deployed`, `imp_seo_workspace_content_created`.

### Tier gating
- **Pro** — Overview dashboard, keyword tracking (unlimited), gap report.
- **Agency** — Competitors, agent task queue, content workflow, deploy pipeline.
- Tier is checked inside each REST `permission_callback` and surfaces an upgrade banner in the admin UI when a tab is locked.

### Architecture
- Replaces standalone Flask SEO Dashboard (`~/seo-dashboard/`) — same feature set, native to WP, single-install.
- No MCP server needed — any agent that can make HTTP requests can poll `/tasks/pending`, claim with `POST /tasks/{id}/claim`, submit results via `POST /tasks/{id}/complete`, then save deliverables via `POST /content`.
- The strategy layer. `class-seo.php` is the on-page engine (meta/schema/OG), `class-seo-analysis.php` is the per-page scorer, SEO Workspace is the business-intent layer on top.

### Anti-patterns audit (new module, clean)
- Zero file-scope hooks — all `add_action()` calls live inside `init()`.
- All `register_rest_route()` calls nested inside `rest_api_init` callback.
- No `{$self::}` string interpolation.
- No PHP arrow functions (`fn`) in class code.
- No JavaScript arrow functions in admin inline JS (WP encodes `=>` to `=&gt;` in post_content — enforced plugin-wide since v2.3.0).
- All hook callbacks are public static.

---

## [4.3.1] — 2026-04-14 — **Download Delivery Transparency**

### Added
- **Version stamp in download delivery email** (`class-email.php::handle_download()`): the "Download Now" button in the IM-Press signup delivery email now includes:
  - `title="IM-Press v{VERSION}"` — browser tooltip on mouseover shows the exact version the recipient will receive.
  - `download="im-press-v{VERSION}.zip"` — when the user saves the file locally, the browser names it with the version (URL stays `im-press.zip` so the auto-updater keeps working).
  - Visible "Version {VERSION}" line rendered beneath the button.
  - All three values pulled from the `IMPRESS_VERSION` constant, so a single source of truth stays in sync with the plugin header.
- **Post-deploy public-ZIP verification** (`deploy-impress-update.sh`): new `[3/4]` step downloads the hosted ZIP at `airobotbuild.com/wp-content/uploads/im-press.zip` and confirms its version matches the one just pushed to the 6 sites. Catches the failure mode where a deploy pushes to all sites but forgets to refresh the public download package, which would leave the auto-updater serving a stale ZIP. Exits with non-zero status if the public package is out of sync.

### Changed
- Default ZIP path in `deploy-impress-update.sh` bumped from `~/im-press-v3.4.3.zip` (stale) to `~/im-press-v4.3.1.zip`.

---

## [4.3.0] — 2026-04-11 — **Licensing & Tier Gating**

### Changed
- **Tier definitions updated** — Starter (Free) now includes 25 blocks (was 16): added tabs, table, testimonials, before-after, pricing-table, process-steps, counter, progress-bar, star-rating. Pro includes all 100+ blocks + 17 feature modules. Agency adds white-label, bulk deploy, multi-site, AI endpoints, page templates, migration tools.
- **Actual feature gating enforced** — 17 classes now check license tier in their `init()` method. Free users get 25 blocks + basic SEO/sitemap/antispam/contact form. Pro features (SEO Analysis, Image Optimization, Redirects, Analytics, Health Monitor, Article Templates, Lightbox, Custom Code, Responsive Controls, Products Manager, Affiliate Manager, Embed, FAQ Schema, reCAPTCHA, Performance) return early with a dismissible admin notice and upgrade link if tier is below Pro. Agency features (Page Templates, White Label) require Agency tier.
- **Static tier helpers** added to `IMPress\Licensing`: `current_tier()`, `requires_tier()`, `is_pro()`, `is_agency()`, `tier_notice()`. Any class can check tier with a single call.
- **Prices in tier definitions** match Stripe: Pro $49/yr ($149 lifetime), Agency $149/yr ($449 lifetime). Starter free forever.

### Fixed
- Removed personal name references from plugin source (`@author [personal name]` → `Aims Clawz LLC`, `Hi-Tech Clawz` → `Aims Clawz LLC`, `Hi-Tech Solutions` → `Aims Clawz LLC`).

---

## [4.2.1] — 2026-04-10

### Fixed
- **PHP-only blocks now show visual preview in editor** (`class-ssr-editor.php`): 48 blocks registered with only `render_callback` (23 from Blocks V2, 23 from Blocks Extra, 2 from WP7) were showing raw attribute forms in the Gutenberg editor. Now wrapped with `ServerSideRender` so the editor displays the actual rendered output. Auto-generated `InspectorControls` sidebar for string, boolean, number, and enum attributes. Empty response placeholder with block name. Blocks with existing JS edit components (the 55 in `src/blocks/`) are untouched.

---

## [4.2.0] — 2026-04-09 — **Editor UX + Admin Polish + SEO Intelligence**

Three improvement tracks closing feature gaps with BB/UABB and Yoast.

### Added — Block Editor UX (Track 1)
- **22 Block Patterns** (`class-block-patterns.php`): Pre-designed section combos registered via `register_block_pattern()`. 9 categories: Heroes (3), Features (3), Pricing (2), Testimonials (2), CTA (3), Contact (2), Team (2), FAQ (2), Footer (3). All patterns use IM-Press blocks for consistent rendering.
- **Editor Polish** (`class-editor-polish.php`): Block transforms (core/heading ↔ im-press/heading, core/button ↔ im-press/button, core/paragraph → im-press/rich-text). Block variations for Hero (centered/left/gradient), Button (primary/outline/ghost), CTA (light/dark), Heading (section/page-title). Editor CSS with IM-Press badge on hover, accent outlines on selected blocks.
- **55 Rich Block Edit Components** (pre-existing from v4.1): Full React edit/save components with RichText inline editing, InspectorControls, MediaUpload, and ColorPicker. Built with `@wordpress/scripts` (487KB bundle at `build/index.js`).

### Added — Admin Polish (Track 2)
- **Unified Settings Hub** (`class-admin-settings.php`): Single "Settings" page with 8 horizontal tabs (General, SEO, Email, Forms, Images, Performance, Security, Advanced). AJAX save via REST API, consistent dark/light theme matching Dashboard V2. Replaces 11 separate module settings pages in the menu.
- **Dashboard Enhancements** (`class-dashboard-enhancements.php`): Animated health score donut chart (SVG arc with CSS transition), Quick Setup Wizard (5 steps: SMTP, SEO, License, Branding, First Page — auto-detects completion), What's New modal on first load after update (dismissible, version-keyed).
- **Menu Cleanup**: 11 individual module settings slugs removed from IM-Press submenu. All settings consolidated into the tabbed Settings page.

### Added — SEO Intelligence (Track 3)
- **Focus Keyphrase System** (`class-seo-analysis.php`): Gutenberg editor sidebar panel via `registerPlugin` + `PluginDocumentSettingPanel`. 10 real-time SEO checks: keyphrase in title, meta description, introduction, H2 subheadings, density (0.5–2.5%), URL slug, image alt text, content length, internal links, external links. Overall score 0-100 with animated SVG circle. Debounced 800ms updates. REST endpoints at `imp-seo-analysis/v1`: GET/POST `/analyze/{id}`, POST `/analyze-content`, GET `/bulk`.
- **Readability Analysis**: Second tab in the SEO sidebar panel. 7 checks: Flesch Reading Ease (with grade level), average sentence length, paragraph length, passive voice %, transition words %, consecutive sentence beginnings, subheading distribution. Overall readability score 0-100.
- **Posts List SEO Columns**: SEO Score (colored dot + number), Readability Score, and Focus Keyphrase columns on Posts and Pages list tables. Sortable by score. Bulk action "Analyze SEO" runs analysis on selected posts.
- Post meta stored: `_imp_focus_keyphrase` (string), `_imp_seo_score` (integer), `_imp_readability_score` (integer).

---

## [4.1.0] — 2026-04-07

### Added
- **Image Fetcher module** (`class-image-fetcher.php`): Auto-fetches Unsplash images for posts published without a featured image. Smart keyword extraction from title + content with stopword filtering, configurable orientation, transient cache, Unsplash download tracking per API terms, sideload to media library with photographer attribution, optional inline insertion after first H2 (uses `$wpdb->update` to preserve inline styles), per-post meta box (disable + custom keywords + fetch-now button), admin page under IM-Press → Images, REST endpoint `imp-images/v1/fetch`, activity log table `{prefix}imp_image_log`.
- **Admin Dashboard V2 — full rewrite**: Polished native PHP dashboard replacing the React bundle. Hero header with status pill, 4 KPI tiles (Health Score, Posts This Month with delta, License, Version), 4 tabbed module groups (Content, SEO & Performance, Site Operations, Agent & Advanced), 30 module cards with status pills + Configure buttons, slide-out drawer for inline module config (Image Fetcher, SMTP, Site Config, License), activity feed (right rail, refreshes every 30s), sticky quick-actions bar (Run Health Check, Fetch Image for Latest Post, Clear All Caches, Image Settings). All inline CSS/JS, dark theme, responsive, zero external dependencies. New REST endpoints: `imp-dashboard/v1/module-config/{slug}`, `/activity`, `/clear-caches`.

### Fixed
- **Form Submissions landing first bug**: Admin Dashboard V2 now registers its `admin_menu` hook at priority 1 and is initialized FIRST in the bootstrap `init` callback (before Forms V2). The Overview submenu (slug = parent slug `impress`) is explicitly registered first, so it correctly becomes the parent landing page instead of Forms V2's "imp-forms".

---

## [4.0.0] — 2026-04-06 — **WordPress 7.0 Native**

The first page builder built natively on WordPress 7.0's Abilities API, AI Client, and MCP Adapter. No React build step. No JavaScript bundles. Pure PHP, agent-first.

### Added — Native WP 7.0 Gutenberg Blocks (`includes/class-blocks-wp7.php`)
- **30 PHP-only Gutenberg blocks** registered via `register_block_type()` with `supports.autoRegister => true` (WP 7.0-beta3+ feature). WordPress auto-generates the InspectorControls UI from each block's attribute schema. Zero React, zero webpack, zero build step.
- **Phase 1 (10 blocks):** Hero, Heading, Button, CTA, Features Grid, Pricing Table, Testimonials, Image+Text, Accordion, Form.
- **Phase 2 (20 blocks):** Timeline, Counter, Info Box, Tabs, Modal, Countdown, Progress Bar, Team, Business Hours, Dual Heading, Fancy Text, Logo Grid, Social Icons, Gallery, Video, Table, How-To Schema (auto JSON-LD), Google Map, Subscribe Form, Flip Box.
- Each block: type-checked attributes, `render_callback` returning safe escaped HTML, supports `align` (wide/full) and spacing controls, registered under custom `im-press` block category.
- Version-gated: silently skips on WP <7.0-beta3 so older sites continue using existing shortcodes.

### Added — WP 7.0 Abilities API Integration
- All 27 IM-Press abilities now expose `meta.mcp.public => true` so they auto-surface to the MCP Adapter. Categories: `im-press-content`, `im-press-seo`, `im-press-site-ops`, `im-press-products`, `im-press-email`. Each ability has full JSON Schema input/output, execute_callback, permission_callback. Registered on `wp_abilities_api_init`.

### Added — Native MCP Server Registration
- New `register_mcp_server()` hooked on `mcp_adapter_init`. Calls `\WP\MCP\Core\McpAdapter::instance()->create_server('im-press', [...])`. Abilities marked `meta.mcp.public=true` are auto-discovered. Legacy filter bridge retained for older MCP Adapter releases.

### Added — AI Client Bridge
- Wraps `wp_ai_client_prompt()` → `WP_AI_Client_Prompt_Builder` chain (`generate_text()`, `as_json_response()`, `using_system_instruction()`) for prompt-to-page, auto-SEO, rewrite-section. Graceful fallback when WP AI Client unavailable. 1-hour transient cache.

### Changed
- Plugin version: `3.4.5 → 4.0.0`.
- Agent layer (5 classes) remains feature-flagged behind `IMPRESS_AGENT_LAYER` constant.

### Migration Notes
- Fully backwards-compatible. On WP <7.0-beta3, new block layer is silently skipped — existing shortcodes continue to render. On WP 7.0+, new blocks appear under "IM-Press" category. No DB changes. Drop-in upgrade.

### Why This Matters
- **First-to-market.** When WP 7.0 ships April 9, IM-Press is the only page builder with native autoRegister blocks, native Abilities, and native MCP server registration on day one.
- **Agent-first.** Every block, ability, and endpoint is callable by an AI agent through MCP without glue code.
- **No build step.** Plugin authors ship blocks by writing PHP. No webpack, no Node, no React rebuild on every WP version bump.

---

## [3.4.5] — 2026-04-05

### Fixed
- **CRITICAL: wp-admin 500 on all sites.** `class-plugin.php` line 143 referenced `new IMPressAdmin_Dashboard()` — a class that doesn't exist. Both `class-plugin.php` and `class-admin-dashboard.php` are in `namespace IMPress;`, so the correct call is `new Admin_Dashboard()` (same namespace, no backslash). The `\Admin_Dashboard` (global namespace) form also fails because the class is namespaced.
- **SQL syntax errors in `class-licensing-v2.php`.** Two `CREATE TABLE` statements (license_keys and license_activations) had missing commas between the last column definition (`updated_at`) and the first `KEY` index line. `dbDelta()` printed MySQL errors to output before headers were sent, causing wp-login.php "headers already sent" failures on some hosts.

### Lessons
- **Hostgator OPcache can only be cleared via HTTP.** CLI `opcache_reset()` and inode swaps do NOT clear the web server's OPcache. Must create a temp PHP file and hit it via HTTP: `<?php opcache_reset(); unlink(__FILE__);`
- **WordPress recovery mode persists in DB.** Even after fixing the underlying PHP error, WP keeps showing "critical error" until `recovery_keys` and `recovery_mode_email_last_sent` options are deleted from `wp_options`.
- **`dbDelta()` is strict about SQL commas.** A missing comma before `KEY` lines causes a syntax error that prints to output (not just logs), breaking any page that sends headers afterward.

---

## [3.4.3] — 2026-04-04

### Fixed
- **WP admin menu consolidation.** IM-Press had 3 separate top-level menus (v1 Dashboard, v2 Dashboard, Forms V2) and submenus scattered across 6+ different parent slugs (`impress-dashboard`, `options-general.php`, `im-press`, `im-press-suite`, `impress-settings`, `imp-dashboard`). All 19 submenu items now register under the single `impress` parent slug. 17 PHP files modified.
- **Products admin page blank.** The Products admin page (and SEO, Health, Email, Config, License pages) were blank because v2 Dashboard's React mount points were overriding v1 Dashboard's working server-rendered PHP pages due to slug conflicts. Fixed by trimming v2 to only the Overview page and letting v1 handle all functional admin pages.

### Changed
- `class-admin-dashboard.php` (v1): removed `add_menu_page()` call; all submenus now register under v2's `impress` parent.
- `class-admin-dashboard-v2.php` (v2): trimmed pages array from 9 entries to 1 (Dashboard only) to avoid slug conflicts with v1's working PHP pages.
- `class-forms-v2.php`: changed from `add_menu_page` (own top-level "Forms" menu) to `add_submenu_page` under `impress`.
- 8 modules changed from `options-general.php` to `impress`: site-config, lightbox, recaptcha, custom-code, embed, antispam, article-templates, responsive.
- 4 modules fixed orphaned parent slugs: white-label (`im-press`→`impress`), sitemap (`im-press-suite`→`impress`), form-admin (`impress-settings`→`impress`), bookings-v2 (`imp-dashboard`→`impress`).
- 2 CPT `show_in_menu` fixed: popup-builder (`im-press`→`impress`), forms (`impress-settings`→`impress`).

### Context
- User reported "2 menus in WP admin panel for IM-Press" and "Products doesn't load in the IM-Press admin menu."
- Root cause was v1 Dashboard (working server-rendered PHP) and v2 Dashboard (React mount points requiring missing compiled JS) both registering under different top-level menus with overlapping page slugs.
- v1 admin pages (SEO, Health, Products, Email, Config, License) remain the functional admin UI. v2 provides only the Overview dashboard.

---

## [3.4.2] — 2026-04-04

### Fixed
- **Agent Workflows 500 error.** `check_permissions` method was referenced as REST permission callback but never defined in `class-agent-workflows.php`. Added the missing method (requires `manage_options` capability).
- **AgentLog class name mismatch.** 10 calls to `AgentLog::log()` should have been `IMPress_Agent_Log::log()`. Caused undefined class errors when workflow endpoints were invoked.

### Changed
- **Agent layer now behind feature flag.** All 5 experimental WP 7.0 agent classes (`class-agent-log.php`, `class-abilities.php`, `class-mcp-bridge.php`, `class-ai-client-bridge.php`, `class-agent-workflows.php`) are no longer loaded by default. Enable with `define( 'IMPRESS_AGENT_LAYER', true );` in `wp-config.php`. This removes ~6,000 lines and 5 REST endpoint namespaces from runtime when not needed.

### Context
- The agent layer was built speculatively for WordPress 7.0's expected agent API. It has no current production use. Feature-flagging keeps the code ready for WP 7.0 while reducing runtime footprint and eliminating the 500 error from the smoke test.
- Found via new `im-press-smoke-test.sh` — 29-endpoint test across all 6 sites.

---

## [3.4.1] — 2026-03-31

### Fixed
- **Critical: REST route registration order bug.** Two modules (`IMPress_Bookings_V2`, `IMPress_Agent_Workflows`) called `register_rest_route()` directly during `init:20` instead of inside a `rest_api_init` callback. On WP 6.9.4, this triggered premature `rest_api_init` firing via `rest_get_server()`, preventing all later modules (v3.2.0+) from registering their endpoints. Routes went from 95 (broken) to 170+ (all working).
- Bookings V2: changed direct `$this->register_rest_routes()` to `add_action('rest_api_init', ...)`, made method public.
- Agent Workflows: split `init()` into `init()` (hook registration) + `register_rest_routes()` (actual route registration).

### Anti-pattern rule added
- **NEVER call `register_rest_route()` outside a `rest_api_init` callback.** This is now a mandatory code review check.

---

## [3.4.0] — 2026-03-31

### Added
- **Antispam module** (`class-antispam.php`, 475 lines) — Zero-config spam protection with three independent methods: honeypot hidden field (catches 80%+ bots), time-based validation (rejects <2s submissions with hash-verified timestamps), JS challenge token (nonce-based, blocks headless bots). Admin settings with block stats. 3 REST endpoints at `imp-antispam/v1`. Fail-open design — legitimate users never blocked. No external API keys needed. Replaces reCAPTCHA as the default spam protection layer.
- **Article Templates module** (`class-article-templates.php`, 1,469 lines) — Universal article template system for native WP posts. Auto TOC from H2/H3 headings (sticky, collapsible on mobile, smooth scroll, Intersection Observer highlighting). Breadcrumb navigation with BreadcrumbList schema. Reading time estimate (238 WPM + 12s/image). Prev/next post nav with thumbnails. Related posts (category-based with fallback). Author box (Gravatar + bio). Social share buttons (X/Facebook/LinkedIn/Email/Copy Link). Featured image hero with overlay. 3 layouts: Standard, Magazine, Minimal. Per-post template override + feature disable via meta box. Pulls branding from Site Config. Admin at Settings → IM-Press Articles. 3 REST endpoints at `imp-articles/v1`.
- **Embed module** (`class-embed.php`, 872 lines) — Secure iframe embedding via `[imp_embed]` shortcode + `imp/embed` Gutenberg block. Auto-detects 10 providers: YouTube (privacy-enhanced mode via youtube-nocookie.com), Vimeo, Google Maps, Spotify, SoundCloud, CodePen, Google Forms, Google Docs, Google Sheets, generic. Domain allowlist (13 defaults) + blocklist. HTTPS enforcement. Sandbox attributes per provider. Responsive containers (16:9, 4:3, 1:1, 21:9). Admin at Settings → IM-Press Embeds. 4 REST endpoints at `imp-embed/v1`.

### Fixed
- Article Templates: `init()` was calling `is_singular()` before query setup — hooks never fired. Moved to appropriate hook priority.
- Antispam: `set_defaults()` used truthy check instead of `false ===` — disabled settings got re-enabled on every load.
- Antispam: timestamp hash not verified — bots could forge submission times.
- Embed: used nonexistent `plugin_loaded` hook (should be direct call in bootstrap).
- Embed: missing ABSPATH check at top of file.
- Embed: used `??` null coalescing operator (PHP 7.0 compat concern on some hosts).

---

## [3.3.0] — 2026-03-31

### Added
- **reCAPTCHA v3 module** (`class-recaptcha.php`, 468 lines) — Google reCAPTCHA v3 invisible spam protection. Score-based verification (configurable threshold, default 0.5). Frontend auto-injection on pages with `[imp_form]`. Fail-open behavior (forms work if not configured). Admin settings page. 3 REST endpoints at `imp-recaptcha/v1`. Separate `recaptcha.js` (210 lines) + `recaptcha-admin.js` (147 lines).
- **FAQ Schema module** (`class-faq-schema.php`, 447 lines) — Auto-detects FAQ content from `[imp_accordion_item]` shortcodes and HTML `data-faq-question`/`data-faq-answer` attributes. Outputs FAQPage JSON-LD in `wp_head`. Manual FAQ meta box on page/post editor. Configurable max questions. 3 REST endpoints at `imp-faq-schema/v1`.
- **Gallery Lightbox module** (`class-lightbox.php`, 787 lines) — Vanilla JS, zero external dependencies. Auto-attaches to `.imp-gallery img`, `.wp-block-gallery img`, and any `.imp-lightbox` image. Arrow key navigation, ESC close, touch swipe, zoom toggle, image counter, focus trap, ARIA labels. Admin settings (animation style, overlay opacity, max width). Only loads on pages with gallery content. 2 REST endpoints at `imp-lightbox/v1`.

### Context
- Closes 3 Beaver Builder feature gaps: form security (reCAPTCHA), structured data (FAQ Schema), and image viewing (Lightbox).

---

## [3.2.0] — 2026-03-30

### Added
- **Responsive Controls module** (`class-responsive.php`, 470 lines) — Breakpoint management (mobile ≤767px, tablet ≤1024px, desktop). Per-page responsive settings via post meta. Global utility CSS classes: `.imp-hide-mobile`, `.imp-hide-tablet`, `.imp-hide-desktop`, `.imp-stack-mobile`, `.imp-cols-responsive`, `.imp-text-responsive`. Per-page responsive CSS generation. 5 REST endpoints at `imp-responsive/v1`. Admin settings page.
- **Custom Code Injection module** (`class-custom-code.php`, 662 lines) — Global + per-page CSS/JS injection. 6 hooks for output at proper priorities. 7 REST endpoints at `imp-custom-code/v1` (CRUD global/page code, validate syntax, list all pages with code). CSS/JS validation (balanced braces, XSS prevention). Admin UI with CodeMirror. **Key differentiator:** agent-accessible custom code via REST API — Beaver Builder has no equivalent.

---

## [3.1.0] — 2026-03-30

### Added
- **Passthrough import mode** — Added to `class-html-importer.php`. Raw HTML deploy via API without any block conversion. For pages that need exact HTML preservation (Kimi-built pages, React SPAs).
- **Page Templates module** (`class-page-templates.php`, 14KB) — Reusable template engine with `{{field}}` placeholder syntax. 8 REST endpoints. Designed for rank-and-rent bulk deployment (create template once, stamp out pages with different business names/phones/addresses).
- **Affiliate Manager module** (`class-affiliate-manager.php`, 27KB) — Multi-program affiliate registry with 2 database tables. Click tracking with analytics. 3 shortcodes: `[imp_affiliate_link]`, `[imp_affiliate_button]`, `[imp_affiliate_disclosure]`. Pre-registered programs: Amazon Associates, Medical Guardian, MobileHelp, Bay Alarm Medical.

### Dev notes
- All 3 files scanned for v3 anti-patterns before merge: no file-scope hooks, no `{$self::}` in strings, no `IMPress_Constants::` references, all hook callbacks public.

---

## [3.0.0] — 2026-03-30

### Added — Core feature upgrades
- **Email V2** (`class-email-v2.php`) — Automation workflows, A/B testing, template builder.
- **SEO V2** (`class-seo-v2.php`) — 17 REST endpoints, bulk audit, schema generator.
- **Forms V2** (`class-forms-v2.php`) — Drag-and-drop builder, conditional logic, multi-step forms.
- **Licensing V2** (`class-licensing-v2.php`) — Multi-product SaaS licensing, Stripe integration.
- **Bookings V2** (`class-bookings-v2.php`) — Resource management, recurring bookings, waitlist.
- **Performance** (`class-performance.php`) — Page cache, critical CSS extraction, Core Web Vitals optimization.

### Added — Block system overhaul
- **Blocks V2** (`class-blocks-v2.php`, 178KB) — 77 blocks with server-side rendering.
- **Blocks Extra** (`class-blocks-extra.php`, 93KB) — 23 additional blocks.
- **Block Parser** (`class-block-parser.php`) — Advanced markup parser.
- **Block Data Bindings** (`class-block-data-bindings.php`) — Dynamic data binding system.
- **Block Animations** (`class-block-animations.php`) — Animation library for blocks.
- **Admin Dashboard V2** (`class-admin-dashboard-v2.php`) — React-powered admin UI.

### Added — WP 7.0 Agent integration layer (experimental)
- **Agent Log** (`class-agent-log.php`) — Structured agent action logging.
- **Abilities** (`class-abilities.php`) — 40 AI agent ability registrations for WP 7.0.
- **MCP Bridge** (`class-mcp-bridge.php`) — MCP adapter configuration.
- **AI Client Bridge** (`class-ai-client-bridge.php`) — AI prompt wrapper with fallback chains.
- **Agent Workflows** (`class-agent-workflows.php`) — Bootstrap, rank-and-rent, dispatch workflows.

### Fixed (during deployment)
- 7 self-init `add_action()` calls at file scope — `__CLASS__` resolves to `""` outside a class definition, causing fatal errors. All self-init code moved to im-press.php bootstrap.
- Bootstrap `::instance()->init()` mismatched to classes using `::init()` (static), `::get_instance()->init_hooks()` (Forms V2), or `new + ->init()` (Admin Dashboard). Each class's init pattern is now called correctly.
- Undefined constants: `IM_PRESS_FILE`, `IM_PRESS_URL`, `IM_PRESS_VERSION` — added as aliases alongside `IMPRESS_*` constants.
- `{$self::$tables}` string interpolation in static context (30 occurrences in licensing) — `$self` in a double-quoted string is a variable reference, not the `self` keyword. Fixed to concatenation.
- Private `register_rest_routes()` used as hook callback — changed to public.
- `IMPress_Constants::PLUGIN_FILE` reference to nonexistent class — removed.

### Breaking
- Block count: 55 → 100 (77 V2 + 23 Extra). Old `class-blocks.php` still loaded but V2 takes precedence.
- Minimum PHP raised to 7.4 (was 7.0 soft requirement).

### Context
- Built for WordPress 7.0 readiness — the WP 7.0 agent API (`register_ability()`, `WP_AI_Client`) is expected to require exactly the patterns in our agent layer. These classes gracefully degrade on WP 6.x (no-op if WP 7.0 functions don't exist).
- Total: 50 PHP classes, 100 blocks, 80+ REST endpoints, 885KB.

---

## [2.5.0] — 2026-03-27

### Added
- **Title Tags module** (`class-title-tags.php`) — Dynamic `<title>` tag management per page/post. REST API endpoint. Overrides Yoast/default WP titles.
- **Analytics module** (`class-analytics.php`) — GA4 integration with measurement ID. REST API for configuration. Conditional script loading.
- **Redirects module** (`class-redirects.php`) — 301/302 redirect manager. REST API CRUD. Admin UI for managing redirects.
- **Image Optimization module** (`class-image-optimization.php`) — Native lazy loading, srcset generation, responsive image handling. REST API.

### Changed
- Yoast SEO fully replaced. IM-Press now handles: meta titles/descriptions, XML sitemaps, JSON-LD schema, OG/Twitter tags, robots meta, canonical URLs, redirects, analytics, image optimization.

### Context
- After this release, Yoast was deactivated on all 5 active sites via direct MySQL. Yoast gaps remaining: focus keyphrase system, readability analysis, internal linking suggestions.
- Total: 33 PHP classes, 58+ REST endpoints, 668KB.

---

## [2.4.0] — 2026-03-26

### Added
- **XML Sitemap module** (`class-sitemap.php`) — Serves `/sitemap.xml` (sitemap index) with sub-sitemaps for pages, posts, and products. All referenced in `robots.txt`.
- **Admin Dashboard** (`class-admin-dashboard.php`) — Central admin UI: Products Manager, Health Monitor, SEO settings, site config, all accessible from one page.

### Context
- This version added the admin experience layer that was missing from v2.3.x (which was API-first with no UI).

---

## [2.3.2] — 2026-03-26

### Fixed
- **Critical: License key option name standardized.** Three different option names existed: `impress_license_key`, `imp_license_key`, `im_press_license_key`. Standardized to `impress_license_key` everywhere. Auto-migration added to licensing init().
- Health Monitor now reads correct license key option.
- Updater now reads correct license key option.
- Naming convention documented: `impress_` prefix for plugin settings/licensing; `imp_` prefix for infrastructure (SMTP, site config).

### Added
- **Centralized Constants** (`class-constants.php`) — Single source of truth for all WP option names. `IMPress_Constants` class with grouped constants. Legacy names tracked with `_LEGACY` suffix.
- CHANGELOG.md included in plugin ZIP for the first time.

---

## [2.3.1] — 2026-03-26

### Added
- **SEO module** (`class-seo.php`) — Meta descriptions, Open Graph tags, Twitter cards, JSON-LD schema (Organization, LocalBusiness, WebPage, Article, Product), robots meta, canonical URLs. Auto-generates from page content. Admin UI at Settings → IM-Press SEO.
- Favicon support via `site_icon` WP option — generated 32×32 + 180×180 PNGs per site with branded colors.
- H1 tag fixes: `page.php` conditionally outputs H1 (skips if content already has one), `react-app.php` has `<noscript>` H1 fallback for crawlers.

### Changed
- `wp_robots` filter added for per-page `noindex`/`nofollow` control.

---

## [2.3.0] — 2026-03-26

### Added
- **Site Config module** (`class-site-config.php`) — Database-stored per-site configuration via `imp_site_config` WP option. REST API CRUD at `/wp-json/imp-config/v1/settings`. Admin UI. Eliminates all per-site PHP file patches (branding, logo, theme layer skip logic, asset dequeue rules).
- **Safe Deploy module** (`class-safe-deploy.php`) — `$wpdb->update()` REST endpoint at `/wp-json/imp-deploy/v1/safe-update`. Bypasses `wp_kses_post` content sanitization to preserve inline `<style>`, `<script>`, `<svg>`. Deploy history tracked per page.
- **Health Monitor module** (`class-health-monitor.php`) — 8 automated checks (pages, SMTP, products, patches, config, license, updates, links) at `/wp-json/imp-health/v1/check`. Public ping at `/imp-health/v1/ping` (returns version + timestamp).
- **Products Manager module** (`class-products.php`, 1,355 lines) — Custom Post Type `imp_product` with `imp_product_cat` taxonomy. Multi-affiliate link support per product. Admin meta boxes. REST API (9 endpoints). `[imp_products]` shortcode with category filters. HTML card migration endpoint.
- Enhanced theme layer: `should_skip_output()` now reads from Site Config. Auto-dequeues IM-Press assets on custom templates. No more mu-plugins needed.
- SMTP option auto-migration: `imp_smtp_username` → `imp_smtp_user`, `imp_smtp_password` → `imp_smtp_pass`.
- Enhanced HTML Importer: image import to media library, form wiring to email handler, link auditing, safe deploy mode.

### Context
- This release eliminated all per-site PHP patches. Configuration is now entirely database-driven via Site Config.
- Total: 25 PHP classes, 56+ REST endpoints, 631KB.

---

## [2.2.0] — 2026-03-25

### Changed
- **Email Handler merged into core** — Standalone `imp-email-handler` plugin deprecated. All email functionality (SMTP, download forms, newsletter subscribers, contact forms, unsubscribe) now in `class-email.php`. REST endpoints at `/wp-json/imp-email/v1/`.
- **Bookings generalized** — Standalone `imp-bookings` plugin deprecated. Booking functionality merged into core for equipment rental services (calendar widget, availability management, email notifications).

### Context
- This was the "plugin consolidation" release. Reduced from 3 plugins (IM-Press + Email Handler + Bookings) to 1.

---

## [2.1.0] — 2026-03-24

### Fixed
- **Bug #9: All 55 blocks now server-rendered with `do_shortcode()`.** Previously, some blocks rendered empty on the frontend.
- **HTML-to-blocks importer** (`/import-html` endpoint) — Converts external HTML pages to WP-native blocks. Used for migrating static pages to WordPress.

### Added
- Frontend content filters applied consistently to all block output.
- Auto-update system: all sites check `airobotbuild.com/wp-content/uploads/im-press.zip` every 12 hours. One-click update from WP admin.

### Context
- First version deployed to all 5 sites. Auto-update system enabled cross-site consistency.

---

## [2.0.0] — 2026-03-16

### Added — Initial release
- **55 blocks** covering: hero, heading, features-grid, pricing-table, accordion-faq, CTA, contact-info, image-text, process-steps, footer, rich-text, button, divider, video, gallery, tabs, testimonials, form, post-grid, modal, countdown, counter, progress-bar, table, before-after, team, social-icons, info-box, flip-box, timeline, business-hours, star-rating, content-toggle, price-list, logo-grid, subscribe-form, off-canvas, fancy-text, hotspot, table-of-contents, how-to-schema, google-map, login-form, video-gallery, image-separator, ribbon, dual-heading, search, slide-box, product-grid, product-categories, cart-button, mini-cart, account-page, checkout-fields.
- **Theme layer** — Header/footer rendering with template skip logic for custom pages.
- **REST API** — Full CRUD for blocks, pages, and settings.
- **Renderer** — Server-side block rendering engine.
- **Forms** — Basic form system with submissions.
- **AI Endpoints** — 7 REST endpoints: prompt-to-page, analyze-page, auto-seo, rewrite-section, site-health, clone-page, bulk-deploy.
- **Animations** — CSS animation system for blocks.
- **Popup Builder** — Modal/popup system with triggers.
- **Dynamic Content** — Dynamic data injection into blocks.
- **White Label** — Rebrand IM-Press UI for client sites.
- **Migrator** — Import layouts from Beaver Builder and Elementor.
- **Licensing** — Tiered license system (Starter/Pro/Agency).
- **Design Tokens** — Centralized design variables (colors, fonts, spacing).
- **Updater** — WordPress-native update mechanism from hosted ZIP.
- **HTML Importer** — External HTML to IM-Press blocks converter.
- **Email** — SMTP email, download forms, newsletter, contact forms.
- **6 WooCommerce blocks** — Product grid, categories, cart button, mini cart, account page, checkout fields.

### Context
- Built as a complete replacement for Beaver Builder + Ultimate Addons for Beaver Builder (UABB). Named "IM-Press" (plays on WordPress + "impress"). Decision was made to merge everything into a single plugin rather than separate theme + plugin (like BB does).
- Total: 18 PHP classes, 39 REST endpoints, 813KB.

---

## Version History Summary

| Version | Date | PHP Classes | Blocks | REST Endpoints | ZIP Size | Key Theme |
|---------|------|-------------|--------|----------------|----------|-----------|
| 2.0.0 | 2026-03-16 | 18 | 55 | 39 | 813KB | Initial BB replacement |
| 2.1.0 | 2026-03-24 | 18 | 55 | 39+ | 603KB | Bug fixes + auto-update |
| 2.2.0 | 2026-03-25 | 18 | 55 | 39+ | 611KB | Plugin consolidation |
| 2.3.0 | 2026-03-26 | 25 | 55 | 56+ | 631KB | Site Config + AI deploy |
| 2.3.1 | 2026-03-26 | 26 | 55 | 56+ | ~640KB | SEO module |
| 2.3.2 | 2026-03-26 | 26 | 55 | 56+ | 644KB | License key fix |
| 2.4.0 | 2026-03-26 | 28 | 55 | 58+ | 657KB | Admin Dashboard + Sitemaps |
| 2.5.0 | 2026-03-27 | 33 | 55 | 58+ | 668KB | Full Yoast replacement |
| 3.0.0 | 2026-03-30 | 50 | 100 | 80+ | 885KB | WP 7.0 readiness + V2 modules |
| 3.1.0 | 2026-03-30 | 52 | 100 | 88+ | 875KB | Templates + Affiliates |
| 3.2.0 | 2026-03-30 | 54 | 100 | 100+ | 882KB | Responsive + Custom Code |
| 3.3.0 | 2026-03-31 | 57 | 100 | 100+ | 898KB | reCAPTCHA + FAQ + Lightbox |
| 3.4.0 | 2026-03-31 | 60 | 100 | 100+ | 915KB | Antispam + Articles + Embed |
| 3.4.1 | 2026-03-31 | 60 | 100 | 170+ | 928KB | Critical REST route fix |

## Known Anti-Patterns (enforced in code review)

1. **NEVER call `register_rest_route()` outside `rest_api_init`** — causes premature server init on WP 6.9.4+.
2. **NEVER use `{$self::$prop}` in PHP strings** — `$self` is a variable reference, not the `self` keyword. Use concatenation.
3. **NEVER use `__CLASS__` at file scope** — resolves to empty string outside a class body. Fatal error.
4. **NEVER use PHP arrow functions in WP `post_content`** — WordPress HTML-encodes `=>` to `=&gt;`.
5. **All hook callbacks must be `public`** — private/protected methods can't be called by WordPress hooks.
6. **Each class has its own init pattern** — check the actual class before writing bootstrap code. Patterns: `::instance()->init()`, `::init()`, `::get_instance()->init_hooks()`, `new + ->init()`.
