=== Launchmind Blog ===
Contributors: launchmind
Tags: blog, content, ai, seo, articles
Requires at least: 5.8
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 5.7.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Display AI-powered Launchmind blog content on your WordPress site.

== Changelog ==

= 5.7.1 =
* **New: `intro=""` attribute on `[launchmind_blog]` for a per-page custom welcome line.** Replaces the auto-generated "Welcome to {site name}'s blog…" copy with whatever the customer types, without forcing `show_intro="false"` + a separate text block above the shortcode. Plain text only (HTML is escaped) so the markup stays predictable across themes. Empty value falls back to the language-default welcome. Example: `[launchmind_blog intro="Welkom op de blog van Whoon. Hier vind je interieurinspiratie."]`.
* **Settings page: Shortcode Reference card rewritten as a copy-paste cookbook.** Adds clearly-labeled examples for the 5.7.0 `wp_post_types` merge, the new `intro=""` attribute, common tweaks (columns, limit, show_excerpt / tags), multilingual usage, Q&A-only, and the single-post shortcode. Each example sits under its own H4 so customers can scan instead of decoding an attribute list. No behavior changes for the existing options.

= 5.7.0 =
* **New: `[launchmind_blog]` shortcode merges any post type, not just native `post`.** Adds a `wp_post_types` attribute (comma-separated) so customers whose own articles live on a custom post type (e.g. Whoon's "algemene-posts", a news site's "news", a magazine's "stories") can mix them into the Launchmind listing alongside their content. Example: `[launchmind_blog wp_post_types="post,algemene-posts" columns="3" limit="12"]`. Default stays `wp_post_types="post"` so existing installs are unaffected. Sanitized with `sanitize_key()` per slug; empty/garbled values fall back to `post`. Solves the page-builder edge case where Elementor Pro's Posts widget can't easily merge two post types in one query — drop the shortcode into an HTML/Shortcode widget instead and the merge happens server-side.
* **Why this matters.** Until now, customers on a custom CPT had to either accept two separate listing widgets (one for their CPT, one for Launchmind) or set up a custom Elementor query ID with PHP. The shortcode now does the merge + chronological sort in one call, no PHP snippet required.

= 5.3.6 =
* **Fix: `launchmind_article` CPT now passes Elementor Pro's second visibility gate (`show_in_nav_menus`).** v5.3.5 set `show_ui=true` to make our CPT visible to page-builder Source dropdowns, but Elementor Pro filters its post-type list with `get_post_types(['public' => true, 'show_in_nav_menus' => true])` — the standard WP convention for "linkable content types". Our CPT had `show_in_nav_menus=false` so we passed the first gate (show_ui) but failed the second. Customers still saw no "Launchmind Articles" in Elementor Pro Posts widget after 5.3.5. This release flips `show_in_nav_menus` to track `page_builder_compat` so both gates open together. JetEngine was unaffected by either gate; this only matters for Elementor Pro / Loop Grid / similar.
* **Side effect: "Launchmind Articles" now appears in Appearance → Menus.** Customers can technically pick a virtual post for a nav menu item, but the picker shows 0 items (we have no real wp_posts rows), so this is a UI no-op rather than a footgun.

= 5.3.5 =
* **Fix: `launchmind_article` CPT now visible to Elementor Pro + other page-builder Post widgets.** The CPT registration hardcoded `show_ui=false` to keep "Launchmind Articles" out of the WordPress admin sidebar. Side effect we hadn't anticipated: Elementor Pro's Posts widget and Loop Grid filter their Source dropdown by `show_ui`, so our CPT was invisible to them even with Page Builder Compatibility enabled. JetEngine used a looser filter and surfaced our CPT either way, masking the issue for sites that primarily used JetEngine. Fix: `show_ui` now tracks `page_builder_compat` (same as `public`, `publicly_queryable`, and `show_in_rest` already did). Admin sidebar suppression is independently controlled by `show_in_menu=false` so the admin UI stays clean. Customers with Page Builder Compatibility ON will see "Launchmind Articles" in every builder's Source dropdown after this update.
* **No customer action needed.** Existing JetEngine / Bricks / Breakdance integrations continue to work unchanged.

= 5.3.4 =
* **Fix: legacy `lm_<slug>_<random>` API keys couldn't auto-resolve subscription_id.** v5.3.3's auto-heal path only matched the modern `lm_<uuid>_<random>` key format. Customers who signed up before the UUID switch have keys shaped like `lm_broadwick_f7670e70...` — the slug isn't parseable into a subscription UUID locally, so the regex returned empty and tracking stayed off even after v5.3.3 shipped. Adds a server-side fallback: when the regex doesn't match, the plugin calls `/blog/customer` (already authenticated via api_key) and reads `subscription.id` from the response. Result is cached via transient (12h TTL) so it costs at most one API call per half-day. Server endpoint now returns `subscription.id` in its response shape — read by 5.3.4+ plugins, ignored by older ones.
* **Backward compatible.** Existing UUID-format installs skip the network call entirely (regex hits first). Sites where neither path resolves (no api_key set) still surface the admin notice from 5.3.3.

= 5.3.3 =
* **Fix: tracking beacons silently dropped on installs where `subscription_id` was wiped (typically after a plugin re-install).** Pre-5.3.3, the JS beacon, server-side PHP beacon, and diagnostic meta tag all read the `subscription_id` plugin option directly and bailed out when empty — even though the api_key (which stays put across re-installs) embeds the subscription UUID in its `lm_<uuid>_<random>` format. New helper `resolve_subscription_id()` returns the option when set, parses the UUID from api_key when not, and persists the derived value back into the option so subsequent calls skip the regex. Affected installs auto-heal on the next page-load — no customer action needed.
* **Always-on analytics tracking — `enable_analytics` UI toggle removed.** The "Enable analytics tracking" checkbox in plugin settings is gone. Tracking is mandatory: partner dashboards exist precisely to show traffic, so a hidden off-switch was a footgun (the most common path was a customer flipping it off while testing then forgetting). Sites that have legitimate reasons to suppress server-side beacons can use the `launchmind_disable_server_pageview` filter (PHP) — that path stays. The `launchmind_blog_enable_analytics` option in the database is now ignored; cleaning it up is a no-op.
* **Admin notice when subscription_id can't be resolved.** When BOTH the option is empty AND the api_key is missing/malformed (the only state where tracking is genuinely impossible), an amber warning surfaces in WP admin pointing the customer at the settings page. Previously these installs sent zero traffic with no UI signal — partners assumed their site had no visitors when actually the plugin couldn't beacon anything.
* **JS beacon detection widened.** `inject_tracking_script_on_footer` no longer gates on `$GLOBALS['launchmind_blog_post_data']` alone — it now uses the same three-path detection (CPT singular | query var | post object) the PHP beacon got in 5.3.2. Sites where the global wasn't populated for the active render path (CPT-singular templates served without the legacy slug interceptor) still get JS beacons.

= 5.3.2 =
* **Fix: PHP server-side pageview beacon never fired on legacy slug-rendering sites.** v5.2 introduced a server-side PHP beacon that was supposed to count pageviews even when the JS beacon got stripped (cache plugins, ad-blockers). In practice it was hooked to `template_redirect` priority 20, which fires *before* the content/template filters that populate `$GLOBALS['launchmind_blog_post_data']` under the legacy slug-interception render path. Result: on every site using legacy rendering (which is most of them), the `is_singular('launchmind_post') || get_query_var('launchmind_post')` gate failed, `register_shutdown_function` was never registered, and the beacon silently never fired — observed across all 4 active WP installs over the 14 days following the v5.2 release. Hook moved to `wp_footer` priority 999 (the same hook the JS beacon already uses), and the gate now reads `$GLOBALS['launchmind_blog_post_data']` like the JS beacon does. Coverage now matches the JS beacon: any render mode that produces a Launchmind article — CPT singular, virtual query, or legacy slug interception — sends one server beacon per render. Bot UA filter, opt-out filter, and the existing 30s dedup window with the JS beacon are all unchanged.
* **Plugin version added to beacon body.** Both PHP and JS beacons now ship `plugin_version` in the request body, persisted to event metadata. Lets the backend monitor plugin adoption per customer and verify that fixes like this one are landing on installed sites.

= 5.3.1 =
* **Fix: PHP error on ACF fields that don't return a string.** The 5.3.0 ACF compatibility hook included a catch-all `add_filter('acf/format_value', 'do_shortcode', 10);` that fired for *every* ACF field type, including ones that return arrays, integers or booleans (image, gallery, repeater, true/false, number, relationship, etc.) — and `do_shortcode()` expects a string. On sites with non-string ACF fields the result was a PHP warning per render, plus the field returning an empty string instead of its actual value. The catch-all has been replaced with a `type=text` scoped filter, matching the existing `type=textarea` and `type=wysiwyg` filters. Net effect: shortcodes still work in `text`, `textarea` and `wysiwyg` ACF fields, but other field types return their native value untouched. Reported by @pippijn on the WordPress.org support forum.

= 5.3.0 =
* **Q&A articles get their own filter path — three flavours.** Articles produced by the new Voice Engine cycle now arrive with `is_voice_article: true` and an auto-injected "Q&A" tag. Three independent ways for customers to put them on a separate page without writing a line of code: (1) **native taxonomy** — WordPress automatically maps the "Q&A" tag into a category/post_tag term, so `/category/q-and-a` (or whatever slug the host theme generates) lists Q&A articles out of the box; (2) **shortcode** `[launchmind_voice limit="6" columns="3"]` — drop on any WP page, behaves exactly like `[launchmind_blog]` but pre-filters to Q&A articles only; (3) **block toggle** — the existing "Launchmind Blog" Gutenberg block gains a "Show only Q&A articles" toggle in the Filter panel. All three live alongside each other; pick the one that matches your editing workflow.
* **Optional "Hide Q&A from main listing" setting.** New toggle under Settings → Voice Engine. OFF by default (existing sites keep current behaviour: Q&A articles appear in the main blog listing). Enable it on sites that want a clean separation — Q&A articles only render on the dedicated `[launchmind_voice]` shortcode, the "Show only Q&A articles" block, or `/category/q-and-a`. Recommended OFF for most sites; flip it ON when you want a dedicated /qa page that stays distinct from the main blog flow.
* **Backward-compatible API gate.** `is_voice_article` is read with a graceful fallback: when the upstream Launchmind API hasn't yet shipped the field on an older deploy, the plugin re-checks the article's `tags` array for "Q&A". So sites running 5.3.0 against an older API still get correct filtering. No breaking change for any existing shortcode, block, or template — adding 5.3.0 to a 5.2.x site that doesn't use Q&A articles is a silent no-op.

= 5.2.0 =
* **Server-side pageview beacon.** The plugin now POSTs a pageview from PHP on every actual Launchmind article render — independent of JavaScript, cache plugins, or render-mode globals. Until 5.2 every pageview was tracked client-side via `navigator.sendBeacon`, which silently dropped on sites where (a) LiteSpeed Cache / WP Rocket JS-optimization stripped or deferred the inline script, (b) the page was rendered through a CPT path where `$GLOBALS['launchmind_blog_post_data']` wasn't populated and the JS injection self-guard returned early, or (c) the visitor used an ad-blocker that flagged `/api/tracking/plugin-view` as a tracker domain. Affected sites showed zero traffic in their partner dashboards despite real visitors. The PHP-side beacon fires from `template_redirect` (deferred to `register_shutdown_function` so it never blocks the response), resolves the article slug via three independent paths (CPT global → query var → post object), filters out common bot User-Agents, and POSTs the same `{subscription_id, article_slug, platform, event_type, source: "server"}` payload the JS beacon already sends. Customers who want PHP tracking off can opt out with `add_filter('launchmind_disable_server_pageview', '__return_true');` (the JS beacon stays under the existing `enable_analytics` setting).
* **Backend dedup window keeps partner counts honest.** When a v5.2 site fires both beacons for the same pageview (PHP from render + JS from the rendered page), the backend now dedups on `(subscription_id, article_slug, ip_hash)` within a 30-second window: the first beacon to arrive increments the partner counters, the second is recorded for audit but skips the increment RPCs. End result for partners: exactly one pageview per visitor, regardless of which side succeeded — sites that previously showed zero traffic because of a stripped JS beacon now count via PHP, sites with healthy JS keep counting via JS, and sites with both running don't double-count.
* **Diagnostic meta tag in `wp_head`.** Adds `<meta name="launchmind-article" content="<sub>:<slug>:<plugin_version>">` on Launchmind article pages so support / debugging tools can verify a page is actually rendered by the plugin without trawling logs. Self-guards on the same `is_singular` check as the beacon — no-op on non-Launchmind pages.
* **Click beacon labelled `source: "client"`** for symmetry with the new server-side flag. No behavior change — clicks remain JS-only because outbound link interception requires a browser-side DOM listener.
* No PHP API, database, settings, shortcode or rendering-mode change. Drop-in upgrade for every existing 5.1.x install. Customers running aggressive cache plugins (LiteSpeed Cache, WP Rocket, Cloudflare APO) should see partner-dashboard pageviews start appearing within minutes of upgrade for the first time since installing the plugin.

= 5.1.11 =
* **WhatsApp / Telegram / Signal link previews now show the cover image.** Partner-uploaded cover images for Launchmind articles are typically 2-4 MB high-resolution PNGs (great for in-article rendering on desktop, problematic for chat-app preview crawlers). WhatsApp's preview crawler skips images larger than ~1 MB and fails outright above ~5 MB; LinkedIn and Facebook handle the same images fine because they re-process server-side at higher size limits. 5.1.11 detects when the cover image is hosted on Supabase Storage (the default for Launchmind partner uploads) and rewrites the og:image / twitter:image URL to use Supabase's `/render/image/public/` transformation endpoint with `width=1200&height=630&quality=80&resize=cover`. The transformed URL serves a ~150 KB JPG that all major chat-app preview crawlers accept, while the in-article body image still uses the raw high-resolution URL. Non-Supabase image hosts (Pexels, custom CDNs, theme-hosted) are left untouched — they either auto-optimize or already serve appropriately sized files. No new options.
* **Settings tab now documents the unified-blog shortcode pattern.** The Shortcode and Examples sections now show the `[launchmind_blog limit="6" columns="2" include_wp_posts="true"]` pattern that merges native WordPress posts and Launchmind articles into one date-sorted grid with built-in pagination. The Shortcode Options table also documents `include_wp_posts` and `offset` parameters that were previously only discoverable via reading the source. Recommended for sites built with Breakdance / Elementor / Bricks / Oxygen where the native Post Loop widget renders Launchmind cards without images or pills (the builders fetch post meta via raw SQL that bypasses the WordPress filter API our virtual posts depend on). One shortcode line replaces the broken builder integration.

= 5.1.10 =
* **Two new routes for page-builder Post Loop coverage.** Cards in Breakdance Post Loop / Elementor Pro Posts / Bricks Query Loop / Oxygen Repeater were rendering with title, date, and link but missing the cover image and category pill, even though our 5.1.6+ filter chain on `get_post_metadata`, `image_downsize`, `wp_get_object_terms`, and friends should have intercepted those calls. Two additions in 5.1.10: (1) the `launchmind_article` CPT now declares `category` and `post_tag` as supported taxonomies, so page builders that gate term-fetching on the CPT's declared taxonomy support no longer skip our posts; (2) when virtual posts are injected via `the_posts`, the WordPress object cache is now prewarmed with `_thumbnail_id` meta for each post, so builders that consult `wp_cache_get` before falling back to raw SQL still find the correct sentinel ID. Strictly additive — installs that already render fine on 5.1.6/5.1.7/5.1.8/5.1.9 keep working unchanged.

= 5.1.9 =
* **Topbar contrast fix for builder-managed "sticky on scroll" headers.** The 5.1.5/5.1.7 sticky-offset + brand-colour backdrop only fired for headers whose CSS `position` was already `fixed` or `sticky` on first paint. Many Breakdance, Elementor Pro, and Divi sites build their topbar as `position: absolute` over a transparent hero and only switch it to `position: fixed` *after* the visitor scrolls — those builders use class names like `bde-header-builder--sticky-scroll-`, `elementor-sticky`, `et_pb_sticky_module`. Until 5.1.9 our detector skipped them, so the article-page sticky-offset was never reserved and the topbar text would clash with the article body's contrast as soon as the visitor scrolled. 5.1.9 extends the detector to also accept absolute/relative headers whose class names match these builder-sticky patterns (or that carry a `data-sticky` attribute), so the offset and the brand-colour backdrop both apply. No-op on installs that don't use any sticky-on-scroll header.

= 5.1.8 =
* **Page-builder pill cleanup + extra term-resolution coverage.** Two fixes for builders that join multiple category names into a single concatenated pill string (Breakdance Post Loop is the canonical example): (1) the virtual `category` / `post_tag` terms emitted for Launchmind cards are now capped at **2 tags max**, with **2 words max per tag**, so long-tail Launchmind keywords ("data science bureau", "AI oplossingen bedrijf") never collide into an unreadable run-on label; the caps are filterable via `launchmind_blog_card_max_tags` / `launchmind_blog_card_max_words` for themes that do render multi-pill cards. (2) Term injection now also hooks the lower-level `wp_get_object_terms` filter as a backup path — some builder configurations resolve term relationships via direct calls that bypass `get_the_terms`, which would leave virtual cards with empty pills despite our `get_the_terms` filter being in place. No breaking changes; existing sites that already render fine on 5.1.6/5.1.7 keep their current pill layout (just with the tighter caps applied).

= 5.1.7 =
* **Auto-detect brand colour for the article-page topbar backdrop.** 5.1.5 added a contrast-safe but neutral backdrop (#101418 / #f5f5f5) behind the topbar on single-article pages whenever the host theme's topbar text was about to disappear against the article body. 5.1.7 makes the backdrop **brand-aware** by sampling the topbar's actual background colour on the homepage and using that on the article page. Mechanism: the sticky-detect script loads `/` in a hidden same-origin iframe during browser idle time, runs the same topbar detector inside that document, walks the topbar's ancestor chain to find the first non-transparent background, and writes the resulting colour to localStorage with a 24-hour TTL. First visit shows the neutral fallback for ~1 second while sampling completes, then swaps to the brand colour with a smooth 220ms transition; every subsequent visit applies the cached brand colour instantly on first paint. Resolution order: theme-set `--launchmind-article-topbar-bg` CSS variable wins, then cached brand colour, then neutral contrast fallback, then async homepage sample. Customers can still override with a single CSS line if the auto-detection picks the wrong region. The whole feature degrades silently — sites that block iframe-of-self via `X-Frame-Options DENY`, themes without a sticky topbar, or homepages where the topbar genuinely is the article-body colour all stay on the neutral fallback (or no backdrop at all). No new options, no admin setup, no breaking changes for installs that don't have a topbar contrast collision.

= 5.1.6 =
* **Critical fix for 5.1.5 page-builder rendering.** 5.1.5 introduced cover-image and category-pill rendering for Launchmind articles inside page-builder Post Loops, but on real Breakdance / Bricks / Oxygen sites the cards still rendered without an `<img>` and with an empty pill. Root cause: the `virtual_thumbnail_id`, `virtual_thumbnail`, `virtual_has_thumbnail`, and `virtual_terms` filters all called `get_post($virtual_id)` to resolve the article, but virtual Launchmind posts have no `wp_posts` row — so on a cold object cache the call did a DB lookup, returned null, and the filters silently returned the original (empty) value. Page builders therefore got `_thumbnail_id = 0` and an empty term list and skipped emitting the inline `<style> background-image: url(...)` block + the pill text. The plugin now keeps a request-scoped `post_id → API payload + slug` registry populated by `Launchmind_Blog_Merge::to_wp_post()` (and `Launchmind_CPT::build_virtual_post()` on the single-post path), and every CPT-aware filter resolves through the registry instead of through `get_post()`. Confirmed on Twentynext: cover images and category pills now render on Launchmind cards alongside native posts.

= 5.1.5 =
* **Page-builder cards now render the cover image.** 5.1.4 made Launchmind articles available to Breakdance Post Loop, Elementor Pro Posts, Bricks Query Loop and Oxygen Repeater queries, but those builders resolve featured images by passing the article's `_thumbnail_id` through `wp_get_attachment_image_src()` — a path that hits the attachment row in `wp_posts` and returned an empty `<img>` for our virtual posts. The plugin now intercepts `image_downsize`, `wp_get_attachment_url` and `wp_get_attachment_image_src` for our sentinel attachment IDs and returns the article's cover URL directly, so the same card template that paints the customer's own posts paints Launchmind articles end-to-end. Cover URLs are pre-registered when the loop builds the virtual post (no race with builder code that resolves images before our `_thumbnail_id` filter runs).
* **Page-builder cards now render category / tag pills.** Builders that loop `get_the_terms($post, 'category')` or `get_the_terms($post, 'post_tag')` to paint the coloured pill on each card got an empty placeholder for Launchmind articles (our CPT uses its own taxonomies). The plugin now returns synthesized `WP_Term` objects built from the article's API tags whenever a builder asks for `category` or `post_tag` on a `launchmind_article` post — pills now show meaningful labels with no setup. Custom-named taxonomies (e.g. `news_category` on bespoke themes) can opt in via the new `launchmind_blog_card_taxonomies` filter. Term archive links resolve to `#` since the synthesized terms have no real archive page.
* **Topbar contrast auto-fix on single-article pages.** Some themes (Twentynext-style agency themes, Wix imports, modern boutique designs) use a transparent `position: fixed` topbar with white logo + nav text, designed to overlay a dark hero on the homepage. On Launchmind single-article pages there's no hero, so that white text became invisible against the body's white background. The companion `launchmind-sticky-detect.js` now samples the topbar's actual text colour and effective background colour, computes the contrast, and — only when there is a real contrast collision — sets `--lm-topbar-backdrop` to a contrasting fixed band drawn behind the topbar. Light text on white pages now reads against #101418; dark text on dark pages reads against #f5f5f5. Customers can override the colour with one CSS line by setting `--launchmind-article-topbar-bg` in their theme — the script reads that first and uses it verbatim. Pages where the existing topbar already has sufficient contrast see no change.
* **Sticky-offset rules and detector now apply to CPT-mode (`single-launchmind_article`) too.** The 5.0.1 detector + scoped CSS were originally written for the legacy single-post body class; CPT mode (default since 4.11) uses `single-launchmind_article`, which the rules silently skipped. Both classes are now covered.

= 5.1.4 =
* **Page builder Post Loops now show Launchmind articles next to native posts.** The `launchmind_article` custom post type registers as `public` / `publicly_queryable` / REST-visible so Breakdance Post Loop, Elementor Pro Posts, Bricks Query Loop, Oxygen Repeater and similar widgets surface it in their Post Types / Include picker. **Crucially, the secondary `WP_Query` that those widgets run is now intercepted** — until 5.1.4 only the WP main blog query was hooked, so adding the CPT to a page-builder widget showed an empty loop because the articles aren't stored as real `wp_posts` rows. The plugin now injects Launchmind virtual posts into any secondary query whose `post_type` clause names `launchmind_article`, sorts the combined feed by publish date, and rewrites `found_posts` so pagination across the merged set is correct. End result: customers can pick Posts + Launchmind Articles in their existing card / loop widget and articles render with the host theme's existing card design — no shortcode, no manual styling. The CPT remains hidden from the WP admin menu, nav-menu picker and built-in search results; only the page-builder query sees it. Toggleable via Settings → Launchmind Blog → "Page Builder Compatibility" (default ON).
* **Setup polish.** The "Where should your articles appear?" welcome notice no longer appears after the setup wizard has already been completed. Previously, sites where the wizard ran to completion still saw the notice as a duplicate prompt. The notice now self-dismisses as soon as an API key is configured (the wizard already records the integration-mode choice), so admins see the question exactly once.
* **Documentation.** Added a "Page Builders (Breakdance, Elementor, Bricks, Oxygen)" help section to Settings → Launchmind Blog with concrete steps for adding `Launchmind Articles` to a Post Loop / Query widget. The internal post-type slug (`launchmind_article`) is documented for builders that ask for the slug rather than the human label.

= 5.1.3 =
* Fixed: Setup wizard "Open Settings & finish setup" button and welcome-notice form/redirect linked to a non-existent admin URL (`options-general.php?page=launchmind-blog` and `admin.php?page=launchmind-blog`), which made WordPress show "You do not have permission to view this page" even for site administrators. Updated all four references to the correct registered admin page (`admin.php?page=launchmind-dashboard`). After upgrading, the setup wizard's finish button and the welcome notice both land on the real Launchmind dashboard.

= 5.1.2 =
* Server-side parity: the Launchmind dashboard now records article-level views and outbound clicks for every plugin install (previously only subscription-level totals incremented for plugin traffic). No plugin-side change is required — existing installs benefit on next pageview. Companion server-side fix to the `metadata.platform` filter ensures plugin-tracked customers correctly appear in the partner dashboard's per-article breakdown.

= 5.1.1 =
* Fixed: Page-view + click analytics beacons silently never fired on sites running CPT rendering mode (the default since 4.11). The tracking script was wired into the legacy single-post renderer's template path, which CPT mode bypasses entirely — partner analytics dashboards therefore showed zero traffic for those installs even though the plugin and its content were rendering correctly. The script is now hooked on `wp_footer` with a self-guard that fires in both legacy and CPT modes, so click + page-view events flow into the dashboard for every Launchmind single-post pageview regardless of rendering mode. No PHP API, settings, shortcode, or rendering-mode change.

= 5.1.0 =
* **Resilience: stale-while-revalidate cache.** API responses are now stored with a 24-hour stale envelope on top of the existing 10-minute fresh window. When the upstream Launchmind API is slow or unreachable, the plugin keeps serving the last-known-good content instead of failing the page. Visitors see no disruption during transient upstream issues. Fresh-window TTL is unchanged for the 99% case where the API is healthy — this is a graceful-degradation safety net, not a longer cache.
* **Resilience: bandwidth + latency win via ETag/If-None-Match.** The API client now persists ETags returned by the Launchmind API and sends them back as `If-None-Match` on the next request. When content hasn't changed, the upstream returns `304 Not Modified` with no body — we re-use the cached value without a fresh download. At fleet scale this cuts plugin → API bandwidth by roughly 80% during steady-state, and shaves response latency on cache-revalidations.
* **Resilience: 8s API timeout (down from 30s).** A degraded upstream can no longer pin a customer site's pageload for half a minute. Combined with the stale-while-revalidate fallback above, slow upstream responses now degrade gracefully (8s → fall back to cached content) instead of stalling the whole page.
* **Scale: jittered cache TTLs.** Cache expiry is now scattered with ±60 seconds of randomness so customer sites don't all expire their caches in lockstep and stampede the upstream API. Spreads the refresh wave across a 2-minute window, dropping peak QPS by roughly an order of magnitude on high-traffic content.
* **Scale: object-cache promotion.** The cache layer now uses WordPress's persistent object cache (Redis / Memcached when present) on top of transients, so high-traffic sites with a managed object cache see fewer options-table reads on cache hits.
* **Scale: chunked sitemap storage.** Previously all sitemap posts were serialized into a single transient. On sites with 2000+ Launchmind posts that blob hit ~6 MB and was deserialized in full on every wp-sitemap.xml request. Storage is now chunked into 100-post pages, the count is read from a separate small transient for `get_max_num_pages()`, and partial fetch failures (page 3 fails) no longer wipe the chunks that did load. Sitemap XML output is byte-identical.
* **Edge cache headers on single-post pages.** Launchmind article responses now emit `Cache-Control: public, max-age=300, stale-while-revalidate=60` so CDN / page-cache layers (Cloudflare, WP Rocket, LiteSpeed) align with the upstream 5-minute cache instead of caching for hours and serving stale titles after edits, or caching for zero seconds and re-hitting upstream on every visit. Logged-in users are excluded so admins always see live content.
* **Security: tracking script no longer ships the API key to the browser.** The page-view + click beacons used to inline the customer's API key into the rendered HTML. The tracking endpoint already accepted the public-safe `subscription_id` on its own; the API key is now kept server-side only and the inline script ships nothing more sensitive than the subscription ID.
* **Visibility: daily plugin-health beacon.** The plugin now sends a once-a-day, fire-and-forget POST to `/api/plugin-health` reporting the plugin version, WordPress version, PHP version, post count, and rendering mode. This is how Launchmind operations answers "which sites are still on a vulnerable plugin version?" at fleet scale. Customers who don't want to send telemetry can opt out with `add_filter('launchmind_disable_health_beacon', '__return_true');`.
* No PHP API, database, settings, shortcode or rendering-mode change. Drop-in upgrade for every existing 5.0.x install. Sites running the previous transient cache format are upgraded transparently the first time each cache key is read post-upgrade.

= 5.0.1 =
* Fixed: On host themes that pin a `position: fixed` topbar to the viewport without compensating the single-post template with matching padding, the article title sat under the topbar at first paint — the first line was hidden behind the bar and only the second line peeked out below. Reported on bwnext.com and identical to the symptom many premium agency themes exhibit when their blog template forgets the sticky-header offset.
  - Adds `public/js/launchmind-sticky-detect.js` (≈1 KB), a vanilla detector that measures the actual height of any `position: fixed` / `position: sticky` element pinned at `top: 0` and writes the result (plus a 16px buffer) to a new CSS variable `--lm-sticky-offset` on `<html>`.
  - Adds scoped CSS rules under `body.single-launchmind_post` that consume that variable on the article title (matching the common WordPress h1 classes: `.entry-title`, `.wp-block-post-title`, `.post-title`, `.article-title`, plus generic `.entry-header h1` / `article > header h1`) and on `scroll-padding-top` so jump-links also land below the topbar.
  - Default value of `--lm-sticky-offset` is `0px`. On installs *without* a sticky topbar (the vast majority) the detector finds nothing, the variable stays at zero, and these rules are no-ops — existing layouts are byte-for-byte unchanged.
  - The script is enqueued only on single Launchmind posts (`is_singular('launchmind_post')`) so non-article pages never load it. Loads in the footer so it sees every theme-injected sticky element. Re-runs on `load` and on `resize` to handle late-injected headers (Breakdance, Elementor Pro, Divi) and mobile/desktop viewport changes. Filters out cookie banners and chat widgets by ignoring fixed elements narrower than 50% of the viewport or taller than 250 px.
* No PHP API, database, settings, shortcode or rendering-mode change. Drop-in upgrade for every existing 5.0.x install.

= 5.0.0 =
* Fixed: Blog listing card layout broke on themes that apply generic typographic rules to `<article>`, `<span>`, `<h3>` or `<img>` elements — visible as a grey band above the post image, the "·" separator dropping to its own line between date and author, and a large vertical gap between the card title and the meta row. The 4.11.x release line hardened the single-article body (`.lm-article-body`) against the same class of theme CSS bleed on Breakdance / Divi / Elementor / custom block themes; 5.0 brings that same defensive baseline to the blog index cards rendered by `[launchmind_blog]` (and therefore the auto-created /blog page, since it runs the shortcode under the hood). Concretely:
  - `.launchmind-post-image img` now locks `width/height: 100%`, `object-fit: cover`, `max-width: none` and a zero-margin display: block with !important, so theme rules like `img { height: auto }` can't shrink the image and leak the placeholder gradient above it.
  - `.launchmind-post-meta` and its child spans (`.launchmind-post-date`, `.launchmind-post-author`) are pinned to `display: flex` / `inline-flex` with !important, so themes that blanket-set `span { display: block }` or inject `<br>` via an aggressive content filter can't break the single-line meta row. The "·" separator stays inline next to the author name on every theme.
  - Margins on the card's direct children (title, meta, excerpt, read-more) are reset to zero with !important so theme rules like `.entry-content > * { margin: 1.5em 0 }` can't bloat the vertical rhythm inside the card.
  - `::before` / `::after` pseudo-element injection on card, image wrapper and date/author spans is disabled from theme styles (using `content: none !important` where we don't render our own content), so theme decorations intended for the main content flow don't reach inside our cards.
* Typography and colour inheritance is unchanged — the card still picks up the site's font family, body colour and accent colour. Only layout-critical properties are hardened.
* No PHP, no database, no settings, no API contract changes. Drop-in upgrade for every existing 4.x install — existing shortcode attributes, merge mode, CPT rendering mode, sitemap, IndexNow and multilingual behaviour are untouched.

= 4.11.5 =
* Fixed: Styling regressions on sites running LiteSpeed Cache / WP Rocket / Autoptimize. The plugin's stylesheet was being pulled into a combined, minified, cached bundle that kept serving stale rules to end-users for hours after a plugin upgrade — on several Breakdance sites users reported "no tables, huge images, no heading hierarchy" even when the plugin was fully up to date. Two fixes land in 4.11.5:
  - `launchmind-blog.css` now ships with `data-no-optimize`, `data-no-minify`, `data-no-combine` attributes so LiteSpeed / WP Rocket / Autoptimize skip it and load it as a standalone `<link>`. Plugin upgrades are now reflected on the page the moment you refresh, no cache purge ritual required.
  - Added ~2 KB of critical inline CSS on Launchmind article pages covering the rules users notice if missing: table borders/padding, image max-width, heading sizes, article-body top padding for Breakdance-style floating brand logos. This loads inside the HTML itself, so even if the external stylesheet is stripped by an aggressive optimizer, tables and images still render correctly.

= 4.11.4 =
* Fixed: Tables broke on iOS Safari in CPT mode. 4.11.3 forced the <table> element to display:block + overflow-x:auto so wide tables wouldn't push the page sideways, but iOS Safari drops the row/cell layout when a table is block-level — users saw either a vertical list of cells or an unstyled block. The table now stays a real <table> and gets wrapped in a `<div class="lm-table-wrapper">` (injected server-side by Launchmind_Virtual_Filters) that carries the horizontal scroll instead. The wrapper even shows a subtle "content scrolls" shadow on narrow viewports so users know there's more off-screen.
* Fixed: Article headings fought with Breakdance's `.breakdance h2` rules. Both selectors have equal specificity (class + element), and the theme CSS loads after ours in the combined LiteSpeed bundle, so Breakdance's 36-48px mobile H2 sizes won and the article's heading hierarchy disappeared. All `.lm-article-body` heading font-sizes and margins are now marked !important so Launchmind's scale wins inside the article wrapper.
* Fixed: First heading of the article hidden behind La Casserole-style floating brand badges. Breakdance + custom site-logo layouts hang a circular logo down into the content column and the article's first H2 rendered behind it. Added 2rem default top padding on every `.lm-article-body`, 4rem on Breakdance sites, 5.5rem on mobile Breakdance — now the first heading always renders clear of the badge.

= 4.11.3 =
* Improved: Table visual contrast. The 4.11.x table CSS used such faint borders (rgba(0,0,0,0.1)) and zebra striping (0.015 alpha) that on light paginas the styling looked like it hadn't applied at all — borders were nearly invisible and alternating rows had no visible tint. Outer border is now 0.18 alpha, header bg 0.06, cell dividers 0.12, zebra 0.035, hover 0.06. Every visual property on the table/th/td rules is now marked !important so aggressive theme resets (Breakdance, block themes) can't zero them out.
* Fixed: Mobile layout overflow. Wide tables on narrow viewports could push the whole page sideways on Breakdance / page-builder templates because the table itself wasn't set as a scroll container. The .lm-article-body wrapper now has max-width:100% + word-wrap break, images/videos/iframes are constrained to 100%, and tables under 720px are forced to display:block + overflow-x:auto + max-width:100% with !important so the table scrolls horizontally on its own instead of the page.
* Improved: Compact table cell padding on mobile (10px 12px instead of 12px 16px) and 0.9rem font-size so 4-column data tables remain readable on 360-400px phones without being cut off.

= 4.11.2 =
* Improved: Mobile reading experience on CPT-rendered articles. The .lm-article-body wrapper now carries a typography baseline — 1.65 line-height, 17px body type, a proper heading scale (H2 1.65rem desktop / 1.4rem mobile), looser list spacing, and more generous paragraph rhythm. Previously on narrow screens the content picked up whatever font-size and line-height the theme used for generic page copy, which on several themes (Breakdance, Divi) produced a dense uninterrupted block of text with no visual hierarchy between paragraphs, bullet lists and headings.
* Improved: Inline links on mobile use a thinner underline with more offset so the first sentence of an article (which is often wrapped in a single long anchor because of citations) no longer visually dominates the opening paragraph.
* Added: Blockquote styling — subtle left border and italic body, only applied inside the Launchmind article wrapper so it never touches the theme's own pull-quotes or shortcodes.

= 4.11.1 =
* Fix: table/hr/image styling was flaky in CPT mode on sites where the theme's single-post template doesn't call body_class() — most visible on Breakdance where tables rendered as unstyled columns with no borders or padding. The CPT renderer now wraps the article content in a guaranteed `<div class="lm-article-body">` container, and all content CSS is keyed on that wrapper (plus the existing body-class and .lm-table fallbacks). The visual rules are marked !important on the critical properties so theme CSS resets can no longer strip the borders and padding from Launchmind tables.

= 4.11.0 =
* Changed: CPT rendering is now the default for new installs. The v4.10 beta flag is promoted to GA — on every fresh activation the plugin hands /blog/{slug}/ requests to WordPress' native template hierarchy, so your theme's header, footer, sidebar and SEO plugin head tags render automatically (classic themes, Full-Site Editing, Breakdance, Elementor Pro, Divi). Existing users who explicitly saved "Legacy" in 4.10.x keep that setting and can flip manually under Settings → Launchmind Blog → Rendering Mode.
* Fixed: Language propagation from the blog listing to individual articles. When a visitor viewed the English version of a multilingual site, clicking an article used to fall back to the site default locale (often Dutch) because the /blog/{slug}/ link carried no language hint. The shortcode now appends `?lang=xx` to each Launchmind post URL and also honours `?lang=xx` on the listing page itself, so the language the visitor sees in the listing carries through to the article they open.
* Improved: The Settings → Rendering Mode description now reflects CPT as the recommended default and labels Legacy as a backwards-compatibility option only.

= 4.10.2 =
* Fix: table rendering in CPT mode. The table / hr / inline-image styling was scoped to .launchmind-single-wrapper, which only exists in legacy mode. In CPT mode the theme renders the content directly via the_content() without the wrapper, so tables rendered borderless and columns ran into each other. CSS is now dual-scoped: legacy wrapper AND body.single-launchmind_article + .lm-table class, so styling applies in both modes.
* Improved: tables now have subtle zebra striping, a visible border and a header background for readability. Added a horizontal-scroll wrapper on screens below 720px so wide tables don't break narrow theme layouts.
* Improved: H2/H3 spacing — more breathing room between headings and the paragraph above, plus scroll-margin-top so jump-links land below sticky site headers (Breakdance, Elementor Pro, etc.).

= 4.10.1 =
* Hotfix: og:type/og:url/og:image emit was tied to the legacy single-post renderer, so sites on the new CPT rendering mode (beta) didn't get Launchmind-specific Open Graph tags in the head. The emitter now hooks globally and self-guards on the active post data — works in both legacy and CPT modes.

= 4.10.0 =
* **New (opt-in beta): CPT rendering mode.** Launchmind articles are handed to WordPress as a real custom post type (`launchmind_article`) on /blog/{slug}/ requests, so your theme's native template hierarchy (single-launchmind_article.php → single.php → singular.php → index.php) renders the page. Your theme header, footer, sidebar, and SEO-plugin head tags now render automatically — including on Full-Site Editing themes, Breakdance, Elementor Pro theme builder, Divi and other page-builder theme setups. Default is still 'legacy' (v4.x renderer) — enable under Settings → Launchmind Blog → Rendering Mode.
* The CPT is registered with public=false, show_ui=false and rewrite=false so it doesn't clutter the WP admin or conflict with other rewrite rules. Existing /blog/{slug}/ URLs keep working unchanged.
* Collision-safe: if another plugin has already registered `launchmind_article`, the refactor silently falls back to the legacy renderer.
* Rollback escape hatch: `define('LAUNCHMIND_BLOG_RENDERING_MODE', 'legacy');` in wp-config.php forces the legacy mode regardless of the Settings toggle.
* All existing features (merge mode, shortcodes, SEO coexistence, sitemap integration, IndexNow, language switcher, tag pills, custom-page detection) work unchanged in both modes.
* Pipeline: raw markdown horizontal rules (`---`, `***`, `___`) are now stripped from generated articles. H2 headings already structure content; the loose separators were noise that occasionally leaked through as literal text.

= 4.9.0 =
* **Single post layout refresh:**
  * Language switcher is now a minimal inline "NL / EN" next to the author in the meta bar, instead of a big centered mid-article block. Active language is highlighted in the accent colour.
  * Tag pills are visible again — the previous rule faded the whole element (including the text) with opacity: 0.15. Now uses a 12% accent tint on the background only, with full-opacity text and a subtle border.
  * Horizontal rules (`<hr>`) and images in the article body get proper spacing/styling (rounded corners, centered, max-width constrained) so they don't butt up against surrounding text.

= 4.8.0 =
* New: Page-builder detection on the /blog page. Supports Breakdance, Elementor, Divi, Beaver Builder, Brizy, Bricks, Oxygen and WPBakery.
* New: When a page builder is detected, both the Settings page and wizard step 3 show a tailored 4-step instruction (open builder → remove empty Post Loop / Post Grid → add builder-specific Shortcode element → paste [launchmind_blog] → save). The one-click "Add shortcode" button is hidden for these pages because page builders ignore post_content, so clicking it would modify stored data without affecting what the builder actually renders.
* Improved: Wizard step 3 now ends on "Open Settings & finish setup" (instead of Dashboard) and renders a dedicated "Next steps" card covering four situations — native posts page (merge will handle it, no action), already has shortcode (nothing to do), page builder (builder-specific instructions), plain custom content (one-click fix in Settings). Each case gets its own color-coded card so the admin always knows what the next click is.
* Improved: The ajax_wizard_save response now returns the resolved blog page id / title / edit URL / page-builder slug so the client UI can render precise next-step text.

= 4.7.0 =
* New: The Settings page now detects when your /blog page has custom content (Elementor, Divi, Gutenberg blocks, page-blog.php template, etc.) but is NOT the native WordPress posts page. In that situation, neither merge mode nor the standalone shortcode will actually inject Launchmind articles — the most common "plugin is installed but I see nothing on /blog" scenario.
* New: One-click **"Add [launchmind_blog] shortcode to this page"** button in the warning. Appends the shortcode to the end of the existing page content — your hero, heading and design blocks stay untouched. Idempotent: running it twice doesn't duplicate the shortcode.
* The warning also offers an "Edit page manually" escape hatch for admins who prefer to place the shortcode at a specific position inside their layout.

= 4.6.0 =
* New: Setup wizard step 3 now asks "Where should articles appear?" right after a successful connection. The correct option (merge for sites with existing posts, standalone for empty sites) is pre-selected based on wp_count_posts, and the choice is saved instantly without leaving the wizard.
* New: "Plain" permalinks warning in the welcome notice now has a one-click **Switch to "Post name" permalinks automatically** action — no more hunting for Settings → Permalinks to unblock /blog/{slug} URLs.
* New: Custom blog-template detection. When merge mode is on and the WordPress "posts page" uses a custom template (which often skips the main query), the welcome notice surfaces this and suggests the `[launchmind_blog include_wp_posts="true"]` shortcode as a fallback.
* All three additions are additive and dismissible — no existing setup breaks and admins who've already chosen a mode are not re-prompted.

= 4.5.0 =
* New: First-run welcome notice. When the plugin is activated, it auto-detects whether the site already has published posts and presents the admin with a clear "Merge with my existing blog" vs "Use separate /blog page" choice — highlighting the option that fits the site best. No more guessing during Launchmind's onboarding wizard.
* The notice is dismissible and can always be revisited under Settings → Launchmind Blog → Blog Integration Mode.
* Merge-mode cache is cleared automatically when the choice is made so the change takes effect on the next page load.

= 4.4.0 =
* New: **Blog Integration Mode** setting with two options:
  * **Standalone** (default): plugin creates its own /blog page with shortcode (existing behavior).
  * **Merge**: Launchmind articles automatically appear in your existing WordPress blog alongside your own posts, sorted by date. No shortcode needed — works with any theme.
* Merge mode uses WordPress `the_posts` filter to inject LM articles as virtual posts into the native blog loop. Themes render them identically to regular posts.
* SEO remains fully intact: canonical URLs, sitemaps, schema markup, Yoast/Rank Math integration all work unchanged.
* Settings page explains both modes clearly so customers can make an informed choice during setup.
* In standalone mode, shortcode `include_wp_posts="true"` (from 4.3.0) continues to work for mixed listings.

= 4.3.0 =
* New: The /blog listing now shows both Launchmind AI articles AND the site's own WordPress posts, merged and sorted by date. Customers can keep writing their own blog posts in wp-admin — they appear alongside AI articles on the same /blog page.
* New shortcode attribute `include_wp_posts="true"` (default: true). Set to "false" to show only Launchmind articles.
* WordPress posts use their native permalink; Launchmind articles keep the /blog/{slug} rewrite — no URL conflicts.
* Pagination now works across the combined list (LM + WP posts together).
* Debug mode shows LM vs WP post counts per page.

= 4.2.1 =
* Fix: Date and author in the blog-list meta row no longer jump to separate lines. The meta block is now rendered without newlines between tags so WordPress' wpautop() cannot inject `<br>` and `<p>` tags that broke the flex layout (most visible on Dutch sites where date + author rendered as two stacked lines with a gap instead of "13 April 2026 · Invorm").
* New: Shortcode UI strings — "Read more", "Previous", "Next", "Page %d", "Tags:", "By %s", pagination aria-label — are now native in every supported language (EN, NL, DE, FR, ES, IT, PL, HI). Previously these relied on the .mo/.po pipeline, so sites without the installed language pack saw the English strings regardless of the surrounding Dutch/German/French content.
* No database changes, no settings changes — this is a drop-in update.

= 4.2.0 =
* Improved: One-field setup — the connect wizard and Settings now ask for an API key only. The Site ID field has been removed everywhere because the backend authenticates on the API key alone; the Site ID field was always decorative and caused confusion during onboarding.
* Improved: API requests no longer send the legacy `site` query parameter.
* Backward-compatible: existing installations that have a saved `site_id` are not affected — the option is retained (and cleaned up on uninstall) but no longer used.

= 4.1.0 =
* Fix: Post links on the blog list no longer break when the page URL lacks a trailing slash. Links are now built from home_url(), which also bypasses an esc_url() quirk that prefixed bare slugs with http:// and produced links to nonexistent domains.
* Fix: /blog/{slug} URLs now keep working after a plugin update — rewrite rules are auto-flushed once per version, so no more manual "Save Permalinks" step.
* New: Welcome intro above the blog grid is now rendered in the site's language (EN, NL, DE, FR, ES, IT, PL, HI). Sites running an unsupported locale (ja, ru, sv, zh, …) fall back to English instead of showing Dutch by accident.
* New: Unsupported locales are normalised everywhere (list + single post + intro) so the Launchmind API never receives languages it doesn't serve.
* New: Admin notice when WordPress is configured with "Plain" permalinks, which silently breaks single-post URLs.

= 4.0.2 =
* Fix: Link previews on WhatsApp, LinkedIn, Facebook, Twitter, and Slack now show the correct article title and cover image instead of the parent page's metadata. Pre-4.0.2, SEO plugins (Yoast, Rank Math, AIOSEO) had no context for Launchmind post URLs and fell back to the metadata of the page that owned the `/blog/` path — resulting in previews saying "Kennisbank" or "Blog" instead of the actual article title.
* Fix: Added filters for `wpseo_title`, `wpseo_metadesc`, `wpseo_opengraph_title`, `wpseo_opengraph_desc`, `wpseo_twitter_title`, `wpseo_twitter_description`, `wpseo_twitter_image`. Equivalents for Rank Math and All in One SEO included.
* Fix: Added `pre_get_document_title` filter (WordPress core) so the browser tab title is correct even when no SEO plugin is installed.
* Fix: `og:image`, `og:image:width/height/alt`, and `twitter:image` are now emitted unconditionally from the Launchmind plugin — SEO plugins don't know about Launchmind's Supabase-hosted images and never emit them via their own `opengraph_image` filter, leaving link previews imageless. Emitting directly at `wp_head` priority 1 guarantees the image is in the head.

= 4.0.1 =
* Fix: No more duplicate `og:type`, `og:url`, `og:site_name`, or `og:image` tags when Yoast SEO / Rank Math / AIOSEO is active. The 4.0.0 release emitted these tags directly AND via SEO plugin filters, resulting in two copies in `<head>`. Yoast's "website" default was rendering before Launchmind's "article", so Facebook/LinkedIn link previews kept classifying posts as generic websites.
* Fix: Added filters for `wpseo_opengraph_type`, `rank_math/opengraph/facebook/og_type`, and `aioseo_og_type` so the SEO plugin emits `og:type=article` directly on Launchmind post pages.
* Fix: Added filters for `wpseo_opengraph_image` and `rank_math/opengraph/facebook/image` so the SEO plugin emits the Launchmind cover image instead of falling back to the site default.
* Improved: `emit_launchmind_head_tags()` now only emits `og:type`, `og:url`, `og:site_name`, `og:image` when no SEO plugin is detected — preventing duplication. Still always emits `og:image:width/height/alt`, `article:published_time`, `article:modified_time`, `article:author` because SEO plugins don't know Launchmind-specific article data.

= 4.0.0 =
* Major: Complete SEO rewrite — Launchmind now coexists with your existing SEO plugin instead of fighting its output. Works cleanly with Yoast SEO, Rank Math, All in One SEO, The SEO Framework, and SEOPress.
* Fix: Canonical URL now respects the WordPress permalink trailing-slash setting via `user_trailingslashit`. No more canonical mismatches or 301-redirect loops in Google Search Console.
* Fix: Removed duplicate canonical tags — the pre-4.0 code injected canonical twice via wp_head and output-buffer regex. Now emitted once via standard SEO-plugin filters.
* Fix: `<title>` and `<meta description>` are no longer overridden when a SEO plugin is active — your Yoast/Rank Math templates are respected. Launchmind only overrides canonical, og:type=article, og:image, hreflang, and JSON-LD Article schema.
* Fix: `/blog/sitemap.xml` and `/wp-sitemap.xml` now emit trailing-slash URLs matching the canonical.
* New: Hreflang `<link rel="alternate">` tags emitted at wp_head priority 1, ahead of SEO plugin hreflang output, so Google correctly identifies language variants.
* New: Article JSON-LD schema moved to wp_footer to avoid conflicts with SEO plugin schema blocks in the head.
* New: Emergency escape hatch — add `define('LAUNCHMIND_SEO_LEGACY_MODE', true);` to wp-config.php to revert to pre-4.0 behavior if a customer site reveals a conflict. Will be removed in 4.1.
* Removed: Output buffer regex-stripping of canonical/og:title/og:url/og:description/og:image tags. No longer mutating the HTML stream — works on any host, any cache plugin.
* Removed: `remove_action` calls that forcibly unhooked Yoast/Rank Math/AIOSEO wp_head tags. We now coexist.
* Net: ~170 lines removed, ~250 lines added across 3 new classes. Smaller attack surface, dramatically better compatibility.

= 2.11.0 =
* New: Pagination on the blog overview shortcode — visitors can now browse through all published articles with prev/next links
* New: `lm_page` query parameter for pagination state (avoids conflict with WordPress's reserved `page` var)
* New: Pagination styling with full dark/light/auto theme support and mobile-responsive layout
* Improved: Blog list shortcode fetches one extra post to detect next page without needing API total count

= 2.9.0 =
* New: Setup wizard for first-time configuration — 3-step guided flow to connect your site
* New: Yoast SEO sitemap integration — articles appear in Yoast's sitemap index
* New: Rank Math sitemap integration — articles appear in Rank Math's sitemap index
* New: All in One SEO sitemap integration — articles appear in AIOSEO's sitemap index
* Fix: WordPress native sitemap (wp-sitemap.xml) now works reliably with all caching plugins

= 2.8.0 =
* New: XML Sitemap at /blog/sitemap.xml — all published articles are now discoverable by search engines
* New: WordPress native sitemap integration — articles appear in wp-sitemap.xml automatically
* New: IndexNow integration — new articles are pinged to Bing and Google for fast indexing
* New: Sitemap URL automatically added to robots.txt
* Improved: IndexNow key verification served via WordPress rewrite (works on all hosts)
* Fix: Clean up all plugin options and transients on uninstall

= 2.7.0 =
* New: Hreflang tags for multilingual SEO — Google now correctly identifies language versions of articles
* Fix: Hreflang only generated for languages with actual content (prevents soft 404s and indexing issues)
* Fix: Version constant synced with plugin header (was 2.5.0, header was 2.6.0)

= 2.6.0 =
* New: Track all link clicks, not just external links
* New: Internal clicks to client website pages (e.g., /services/, /contact/) are now tracked
* New: target_url field in click data for better analytics
* Improved: Skip tracking for blog-to-blog navigation

= 2.5.0 =
* New: Analytics tracking - page views and clicks are now tracked for your Launchmind dashboard
* New: AI traffic detection - see how many visitors come from ChatGPT, Claude, Perplexity, etc.
* New: Organic vs AI traffic breakdown in your partner dashboard
* Privacy: All tracking is anonymous (no cookies, no PII)

= 2.4.1 =
* Fix: Language switcher now only shows available languages for each article
* Fix: Language switcher positioned between intro and main content for better UX
* Improved: Backend API now returns availableLanguages array to reduce API calls
* Improved: Language switching now works correctly - clicking a language updates the content
* Improved: Better visual styling for inline language switcher

= 2.4.8 =
* Hotfix: Output buffering catches canonical URLs and meta tags added by themes/plugins
* Fix: Removes incorrect `<title>` tags that contained "Kennisbank"
* Fix: Multiple regex patterns handle canonical/OG variants with different spacing and quoting
* Improved: Head section processed separately for more reliable tag replacement
* Improved: Comprehensive error handling to prevent site crashes

= 2.4.7 =
* Fix: Aggressive HTML filtering to force correct canonical URL and og:title tags
* Fix: Output buffering with safety checks to override theme/plugin meta tags
* Improved: Multiple layers of protection (wp_head hooks + HTML filtering)
* Improved: Only processes Launchmind blog posts (no impact on other pages)
* Improved: Works even when caching plugins have active output buffers

= 2.4.6 =
* Fix: Link previews now show correct article title instead of site/page title
* Fix: Canonical URL now correctly points to article instead of parent page
* Improved: Open Graph tags override SEO plugin tags to ensure correct previews
* Improved: Image dimensions (1200x630) added for better LinkedIn/Facebook previews
* Improved: Image fallback system (coverImage → first image in content → site icon)

= 2.4.5 =
* New: Added Launchmind logo as plugin icon for WordPress.org directory
* Improved: Plugin displays branded icon in WordPress admin and plugin directory

= 2.4.4 =
* New: Added comprehensive SEO meta tags (meta description, canonical URL)
* New: Added Open Graph tags for social sharing and AI engines
* New: Added Twitter Card tags for better social previews
* New: Added Schema.org JSON-LD structured data for Google and AI engine indexing (ChatGPT, Claude, Perplexity)

= 2.4.3 =
* Improved: Removed specific company references from example descriptions for cleaner documentation

= 2.4.2 =
* Improved: base_url parameter now prominently displayed as first option in Settings documentation
* Improved: Better organization of shortcode options (Essential vs Styling options)

= 2.4.0 =
* New: Multi-language support with language switcher on single post pages
* New: Hindi (hi) language support added
* New: Automatic language detection from WordPress locale
* New: Language switcher displays available languages with flags (EN, NL, DE, FR, ES, IT, HI)
* New: Optional automatic dark/light mode detection (opt-in via theme="auto")
* Improved: Backward compatible - existing sites unchanged unless explicitly opting in
* Improved: Enhanced Settings page with comprehensive layout options documentation
* Improved: Better dark mode support with opt-in automatic detection
* Fixed: Dark mode only activates when explicitly enabled (prevents breaking existing custom CSS)

= 2.3.1 =
* Fixed: Markdown tables are now properly converted to HTML tables
* New: Beautiful table styling with responsive design
* New: Tables support header rows and proper cell alignment
* Improved: Tables scroll horizontally on mobile devices

= 2.3.0 =
* Major: Complete CSS rewrite for better website integration
* New: Plugin now inherits website typography and colors automatically
* New: `accent_color` parameter - use preset (teal, blue, gold) or hex (#00a69c)
* New: CSS variables can be overridden by website theme
* Improved: Cards use `inherit` and `currentColor` for seamless integration
* Improved: Cleaner, more neutral styling that adapts to any website
* Changed: Default columns from 4 to 3 for better readability

= 2.2.8 =
* Improved: Updated Settings documentation with all shortcode options
* Improved: Added dark theme example shortcode in Settings
* Improved: Reduced spacing between intro and blog grid
* Improved: Better documentation for theme, base_url, show_intro options

= 2.2.7 =
* New: Adaptive dark/light theme support - cards auto-detect theme
* New: `theme="dark"` or `theme="light"` shortcode attribute to force mode
* Improved: Reduced spacing and margins for cleaner look
* Improved: Cards now use semi-transparent backgrounds with backdrop blur
* Improved: Better mobile responsiveness

= 2.2.6 =
* Fix: Added safety checks to prevent fatal errors on activation
* Fix: Wrapped API calls in try-catch for error resilience
* Fix: Class existence checks before calling methods

= 2.2.5 =
* Fix: Resolved critical error caused by template handler
* Fix: Improved PHP 7.4 compatibility
* New: Automatic blog page creation on activation
* New: SEO-friendly URL structure for single posts (/blog/post-slug)

= 2.0.1 =
* Fix: Removed duplicate Plugin URI to comply with WordPress.org guidelines
* Fix: Assets excluded from plugin zip
* Changed: Branding disabled by default

= 2.0.0 =
* New: Modern admin dashboard with tabs, cards, and preview modal
* New: Gutenberg blocks (blog list + single post)
* New: Analytics tab with improved settings
* New: WordPress.org compliant readme
* Changed: Branding now defaults to off (opt-in only)

= 1.0.0 =
* Initial release with shortcodes, cache, and API client

**External Service**

This plugin connects to the [Launchmind.io](https://launchmind.io) API to retrieve blog content. A Launchmind account and API key are required for the plugin to function.

* [Launchmind Terms of Service](https://launchmind.io/terms)
* [Launchmind Privacy Policy](https://launchmind.io/privacy)

When using this plugin, blog content is fetched from Launchmind servers. No personal user data from your WordPress site visitors is sent to Launchmind. Only the API key and content requests are transmitted to authenticate and retrieve your blog posts.

== Installation ==

1. Upload the plugin to `/wp-content/plugins/launchmind-blog`.
2. Activate the plugin through the Plugins menu in WordPress.
3. Navigate to the Launchmind menu item (left sidebar) and enter your API key.
4. Add a Gutenberg block (`Launchmind Blog`) or use the shortcode `[launchmind_blog]` on any page.

== Testing ==

**For WordPress.org Reviewers:**

A demo API key is available for testing purposes:

`lm_test_demo1234567890`

This key provides access to 6 sample blog articles for testing the plugin functionality.

**Test the connection:**

1. Install and activate the plugin
2. Go to **Launchmind** in the WordPress admin sidebar
3. Click the **Settings** tab
4. Enter the demo API key: `lm_test_demo1234567890`
5. Click **Save Settings**
6. Click **Test Connection** - should show "Connection successful"
7. Go to the **Articles** tab to see 6 sample posts
8. Add the Gutenberg block (`Launchmind Blog`) or use shortcode `[launchmind_blog]` on any page

**Sample articles included:**

* 10 Tips to Boost Your E-commerce Conversion Rate
* The Ultimate Guide to SEO for Online Stores
* How to Create a Winning Product Description
* Email Marketing Strategies That Actually Work
* Social Media Marketing for Small Businesses
* Customer Retention: How to Keep Customers Coming Back

**Getting your own API key:**

Sign up at [launchmind.io](https://launchmind.io) to get your personal API key for production use.

== Frequently Asked Questions ==

= Where do I find my API key? =

Log in to your [Launchmind Dashboard](https://launchmind.io/backlinks/dashboard) and navigate to the "Webshop Plugins" tab. Your API key is displayed in the green box.

= Can I use shortcodes instead of Gutenberg blocks? =

Yes! Use `[launchmind_blog limit="6" columns="3"]` for a blog list or `[launchmind_post slug="your-post-slug"]` for a single post.

= Does this plugin track my visitors? =

No. This plugin only communicates with Launchmind servers to fetch your blog content. No personal data from your site visitors is collected or transmitted.

= What data is sent to Launchmind? =

Only your API key and content requests (such as post slugs and language preferences) are sent to authenticate and retrieve your blog posts. No visitor data is transmitted.

= Is the "Powered by Launchmind" branding required? =

No. The branding is disabled by default. You can optionally enable it in the block settings or shortcode attributes if you wish to display it.

== Screenshots ==

1. Modern admin dashboard with article overview
2. Gutenberg blog list block in the editor
3. Single post block displaying article content

== Privacy Policy ==

This plugin connects to external Launchmind.io servers to fetch blog content. By using this plugin, you agree to Launchmind's [Terms of Service](https://launchmind.io/terms) and [Privacy Policy](https://launchmind.io/privacy).

**Data transmitted:**
* Your Launchmind API key (for authentication)
* Content requests (post slugs, language preferences)

**Data NOT transmitted:**
* Personal data of your WordPress site visitors
* WordPress user information
* Any other site data

All API communication uses HTTPS encryption.

== Upgrade Notice ==

= 2.0.0 =
Major update with new Gutenberg blocks and dashboard. Please verify your API key is still configured after updating.
