=== PublisherKits SocialPromoter ===
Contributors: publisherkits
Tags: social media, twitter, linkedin, facebook, post scheduler
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.54.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Post and schedule social media promotions from WordPress Posts directly to X, LinkedIn, Facebook and Bluesky.

== Description ==

SocialPromoter by PublisherKits lets publishers post and schedule social media content from WordPress Posts directly to X (Twitter), LinkedIn, Facebook Pages and Bluesky — without leaving the editor.

**Core features (free — everything in this plugin is fully functional):**

* Write social copy with a live character counter (280-character X warning)
* Click post tags to insert as hashtags
* Post Now — fires immediately to all selected accounts
* Schedule — pick a date and time, WP Cron handles the rest
* Queue view — see all upcoming scheduled posts and cancel any
* Post history — the full, paginated log of everything sent, with errors
* Multiple accounts per platform — ideal for agencies and multi-brand publishers
* Brand hashtags — standing tags blended into every post

**Pro features — supplied in a separate package; their implementation is not included in this WordPress.org build:**

* Engagement statistics — likes, comments, shares and clicks on sent posts
* Campaigns — group posts, UTM-tag every link, PDF/GA4 client reports
* Automation — new-post suggestions, reposting and story boosts
* AI copy drafting — tone, goal and technique variants
* Branded images — platform-sized canvases with your logo and headline bar
* WP Job Manager support — post and schedule from job listings
* Author tagging — writers' handles and hashtags suggested automatically, stored in BioBuilder-compatible fields so the two stay in sync
* Assisted posting — for places that will not accept a post from an app, LinkedIn and Facebook groups among them. SocialPromoter does not post to those on your behalf and neither does anything else: both platforms closed group posting to third-party apps. What it does instead is everything up to the send — writes the copy, prepares the image, holds it until the moment you chose, then reminds you and hands it over with one-click copy and a link straight to the destination. You paste; it records that you posted it

**Supported platforms:**

* X (Twitter) — OAuth 1.0a, posts via API v2
* LinkedIn — personal profiles and company pages
* Facebook — pages (requires Facebook developer app)
* Bluesky — app-password login, posts with clickable links

X and Facebook use your own developer app credentials. LinkedIn uses the shared PublisherKits relay app so company-page posting can use the approved LinkedIn Community Management API access.

= More from PublisherKits =

SocialPromoter is one of a small range of tools built for people who actually run publications. Each works on its own, and all are free on WordPress.org:

* [PublisherKits BioBuilder](https://wordpress.org/plugins/publisherkits-biobuilder/) - stop leaving author profiles blank. When a post is published, BioBuilder finds the author's professional profile and fills in their bio, social handles, job title and employer.
* [PublisherKits ExcerptBuilder](https://wordpress.org/plugins/publisherkits-excerptbuilder/) - AI-powered excerpt generation using the WordPress AI Client. Write, rewrite or tidy excerpts on demand, or auto-draft them on publish for review.

= External Services =

This plugin connects to the following external APIs when you have configured credentials and connected accounts:

* **X API** (api.twitter.com and upload.twitter.com) — for OAuth authentication, posting tweets and uploading post images. Your post text, link and image are sent when you post; your app credentials are sent when authenticating. [X Developer Terms of Service](https://developer.twitter.com/en/developer-terms/agreement-and-policy) | [X Privacy Policy](https://x.com/en/privacy)
* **Facebook Graph API** (graph.facebook.com) — for OAuth authentication and posting to Facebook Pages. Your post text, link and image are sent when you post; your app credentials are sent when authenticating. [Meta Platform Terms](https://developers.facebook.com/terms/) | [Meta Privacy Policy](https://www.facebook.com/privacy/policy/)
* **LinkedIn API** (api.linkedin.com) — for OAuth authentication and posting shares. Your post text, link and image are sent when you post. [LinkedIn API Terms of Use](https://legal.linkedin.com/api-terms-of-use) | [LinkedIn Privacy Policy](https://www.linkedin.com/legal/privacy-policy)
* **Bluesky API** (bsky.social) — for app-password authentication and posting. Your post text, link and image are sent when you post. [Bluesky Terms of Service](https://bsky.social/about/support/tos) | [Bluesky Privacy Policy](https://bsky.social/about/support/privacy-policy)
* **PublisherKits LinkedIn connect relay** (pkits.org) — LinkedIn connections are authorised through a shared PublisherKits developer app, so you do not need your own LinkedIn developer app. The relay is operated by us (PublisherKits, Black and White Trading Ltd) and is contacted only during the connect step: when you click Connect, your browser is sent to LinkedIn to sign in, and your site then posts a one-time ticket, a nonce and a signature to the relay to complete the OAuth handshake and receive the LinkedIn access token, which is stored encrypted on your own site. No post content, personal data or analytics is ever sent to the relay; posts go directly from your site to api.linkedin.com. [PublisherKits Terms of Service](https://publisherkits.com/terms/) | [PublisherKits Privacy Policy](https://publisherkits.com/privacy-policy/)

* **PublisherKits Support Copilot service** (bwtlsupport.com) — optional in-plugin support assistant, the Help button on SocialPromoter admin screens. When you use it, your question and recent chat history are sent, together with your site address and (where available) its licence installation identifier used to authorise the request and apply fair-use limits, the plugin version and those of other installed PublisherKits plugins, the admin screen name, and a short whitelist of non-sensitive settings and counts (which social platforms have a connected account, how many accounts and how many queued, sent and failed posts exist, and five on/off settings covering reminders, queue auto-cancel, debug logging and uninstall clean-up). No account names or handles, access tokens, API keys, post copy or scheduled-post content are ever sent, and the assistant is read-only — it cannot change anything on your site. Nothing is sent until a user who can manage this plugin's settings accepts the in-widget privacy notice; consent can be withdrawn on the Support screen. Ordinary questions are not retained after they are answered; only if you choose "Send to support" is the conversation kept, to become a support ticket. [PublisherKits Terms of Service](https://publisherkits.com/terms/) | [PublisherKits Privacy Policy](https://publisherkits.com/privacy-policy/)

No data is sent to these services until you have configured app credentials and connected accounts. Posting only occurs when you explicitly click Post Now or approve a scheduled post. The Support Copilot sends nothing until its privacy notice is accepted.

This plugin also uses the **Freemius SDK** (freemius.com) for licensing, updates and optional usage diagnostics. No data is sent to Freemius unless you explicitly opt in on the activation screen — skipping the opt-in keeps the plugin fully functional. [Freemius Terms of Service](https://freemius.com/terms/) | [Freemius Privacy Policy](https://freemius.com/privacy/)

This free plugin makes no AI requests: AI copy drafting belongs to the separately distributed Pro version, which uses the AI provider the site configures in WordPress core's Settings > Connectors, under that provider's own terms and privacy policy.

== Installation ==

1. Upload the plugin folder to `/wp-content/plugins/`
2. Activate via Plugins in the WordPress admin
3. Go to **PublisherKits > SocialPromoter** to configure your platform credentials
4. Add the callback URL shown on the Accounts page to your developer app settings on each platform
5. Click **Save & Connect** to authorise posting

== Frequently Asked Questions ==

= Do I need developer accounts for each platform? =

You register developer apps for X and Facebook. LinkedIn uses the shared PublisherKits relay app, so no local LinkedIn developer credentials are required in the plugin.

= What is the callback URL? =

It is shown at the top of the Accounts page. Add it to the Redirect URIs / Callback URLs in each platform's developer portal before connecting.

= Does this work on shared hosting? =

Yes. Scheduled posts use WP Cron, which works on cPanel shared hosting. Posting runs as soon as WP Cron fires after the scheduled time, typically within a few minutes of a site visit.

= Can I connect multiple X accounts? =

Yes — you can connect multiple accounts per platform (e.g. separate X accounts for each of your brands).

== Screenshots ==

1. Compose: pick any published post, choose X, LinkedIn and Facebook, write your copy, and schedule or post now.
2. Activity: see every scheduled post across your platforms at a glance, and cancel any before it sends.
3. Connected Platforms: connect X, LinkedIn, Facebook and Bluesky, and manage all your accounts in one place.
4. Settings: control the publish reminders, the post types they apply to, the roles that see them, and your default post copy.

== Changelog ==

= 1.54.0 =
* Security: The Support Copilot now checks who is asking as well as where the request came from. Its admin requests were guarded by a single check that proves a request originated from a page the plugin rendered — which is not the same as proving the person sending it is allowed to make it. Those are now two separate checks, and both must pass. Nothing changes in normal use and no action is needed.
* New: An AI assistant can now read this site's profile. If you use an assistant that speaks MCP, it can ask the site for the publication's own context — what you cover and who you publish for — so it works from the same description SocialPromoter already uses when it writes copy. It is read-only, it is limited to users who could see that information in the admin anyway, and it needs WordPress 6.9 or later. On earlier WordPress nothing changes and nothing breaks.
* Fix: Clearing a field on the site profile now really clears it. Emptying the site context used to leave the previous wording behind in the older shared setting, and that copy kept feeding AI prompts — so a publication that had rewritten its description could find the old version still shaping generated posts. One safeguard is deliberately kept: an empty value cannot wipe a complete older profile on a site whose profile has never held that field.
* New: Generated copy is now framed by the publication's highest-value audience where the site profile records one, so posts are pitched at the readers that matter most rather than at a general audience.
* Fix: Longer site profile notes are no longer flattened onto a single line each time they are saved.
* Fix: The shared profile no longer carries a site name it did not need, and is de-identified before it is passed between plugins.
* Update: Bundles PublisherKits Core 0.8.4 to 0.9.16. The shared Site Profile is mirrored onto the older shared options, so other PublisherKits plugins that have not moved to the core profile keep reading the same values. On a site running SocialPromoter alongside another PublisherKits plugin, this becomes the active core for both.

= 1.53.1 =
* Fix (Pro): The Assisted posts screen now uses SocialPromoter's own styling rather than plain WordPress form styling, so it matches the rest of the plugin.
* Fix (Pro): On the Assisted posts screen, the destination's name field was labelled only "Name", which could be read as asking for the post's title. It now says what it wants: the name of the group or community you are posting to. The other fields on that form gained short explanations too.
* Fix (Pro): The composer now shows the assisted destinations card even before you have added any, with a link to add one. Previously the card was hidden entirely until a destination existed, so the screen where you decide where a post goes never mentioned that assisted destinations were an option.

= 1.53.0 =
* New (Pro): Assisted posting, for the places a plugin is not allowed to post to. LinkedIn closed its groups API to third parties, and Meta removed group posting from every version of its API in April 2024, so no plugin can post to a LinkedIn or Facebook group for you — ours included. Assisted posting does everything else: add the group as a destination, schedule to it from the composer exactly as you would to a connected account, and SocialPromoter prepares the copy and the image and holds it until the moment you chose. Then it tells you it is ready, gives you the text on one click and a link straight to the group, and records that you posted it by hand. It can also suggest recent posts you have not offered to a group yet, and you can wave away one suggestion or all of them. Your history says a person posted it, never that SocialPromoter did, and there are no engagement figures for these — no API returns them, so any number would be invented.
* Fix: The Social panel in the post editor could show a blank status line, and hide a real one. A group of sends with nothing left to report — after cancelling a scheduled post, for instance — was still treated as the most recent activity, so it displaced the "Posted to X, LinkedIn" line for a post that really had gone out. Sends with nothing to report are now skipped, and a post with no reportable activity says so plainly.
* Fix (Pro): Campaign objective progress no longer counts posts that never went out. The channel report worked out which posts belonged to which campaign objective by looking for a send timestamp, but that timestamp is written whenever a post reaches its final state - including when a send fails - so posts that never reached the platform were being counted towards the objective as though they had. Skipped assisted posts would have counted the same way. Only posts that actually sent are counted now. **Existing campaigns may show a lower figure than before, and in some cases noticeably lower.** Nothing has been lost and no data has changed: the earlier number was overstating what went out, and the new one is the true count.
* Fix: Two SocialPromoter screens were rendering without the plugin's own styling. Admin styles and scripts were loaded from a hand-maintained list of screens that had to be edited whenever a screen was added, and twice it was not — the Support screen (added in 1.52.0) and the Pro Channel report have both been showing default WordPress form styling rather than SocialPromoter's, with the SP badge in the heading collapsing to plain text. The list is gone; the plugin now works out its own screens automatically, so this cannot happen again as screens are added.
* Fix: The "Show to user roles" setting under Settings now does what it says. It has always been saved, but nothing read it, so the promote-this-post reminder and the amber unpromoted banner appeared for every user who could edit the post regardless of which roles were ticked. Sites that never changed the setting are unaffected: the default is still administrators and editors.

= 1.52.2 =
* Fix: SocialPromoter could clear the site's primary goal or audience description held in the shared PublisherKits site profile. Saving SocialPromoter's settings sent both values even when the form only carried one, so whichever field was absent was blanked - overwriting a value set in another PublisherKits plugin. SocialPromoter now sends only the fields the form actually carried, and will no longer clear a value that is already set. Clearing a goal or audience is done where it is edited.

= 1.52.1 =
* Security: the Support Copilot now sends its requests through WordPress's protected HTTP function, which validates the destination and refuses private, loopback and link-local addresses — including on redirects. The support endpoint is an administrator-editable setting, so this closes off the possibility of it being pointed at internal infrastructure. Recommended for all users.
* Security (Pro): Google Analytics credentials — the service account JSON, and the OAuth client secret and refresh token where they are used — are now encrypted before they are stored, so they no longer sit in plain text in database exports and backups. Existing connections keep working: the stored values are re-encrypted automatically the first time they are read, and nothing needs to be re-entered or reconnected.

= 1.52.0 =
* New: Support Copilot — a Help button on SocialPromoter admin screens that answers usage and setup questions in a popup chat, grounded in documentation matched to your installed version. It is read-only: it can explain, but it can never change anything on your site. Manage it under SocialPromoter > Support. Nothing is sent to the support service until a user who can manage this plugin's settings accepts the in-widget privacy notice, and consent can be withdrawn at any time. Your connected account names and handles, access tokens, post copy and scheduled posts are never sent. See the External Services section for the full disclosure.

= 1.51.13 =
* Automated posting now spreads posts published close together across your posting window, instead of sending them all at once. The minimum gap was previously measured only from the last post that had actually been sent, so several articles published within a few minutes of each other could each be given the same slot and go out together.

= 1.51.12 =
* Facebook: image posts are now published explicitly as Page feed posts — the image is uploaded first, then attached to a feed post. This is a clearer publishing path and returns a proper post ID for engagement statistics.
* Branded images (Pro): the automatic crop now keeps faces in frame, anchoring to the head with headroom above it so crowns are no longer clipped on portrait photos.

= 1.51.11 =
* Fixed the History screen capability so it and its manage actions require the same permission (administrator), instead of appearing for editors who would then be refused.
* Hardened the Compose screen's JavaScript: AJAX result text and all preview image URLs are now inserted as text/attributes rather than built into HTML strings.
* Removed the last vestigial licence-flag branches and comments from the free build; upgrade notices render through a small, clearly-named helper.
* Tidied several stale comments and one External Services wording detail.

= 1.51.10 =
* Post history is now a full, paginated free feature: a dedicated History screen lists every sent post and any failures, grouped by send, with per-send error messages and delete/clear-all. Engagement statistics on that screen remain a Pro feature. (Previously the free build showed only the most recent sends.)
* Removed the Pro licence flag from the free package entirely (it was already unused there); the upgrade notices simply always show, since this is the free tier.
* Removed the remaining branded-image preview fields and hooks from the free Compose screen; an add-on now enhances the preview through a neutral event.
* Minor: readme description and the accounts screen no longer mention Pro-only surfaces; tidied a few stale comments.

= 1.51.9 =
* Review-hardening: the bundled PublisherKits core library's admin strings now use this plugin's text domain in the shipped build; the plugin's own Freemius bootstrap helpers and admin script globals were standardised to the plugin's pkits_sp / pkitsSp prefix; and the free build no longer references the Pro licence flag at all (upgrade notices show unconditionally, since this package is the free tier).
* Compose: added a native Bluesky post preview (300-character layout) instead of falling back to the X preview; moved the last branded-image preview labels into the premium package.

= 1.51.8 =
* Fixed a fatal JavaScript syntax error in the free admin script left by the previous build's code separation; every queue and posting behaviour it carries works again, verified by parser check on the shipped file.
* Fixed a Compose script error when clearing a selected post (a variable owned by the premium author-tagging module leaked into the free script).
* Completed the free/Pro separation the previous passes missed: the History screen's row-management script, the signposting content-class subsystem and its term-search endpoint, the topic-balance scorer, the AI prompt-framing helper, the queue-item delete endpoint (premium History is its only caller), the branded-image font, and the Pro campaign-editor media screens now all live only in the premium package.
* Brand hashtags' Fallback mode is now honoured on the free send path (skips posts already carrying two or more of their own hashtags).
* External Services now discloses upload.twitter.com (X image uploads); Bluesky wording corrected to "posts with clickable links"; settings copy no longer describes AI behaviour the free build does not have; the LinkedIn relay URL is written plainly in code.

= 1.51.7 =
* WordPress.org review compliance, final pass: the Compose screen's remaining Pro scaffolding (author-tags container, image-headline editor script, branded-image fit notes, campaign field wiring) and the Pro admin script behaviours (engagement-stat refresh, clear-all-history, author-tag insertion) now live in separate premium-only files; the free package's JavaScript references only AJAX actions the free package registers.
* Removed the Pro campaign link and page scaffolding from the free Accounts screen and admin loader.
* Security: publishing from Compose now requires the publish_posts capability plus per-post edit permission, and the publish-reminder status endpoint now checks per-post edit permission.
* Fixed: "Delete all failed" on the queue screen was rejected by its own handler; the delete-all sentinel now works.
* Renamed the private crypto class to the plugin prefix (PKITS_SP_Crypto) and removed its class_exists wrapper.

= 1.51.6 =
* WordPress.org review compliance, third pass — the free/Pro boundary is now implementation-level, not gate-level. Removed from the free build: the per-platform engagement-stat fetchers inside each connector (and the Facebook read_insights opt-in and OAuth scope), the "Boost this story" metabox controls and save handling, the reporting/GA4 configuration store, the analytics read API, the campaign/stats/campaign-link database tables and their migrations, automation-suggestion storage and approval surfaces, the full-history links and clear-history action, and the Compose screen's AI drafting, campaign and author-tagging scaffolding (now separate premium files).
* The free build's licence flag is now a plain false constant: nothing is consulted and nothing can be unlocked, because no premium implementation exists in the package.
* Brand hashtags now genuinely blend on the free build's sends (previously the blending only ran in Pro); modes and per-platform caps are honoured ('fallback' applies when a post carries fewer than two of its own hashtags).
* The plugin description and platform list now describe the free plugin only, and include Bluesky.

= 1.51.5 =
* WordPress.org review compliance, second pass: every Pro feature is now delivered as separate premium-only code that is physically absent from the free build — engagement statistics (class, scheduled refresh, queue display and refresh actions), WP Job Manager support (now a Pro integration module the free code exposes filters for), AI copy drafting (client, prompt library, settings and defaults), the GA4 connection, channel-report and PDF/UTM settings and their save handling, and the metabox author-tagging helpers. The free build no longer contains any licence-gated functionality, and the licence flag no longer carries a force override there.
* Multiple accounts per platform is now correctly listed as a free feature (the code never limited it), and brand hashtags are listed free too. Engagement statistics move to the Pro list, matching the code.
* Removed the remote Google Fonts stylesheet from admin screens; the UI now uses bundled system font stacks (no external requests).
* Fixed a Compose-screen script error: a PHP constant leaked into JavaScript as an undefined name when a post was selected.
* WordPress.org review compliance: the Pro branded-image pipeline (resizing to platform canvases, logo watermark, headline bars) no longer ships in the free build in any form. The image code is split into a free handler (fetch, validate, clean up - free posting is unchanged and posts images exactly as supplied) and a separate Pro-only brander that is physically removed from the free build, along with its settings panel, live preview, save handling and Compose headline editor.
* Fixed: the Compose screen could fatal on the free build (an unguarded reference to the Pro AI copy class).
* Fixed: the Brand hashtags settings (a free feature) were only saved when Pro was active; they now save on the free build too.
* The AI connector diagnostic no longer reads any core AI credential option; it only reports whether a connector's provider plugin is active.
* External Services: the PublisherKits LinkedIn connect relay entry now spells out exactly what is sent (a one-time ticket, nonce and signature during the connect step - never post content or personal data) with terms and privacy policy links, and every platform API entry now carries both terms and privacy links.
* Escaping: admin-ajax URLs printed into inline scripts now use esc_url (output escaping) rather than esc_url_raw.

= 1.51.4 =
* Facebook: new optional "Show link-click counts" setting on the Platforms page. Tick it (after adding the read_insights permission to your own Facebook app - available at Standard Access for Pages you administer, no App Review) and reconnect the Page to see link clicks on sent posts, alongside likes and comments.
* Facebook: post impressions and reach are no longer requested - Facebook removed those metrics from its API for all apps, so they cannot be shown. Requesting them alongside the working metrics was also occasionally stopping likes, comments and clicks from appearing; that is fixed.
* Updated the on-screen stats notes so they describe what each platform actually provides today.

= 1.51.3 =
* Fixed: campaign report PDFs could fail on sites running another PDF-generating plugin. The bundled PDF library now ships its complete font set, always uses its own fonts, and a PDF failure now shows a clear error instead of a broken page. The PDF library is no longer included in the free build (campaign reports are a Pro feature).

= 1.51.2 =
* Fixed: two features had silently never worked - the author tagger's curated-topic candidates and the channel report's topic axis both called a Classifier method that never existed, so they always fell back to tags/taxonomy. Both now read the shared PublisherKits topic framework, so they work on every site with topics set up (deterministic everywhere, AI where the Classifier runs).

= 1.51.1 =
* The "Brand voice / extra context" setting is now the shared PublisherKits Site Profile context: one value, editable here, on the Classifier's Configuration screen, or under PublisherKits > Site Profile - used by every PublisherKits plugin. AI copy reads the shared profile first (older storage remains as a fallback), and the publication context now appears exactly once per AI prompt.
* SEO keywords: when none are set here, AI copy falls back to the shared Site Profile's keywords.

= 1.51.0 =
* New: SocialPromoter now bundles the PublisherKits Core Library — the shared site profile and topic framework used across the PublisherKits suite. Fill in the Site Profile and Topics once (Site Profile menu) and every PublisherKits plugin, including SocialPromoter, uses the same answers.
* New: automated reposting now balances topics. The repost picker gently prefers posts from topics your social channels have under-served recently, so the auto-queue stops fixating on one corner of your site. Requires the site topic framework to be set up; behaviour is unchanged without it.
* Improved: AI copy drafting is grounded in the shared Site Profile when populated (sector, house style, publication context), in addition to the audience and goals framing it already used.

= 1.50.1 =
* Compatibility: admin CSS and JavaScript are now registered through the standard WordPress enqueue functions.
* Housekeeping: removed the redundant text-domain loader (WordPress loads translations automatically) and tidied the plugin for the WordPress.org directory.

= 1.50.0 =
* Security: posting and scheduling to your social accounts now require publish-level permission (Author and above) plus edit access to the specific post, and cancelling/deleting queued or failed sends now requires administrator permission. Previously a Contributor-level user could post as your official accounts or clear others' scheduled sends.
* Security: stored account credentials are now encrypted with a full-strength key and a versioned format that fails safe. Existing connections keep working with no reconnection needed — older stored credentials are read as before and upgraded automatically the next time each account is saved.
* Security: the X (Twitter) account-connection callback now requires administrator permission, matching Facebook and LinkedIn, and image previews can no longer be used to make the server fetch internal/private addresses (SSRF hardening). Remote image downloads are also size-capped to protect against memory exhaustion.
* Reliability: expired LinkedIn and Facebook access tokens now show as a stats error instead of silently recording empty stats; a failed Facebook long-lived-token step now reports the failure at connect time instead of leaving an account that stops working within hours; and malformed AI responses no longer cause a fatal error during copy generation.
* Reliability: a scheduled or approved post whose time had already passed could get stuck as "pending" and never send — it is now always scheduled to fire shortly. A single post scheduled far in the future no longer suppresses all other automated posting. Deactivating the plugin now fully clears its scheduled events.
* Fixed composing a scheduled post into a campaign creating a duplicate "draft" copy in the campaign: the real scheduled post is now linked to the campaign directly.
* Uninstall (when "delete all data" is enabled) now also removes the automation and content-class settings and the plugin's post meta, and every plugin cron event is unscheduled on delete regardless of the data-retention setting.
* Fixed X (Twitter) posts failing entirely when an image upload failed. An internal diagnostic field was being included in the payload sent to the X API, which rejects unknown fields, so the intended "post without the image" fallback failed the whole tweet. The image error is now recorded on the post history instead, and the tweet publishes.
* Fixed the campaign workspace "Edit post" screen silently corrupting a saved post: it loaded the on-screen (truncated) copy back into the editor, so saving a longer post cut off the end of its text, and could also reassign the post to the wrong social account or clear a per-post link override. The editor now loads the full stored values.
* Fixed a rare double-post: two overlapping scheduled-send runs could both publish the same queued post. Sends are now claimed atomically so only one runs.
* Fixed scheduled send times being an hour or more off on sites not set to UTC (for example UK sites during British Summer Time). The scheduler now correctly treats the time you enter as your site's local time and converts it. This applies to both the post editor scheduler and the Compose screen.
* Performance: the channel report (and the Insight Engine read contract) now caches its Google Analytics data for a short period, instead of making live Google API calls on every view.
* Branding: fixed silent loss of branding bars on live posts. When the branded-image step failed only on the live send (for example when the GD image library is unavailable to the WP-Cron process), the post shipped with the original unbranded image and no reason was recorded, even though the admin preview showed the bars. The send now records why branding was skipped on the post's history record, so the cause is diagnosable. Behaviour is unchanged when branding succeeds.
* Branding: when a LinkedIn or Bluesky image upload fails (and the post falls back to a link/text post, which is why those platforms can show an unbranded, scraped thumbnail while Facebook keeps its branded image), the history record now includes the HTTP status, the response body, and the image size, dimensions and type that were sent — so platform-specific rejections such as Bluesky's ~976KB blob-size limit or a LinkedIn token/permission error are identifiable.
* Branding: fixed Bluesky showing the image inside a large squarish box with a pale grey background. The image embed now declares its aspect ratio, so Bluesky renders it at its true shape (for example 16:9) instead of letterboxing it in a default frame.
* Reliability: Bluesky now reuses a cached session instead of re-authenticating on every post and every stats fetch, avoiding Bluesky's sign-in rate limits (which could otherwise block posting after a stats refresh). An expired LinkedIn connection now shows a clear "please reconnect" message instead of failing silently.
* Performance: the automation engine and calendar no longer run large numbers of repeated database queries when scoring posts to promote; the campaign preview downloads the source image once instead of once per platform; and the branded-image AI fit mode no longer decodes the image twice. Upgraded sites also have the queue table's optional columns corrected so house-campaign posts always save.
* The "Queue" menu item is renamed "Activity" (it shows suggestions, scheduled and recently-sent posts, so "Queue" was misleading when nothing was scheduled ahead).
* Stats: a sent post with no stats now explains why per platform (e.g. LinkedIn company-page analytics need API approval; X impressions need the paid tier) instead of a bare "No stats yet", and the Activity page's Refresh button now shows the reason when a refresh returns nothing instead of appearing to do nothing.
* Fixed Facebook likes, comments and shares not appearing: they were being requested in the same call as impressions, which needs the read_insights permission, so without it Facebook rejected the whole request. Engagement is now fetched separately (using the page permission you already have) and works on Refresh; impressions/reach still require read_insights (a reconnect and Facebook review), which remains a separate step.

= 1.49.0 =
* Preview modal fixes: the preview is now capped to the screen height with the header and close button always visible, and the body scrolls inside it instead of overflowing off the top.
* Preview cards now sit in a fixed three-column grid, so a fourth platform (for example Bluesky) sits under the first card at the same size instead of stretching full width.
* The preview now only shows platforms that have a connected, active account, so a platform with no account set up is no longer previewed.

= 1.48.0 =
* Added a per-post "Image branding bars" override in the post editor. Each post can use the campaign default or be set individually to headline and logo bars, logo bar only, logo overlaid on the full image, or no bars. This lets a designed image that already carries its own headline (set it to overlay or no bars) sit in the same campaign as ordinary photos (which keep the headline bar) without the plugin stacking a second headline or logo bar over the artwork.
* The post preview reflects the per-post choice, and the override is applied on the live send. Posts with no override continue to follow the campaign default.
* Database: added an image_layout column to the queue, applied automatically on upgrade.

= 1.47.0 =
* Smarter image cropping. When a photo has to be cropped to fill the space, the plugin now keeps the busiest, most likely face-bearing band of the image in frame instead of blindly cropping from the centre. This stops faces and subjects being sliced off the top or bottom. Runs locally with no per-image cost.

= 1.46.0 =
* Added Bluesky as a posting platform. Bluesky does not suppress external links, so the tracked URL posts directly in the body as a clickable link alongside the branded image.
* Connect a Bluesky account on the Connected Platforms page with your handle and an app password (Bluesky Settings, then App Passwords). No developer app, API key or approval is required, and the app password is encrypted at rest.
* Bluesky appears in the campaign preview, automation rules, the post send summary and the channel report. Posts respect Bluesky's 300-character limit, preserving the link when copy is trimmed.
* Note: the connector uses the default bsky.social host; accounts on a third-party PDS are not yet supported.

= 1.45.0 =
* Fixed: the branded image now always fills the full width of the available space, cropping top/bottom as needed, instead of letterboxing with side gaps. This removes the wider side gaps that showed on LinkedIn and Facebook, whose image regions are shorter than X.
* Preview: step through every image in the pool with Prev / Next controls and an "Image x / n" counter, so you can check each rotation image without sending.
* Preview: larger modal and larger platform cards for a clearer view.
* Preview: the campaign-level Preview button now sits in the Campaign summary block rather than the Posts header, so it reads as a campaign action.

= 1.44.0 =
* Added a "Logo overlaid on full image" image option for campaigns. It keeps the full 16:9 design uncropped and places the publisher logo in a corner, so finished designs that already carry their own headline still get publisher branding without a reserved bar cropping them. The bar options remain for plain photos.

= 1.43.0 =
* Added a per-campaign "Image branding bars" option: Branded (use Settings layout), Headline and logo bars, Logo bar only, or No bars (full image). Designed images that already carry their own text and logo can now post full-bleed with no bars, while make-do site images can keep the branding furniture.
* Whatever the choice, the image now fills the available space (cropped to cover) instead of letterboxing with side gaps. The editor shows the optimum source image size for the chosen option (full 16:9 1200 x 675 for no bars, 1200 x 583 for one bar, 1200 x 491 for two bars).
* The chosen layout is reflected in the campaign preview and the live posts through the same pipeline.
* Database: added an image_layout column to campaigns, applied automatically on upgrade.

= 1.42.0 =
* Added an optional Headline field to house campaigns. When set, it is drawn in the branded-image headline bar following the Settings brand standard (the logo and URL bars are always applied); leave it blank for no headline bar. Content-push posts continue to take their headline from the post. The headline shows in the campaign preview. It applies to the image only and is not added to the post text.
* Database: added a headline column to campaigns, applied automatically on upgrade.

= 1.41.0 =
* Campaign preview: added Preview buttons on the campaign editor, one per post and one for the whole campaign. Each opens a modal with a card per platform (X, LinkedIn, Facebook) showing the real composed copy, hashtags and branded image, plus a character count against each platform limit. The preview is built through the same copy composer and image pipeline as the live send, so it matches what will publish. It notes when AI-varied copy will differ on the day, and that house campaigns post without hashtags.
* Internal: the send path copy assembly (link tagging and hashtags) is now a shared method, so the preview and live posts cannot drift apart.

= 1.40.0 =
* Added a "Registration / Sign-up" campaign objective for newsletter sign-up and registration drives. It also appears as a format/asset type in the Channel report.

= 1.39.0 =
* Fixed the campaign image library buttons not opening: the picker script bailed out before the media library finished loading. The wp.media check now runs when the button is clicked, and the media library is loaded directly on the campaign editor page.
* Clearer on-page guidance: the image pool field now explains that the engine cycles through the pooled images and that the single house Image URL is only a fallback used when the pool is empty; the house Image URL field carries the same note.

= 1.38.0 =
* Campaign editor: the image pool and the house campaign image can now be chosen from, or uploaded to, the WordPress media library instead of pasting URLs by hand. "Add images from library" on the pool is multi-select and appends one URL per line; "Select" on the house image is single-select. The URL fields remain editable, so external image URLs still work.

= 1.37.0 =
* Campaigns page now shows a recommended image sizes note (landscape, at least 1200px wide; renders to 1200 x 675 for X and 1200 x 630 for Facebook and LinkedIn).

= 1.36.0 =
* History page now renders each send as its own card, with the post title and copy at the top and the channel rows (one per platform) grouped inside. Channels are clustered by post and send time, so a story's channels stay together even when their timestamps differ by a second or two.
* Fixed the per-row Refresh and Delete controls on the card layout: removed a duplicate inline handler that fired alongside the main one (causing a double confirmation and a call to a delete action that did not exist), and made the single source card-aware.
* Per-row Refresh now repaints all stat types (impressions, reach, likes, shares, comments, clicks) instead of only likes, shares and comments.

= 1.35.0 =
* History page now groups the channel rows of each send into a separate block, with the post title and copy shown once per batch and a clear divider between batches, instead of a continuous flat list that made it hard to see which rows belonged together.

= 1.34.0 =
* Branded images: added a separate "Central panel background" colour for the area behind a contained image (logos and graphics that do not fill the frame). Previously this shared the logo bar colour, so the bar could blend into it when an image was shrunk. Defaults to the logo bar colour, so existing output is unchanged until set.

= 1.33.0 =
* Blend hashtag mode now guarantees a mix: brand hashtags are given reserved slots within each platform cap instead of being dropped when a story already has several tags of its own. Previously a story with enough tags could push the brand defaults past the cap, worst on Facebook with only two slots.

= 1.32.0 =
* Fixed the post-editor channel rows: they now lay out as a proper column with the checkbox fixed and each account name in its own block, wrapping under itself instead of under the checkbox. Account names and the image label are no longer force-capitalised, and the featured-image thumbnail is smaller so it sits evenly with the channel badges.

= 1.31.0 =
* Tidied the post-editor panel. The social status now sits at the top so you see the state first. Author tagging and the Boost controls are folded into collapsed sections with neutral styling (the yellow box is gone), and help text is trimmed. No capability removed.

= 1.30.0 =
* Fixed a duplicate-posting defect: publishing a post (which auto-schedules a send) and then posting the same post manually no longer results in the story going out twice. A manual Post Now or Schedule now cancels any pending auto-scheduled send for that post, and auto-scheduling is skipped if the post already has a send queued or sent.

= 1.29.0 =
* Redesigned the Queue & History panel in the post editor. It now groups by send (one line per send) instead of one row per platform, so a single share to all channels reads as one line with a coloured chip per channel showing each channel's status at a glance. Queued sends are grouped above posted ones, with a single Cancel per queued send, compact relative times, and aggregated engagement.

= 1.28.0 =
* Hashtags are now applied to every post (automation, reposts, scheduled and manual) per the brand-tag mode, deduped against any tags already in the copy and capped per platform. The Brand hashtags settings now take effect; previously they were never written into posted copy.
* The in-post sidebar now refuses to post or schedule until the post is published, so a social post can no longer be sent against an auto-draft (which produced "Auto Draft" copy, a /?p=ID link and an unbranded image).
* Untouched default copy is rebuilt from the published post at send time, so the title and link are always current. Edited copy is left exactly as written.

= 1.27.0 =
* Cadence is now a single suite-wide setting (minimum gap, maximum per day, repost idle gap) instead of per-platform, matching the slot model where one story fans to every channel at once. Per-platform values could never be honoured and silently made the suite stricter.
* Content profiles now apply real cadence and reposting values when selected, so the chosen profile does what its label promises out of the box.
* Added per-field help notes on the Automation screen and a support sidebar with model settings by site type.
* Existing per-platform settings are migrated to the equivalent suite values on upgrade, so live behaviour does not change.

= 1.7.4 =
* LinkedIn relay callback now uses a dedicated admin-post endpoint to avoid silent callback drops on some hosts.
* Improved LinkedIn callback error handling when nonce or ticket data is missing.

= 1.7.3 =
* Added LinkedIn relay ticket exchange callback handling for shorter, safer OAuth returns.
* Improved LinkedIn relay error handling on the accounts page.

= 1.7.2 =
* Updated LinkedIn posting to the versioned Community Management Posts API.
* Updated LinkedIn image uploads to the versioned Images API.
* Added LinkedIn API version headers and improved company-page account discovery.

= 1.0.0 =
* Initial release.
* Connected accounts manager for X, LinkedIn, Facebook.
* Post Now and Schedule from the post editor metabox.
* Tag-to-hashtag helper.
* Character counter with X 280-character warning.
* Upcoming queue admin page.
* Pro: WP Job Manager support, multiple accounts, post history.

== Upgrade Notice ==

= 1.7.3 =
Adds the LinkedIn relay ticket exchange needed by the updated pkits.org relay.

= 1.7.2 =
Updates LinkedIn posting for the approved Community Management API flow.

= 1.0.0 =
Initial release.
