# DraftSEO.ai - WordPress Plugin

Publish AI-generated blogs from DraftSEO.ai platform directly to WordPress with automatic image import and SEO optimization.

## Installation

### From WordPress.org (Recommended)

1. Go to WordPress Admin → Plugins → Add New
2. Search for "DraftSEO.ai"
3. Click "Install Now" and then "Activate"

### Manual Installation

1. Download the plugin ZIP file
2. Go to WordPress Admin → Plugins → Add New → Upload Plugin
3. Choose the ZIP file and click "Install Now"
4. Activate the plugin

### From this Repository

1. Clone or download this repository
2. Copy the `draftseo-ai` directory to `wp-content/plugins/`
3. Go to WordPress Admin → Plugins
4. Find "DraftSEO.ai" and click "Activate"

## Configuration

1. Navigate to WordPress Admin → DraftSEO → Settings
2. Click "Connect with DraftSEO.ai" button
   - You'll be automatically redirected to DraftSEO.ai
   - Sign in if you're not already logged in
   - Connection completes automatically (OAuth-based, no API key needed)

## Features

### Core Features
- ✅ One-click publishing from DraftSEO.ai to WordPress
- ✅ Automatic image import from DraftSEO.ai to WordPress Media Library
- ✅ SEO metadata preservation (keywords, meta descriptions)
- ✅ WordPress category sync
- ✅ Automatic tag creation from blog keywords
- ✅ Multiple post status options (draft, publish, schedule)
- ✅ Secure API key encryption and HMAC-SHA256 webhook signatures

### Image Handling
The plugin intelligently handles images based on blog size:

- **1-5 images**: Direct import — all images downloaded in parallel and imported immediately (typically 5–15 seconds)
- **6+ images**: Hybrid approach
  - Featured image imported immediately
  - Remaining images downloaded in parallel and imported in the background via Action Scheduler (no page visit needed)

All images are downloaded directly from DraftSEO.ai to your WordPress Media Library.


## Usage

### Publishing a Blog

1. **Generate Blog on DraftSEO.ai**
   - Create your blog using DraftSEO.ai platform
   - Configure SEO settings, categories, and tags

2. **Publish to WordPress**
   - Click "Publish to WordPress" button
   - Select your connected WordPress site
   - Choose publishing options (author, category, status, date)
   - Click "Publish"


### API Endpoints

The plugin provides REST API endpoints for DraftSEO.ai integration:

- `GET /wp-json/draftseo/v1/users` - Get WordPress users
- `GET /wp-json/draftseo/v1/categories` - Get WordPress categories
- `GET /wp-json/draftseo/v1/tags` - Get WordPress tags
- `GET /wp-json/draftseo/v1/site-info` - Get site metadata (name, locale, language)
- `GET /wp-json/draftseo/v1/posts` - Get paginated published posts with SEO metadata
- `POST /wp-json/draftseo/v1/publish` - Publish blog to WordPress
- `POST /wp-json/draftseo/v1/update` - Update/republish existing post
- `GET|POST /wp-json/draftseo/v1/test-connection` - Test API connection
- `POST /wp-json/draftseo/v1/remote-disconnect` - Clear connection (called by DraftSEO.ai)
- `GET /wp-json/draftseo/v1/find-by-blog-id` - Look up a WordPress post by DraftSEO blog ID
- `POST /wp-json/draftseo/v1/find-by-blog-ids` - Bulk look up posts by DraftSEO blog IDs (up to 500)
- `GET /wp-json/draftseo/v1/all-published-posts` - Paginate all posts published via DraftSEO

All endpoints require Bearer token authentication using your API key.

## Requirements

- **WordPress**: 6.2 or higher
- **PHP**: 7.4 or higher
- **MySQL**: 5.6 or higher
- **WordPress Cron**: Enabled (for background image processing)
- **DraftSEO.ai Account**: Active subscription

## File Structure

```
draftseo-ai/
├── admin/                          # Admin interface files
│   ├── css/
│   │   └── admin-styles.css       # Admin page styles
│   ├── js/
│   │   └── admin-scripts.js       # Admin page JavaScript
│   └── settings-page.php          # Settings page template
├── includes/                       # Core plugin classes
│   ├── class-api-client.php       # DraftSEO.ai API communication
│   ├── class-content-processor.php # HTML cleanup and formatting
│   ├── class-rest-api.php         # Route registration, auth, utility endpoints
│   ├── class-blog-publisher.php   # publish_blog + update_blog handlers
│   ├── class-frontend-output.php  # FAQ schema JSON-LD + inline CSS output
│   ├── class-seo-handler.php      # SEO metadata management
│   ├── class-settings.php         # Settings and API key management
│   └── images/                    # Image pipeline (one class per file)
│       ├── class-image-downloader.php # curl_multi parallel download + fallback
│       ├── class-image-importer.php   # Media Library import, dedup, URL rewrite
│       ├── class-image-handler.php    # Import strategy selection + AS job callbacks
│       ├── class-image-repair.php     # Self-healing one-time repair scan
│       └── class-image-health.php     # Recurring daily audit
├── languages/                      # Translation files
├── draftseo-ai.php         # Main plugin file
├── LICENSE.txt                     # GPL v2 license
├── readme.txt                      # WordPress.org readme
├── uninstall.php                  # Uninstall handler
└── README.md                       # This file
```

## Development

### Coding Standards
- Follows WordPress Coding Standards (WPCS)
- PHPCS compliant
- Nonces for security
- Capability checks for permissions
- Input validation and sanitization

### Hooks and Filters

#### Actions
- `draftseo_process_images_background` - Background image processing
- `draftseo_before_publish` - Before publishing a blog
- `draftseo_after_publish` - After publishing a blog

#### Filters
- `draftseo_content_cleanup` - Modify content cleanup settings
- `draftseo_seo_metadata` - Modify SEO metadata before saving
- `draftseo_image_import_strategy` - Override automatic strategy selection

## Troubleshooting

### Connection Test Fails
- Verify API key is correct
- Check WordPress site URL is accessible from internet
- Ensure REST API is enabled on WordPress
- Check firewall/security plugins aren't blocking API requests

### Images Not Importing
- Check WordPress has write permissions to uploads directory
- Verify WordPress Cron is running (test with WP Crontrol plugin)
- Check PHP `max_execution_time` setting (should be 60+ seconds)
- Review WordPress error logs for image download errors

### Tags Not Created
- Ensure "Auto-create tags" is enabled in settings
- Check keywords are provided in the blog data
- Verify user has permission to create tags
- Check maximum tags limit in settings

## Support

- **Support**: [Contact Support](https://draftseo.ai/contact)
- **WordPress.org**: [Plugin Support Forum](https://wordpress.org/support/plugin/draftseo-ai)

## License

This plugin is licensed under the GNU General Public License v2 or later.

## Credits

Developed by [DraftSEO.ai](https://draftseo.ai)

## Changelog

### 1.2.4

Fixes stuck "images importing…" jobs, adds a Logs admin tab with CSV export and DB-backed structured logging, and improves error visibility across all image import paths.

- **Sync resolves stuck processing jobs (Phase 4)** — When the HMAC-signed callback URL expires before the WordPress plugin fires it (24-hour window), the syndication job is left permanently in `'processing'`. Sync now adds a Phase 4 that detects these jobs (stuck > 1 hour, WordPress confirms the post exists) and marks them `'completed'` automatically. No user action required — just run Sync.
- **Repair scan fires DraftSEO callback after success** — After successfully importing all images for a post, `DraftSEO_Image_Repair::repair_post_images()` now reads the `draftseo_syndication_job_id` post meta and calls the new `POST /api/wordpress/images-complete` endpoint (authenticated via long-lived Bearer API key, not an expiring HMAC URL). This closes the loop on any syndication job whose original callback had already expired.
- **New `/api/wordpress/images-complete` endpoint** — Server-side endpoint that accepts completion notifications from the plugin. Authenticates by decrypting all stored WordPress API keys and comparing with the Bearer token; then cross-checks the `job_id` against the authenticated account before writing. Idempotent: already-completed jobs return 200 without overwriting.
- **`draftseo_syndication_job_id` post meta** — Both `publish_blog` and `update_blog` now write the DraftSEO syndication job ID to post meta when the `jobId` field is present in the publish/update payload. This is what enables the repair scan to send the completion notification without the original expiring callback URL.
- **Logs tab in plugin settings** — Browse, filter by level/type, search messages, export to CSV, copy to clipboard, or clear all entries from a dedicated Logs tab in the DraftSEO settings page. Entries older than 30 days are purged automatically by Action Scheduler.
- **Structured DB logger (`wp_draftseo_logs`)** — New `DraftSEO_Logger` class writes structured rows (`level`, `type`, `message`, `post_id`) to the DB and mirrors them to `error_log()`. All image import paths now write to this log: `import_images_direct`, `import_images_async`, `process_images_with_callback`, `process_images_background`, `fire_callback`, `import_from_temp`, `sequential_download_images`, `parallel_download_images`, `process_repair_batch`, `repair_post_images`, and `process_daily_health_check`.
- **`process_images_background` now has full logging** — The hybrid strategy (6+ images, no callbackUrl) was previously completely silent on errors. Now emits per-URL download failure logs and a summary on completion.
- **`import_images_direct` per-failure logging** — Logs each download failure with the URL and prints an end-of-run summary.
- **`import_from_temp` logs at source** — MIME-type rejections and `media_handle_sideload` errors are now logged here so every caller gets the detail automatically.
- **`sequential_download_images` failure logging** — The curl_multi fallback path was completely silent on failure; now logs the error message and URL for every failed download.

### 1.2.3

Critical fix: background image health check and repair scan were silently never scheduled.

- **Action Scheduler initialisation timing bug** — Both `maybe_schedule_repair` and `schedule_health_check` were hooked to `plugins_loaded` at priorities 20 and 25. Action Scheduler's data store is not ready until the WordPress `init` hook (priority 1), deferred from `plugins_loaded` priority 1. Every `as_*()` helper function is guarded by `ActionScheduler::is_initialized()`, which returns `false` until the data store is ready and silently returns `0`/`false` instead of scheduling anything. The result: no `draftseo_daily_health_check` recurring action was ever registered, and no `draftseo_repair_missing_images` scan was ever queued, on any site running 1.2.0–1.2.2. Fixed by moving both hooks to `action_scheduler_init`, the hook AS fires specifically to signal the data store is ready.

### 1.2.2

Bug fixes, new SEO plugin support, and automatic sync reconciliation.

- **Duplicate post guard** — Publishing the same blog twice (e.g. after a network timeout, or when the DraftSEO app doesn't have the WordPress `post_id` stored) no longer creates a duplicate. The `/publish` endpoint checks for an existing post with the same DraftSEO blog ID before inserting, and returns the existing post with `status: 'already_exists'` if found. Positive lookups are cached for 60 seconds to absorb rapid retries.
- **SEO title now written** — The per-post SEO title (SERP title override, separate from the WordPress `post_title`) was silently dropped. It is now written to Yoast (`_yoast_wpseo_title`), Rank Math (`rank_math_title`), All in One SEO (`_aioseo_title`), and SEOPress (`_seopress_titles_title`) when a `seoTitle` value is present in the publish or update request.
- **SEOPress fully supported** — SEOPress is now detected and handled alongside Yoast, Rank Math, and AIOSEO. All three writable fields are populated: meta description (`_seopress_titles_desc`), SEO title (`_seopress_titles_title`), and canonical URL (`_seopress_robots_canonical`). SEOPress has no dedicated focus keyword field, so that value is omitted.
- **Image captions now imported** — Images carrying a `caption` field from DraftSEO had their caption silently dropped. The caption is now written to the attachment's `post_excerpt` immediately after import, making it visible in the WordPress Media Library, the block editor image block, and default `[caption]` / `<figcaption>` output.
- **Repair scan silently skipped after manual update** — The most common plugin update flow (deactivate → upload new ZIP → activate) caused the self-healing repair scan to be permanently skipped. The fix stamps the DB version only on fresh installs; upgrades leave the old version in place so the version-mismatch check fires correctly on the next page load.
- **Double-scheduling guard fixed** — The guard that prevented the repair scan from being queued more than once was comparing against an empty args array, but the actual scan job stores a batch number in its args. Fixed by passing `null` (match any args) instead of `array()` (match empty args only).
- **VideoObject schema for YouTube embeds** — Posts containing YouTube videos (Gutenberg embed blocks, `watch?v=`, `youtu.be/`, `embed/`, or `shorts/` URLs) now have `VideoObject` JSON-LD injected into the page `<head>` at publish time, making them eligible for video-rich results in Google Search.
- **Sync restores lost blog→post links** — When you click Sync for a WordPress connection, DraftSEO checks every locally known syndication job against WordPress and restores the post link (`externalId`, `publishedUrl`) for any job where it was lost (e.g. after a database restore). The sync response includes a `reconciliation` summary with counts of recovered and not-found posts.
- **Retroactive publish recovery on Sync** — Sync also paginates through all WordPress posts published via the DraftSEO plugin and reconstructs any missing DraftSEO-side records. Blogs that exist on WordPress but lost their tracking (e.g. after a database restore or account transfer) immediately show as Published on the Posts page. The sync response includes a `retroactiveReconciliation` summary.
- **Blog-ID lookup endpoints** — New `GET /find-by-blog-id` and `POST /find-by-blog-ids` (up to 500 IDs per call) endpoints allow DraftSEO to look up WordPress posts by their DraftSEO blog ID. Both require standard API key authentication.

### 1.2.1

Internal code reorganisation — no new features or behaviour changes.

- **Code structure** — Split monolithic PHP files into smaller, single-responsibility classes (image pipeline, publish handler, frontend output); no public APIs, hook names, or plugin behaviour changed.

### 1.2.0

Image reliability fixes, automatic self-repair for all previously affected posts, and continuous background health monitoring.

- **Images missing after publishing** — Images were sometimes missing from the WordPress Media Library after publishing from DraftSEO. This happened silently on certain hosting environments due to differences in how the server handles external downloads. The plugin now tries an additional download method before giving up, significantly increasing import success rates across all hosting environments.
- **Automatic repair for previously affected posts** — After updating, the plugin automatically scans your entire site in the background for any posts where images were never properly imported from DraftSEO. Affected posts are silently repaired without any action needed from you. Progress is visible under **Tools → Scheduled Actions** in your WordPress admin.
- **Daily background check for image health** — A lightweight check now runs automatically every 24 hours. If any post is found with images that didn't fully import — for example because a server timeout interrupted the process overnight — the plugin queues a repair automatically. No monitoring required: healthy sites see no activity and affected posts are fixed without manual intervention.
- **Health checks stay fast as your site grows** — Once a post's images are confirmed fully imported, the daily check permanently skips it in future runs, so it stays fast regardless of how many posts are on your site. On a fully audited site the check completes in a fraction of a second. Republishing a post from DraftSEO automatically resets it so new images are always verified.
- **Built to handle high-volume publishing** — Designed and tested for sites publishing large numbers of blogs, each with many images. Image imports run in controlled parallel batches to stay within the resource limits of any hosting environment, from shared hosting to dedicated servers.

### 1.1.6

Faster republishing — only new or changed images are re-downloaded.

- **No redundant image downloads on republish** — Previously, every republish downloaded all images from the CDN to WordPress even if nothing had changed. The plugin now checks which images are already in the Media Library before downloading anything, and skips the download entirely for images it has already imported. For a typical blog republish where images haven't changed, this means zero image downloads.

### 1.1.5

Reliability improvements for publishing and republishing.

- **Republish no longer blocked** — Republishing a post could fail if images from a previous publish were still importing in the background. Republishing now always works as long as the post exists on WordPress.
- **Accurate publish completion** — DraftSEO now correctly waits for all images to finish importing before marking a publish as complete, rather than marking it done immediately. Status in DraftSEO now reflects reality.
- **Automatic retry on delivery failure** — If DraftSEO cannot be reached to confirm image import completion, the plugin will retry automatically instead of leaving the publish stuck.

### 1.1.4

Cosmetic and connectivity improvements.

- **Settings heading updated** — Page title now reads "DraftSEO.ai" with the installed plugin version displayed next to it in small grey text.
- **"Connection Issue" state** — When authentication with DraftSEO.ai fails, the connection badge now shows an amber "Connection Issue" status instead of the green "Connected" badge. A guided panel explains the four steps needed to reconnect.
- **Cleaner settings button** — The "Go to DraftSEO.ai" button on the connected settings page no longer shows an icon; text updated to match current branding.

### 1.1.3

Reliability improvements, faster image loading, and cleaner background processing.

- **Images now load reliably on quiet sites** — Images were sometimes not appearing on published posts on low-traffic websites because background download jobs weren't starting until the next page visit. They now start immediately regardless of site traffic.
- **Clean deactivation** — Switching the plugin off while a publish was in progress could leave background tasks running after deactivation. The plugin now cleanly stops all background jobs when it is turned off.
- **Proper uninstall disconnect** — Removing the plugin now correctly notifies DraftSEO.AI and closes the connection before all plugin data is deleted.
- **Translations fixed** — Plugin text was not translating correctly on WordPress sites running in a language other than English. Translation files now load properly.
- **Action Scheduler for background jobs** — Background image jobs now run via Action Scheduler instead of WP Cron. Action Scheduler does not need a page visit to start — it runs as a true background process — and retries jobs automatically if a download fails. Pending and completed jobs are visible in the WordPress admin at **Tools > Scheduled Actions**.
- **Parallel image downloads** — Images in background jobs are now downloaded simultaneously before being imported into the Media Library. For a blog with 20 images this cuts total download time from roughly 60–100 seconds (sequential) to 5–15 seconds (parallel).
- **No duplicate images on republish** — Republishing a post was creating a duplicate Media Library entry for every image that had not changed since the previous publish. Only new or replaced images are now downloaded and imported — unchanged images are reused from the existing entry, keeping the Media Library clean.

### 1.1.2

Security update: fixes API token authentication on WordPress sites where the Application Passwords feature or a security plugin was blocking server-to-server requests from DraftSEO.AI before they could be validated.

### 1.1.0

Automatic cleanup when republishing with a new image.

- **Image replacement** — When you republish a post with a new AI-generated image, the new image is swapped in automatically
- **Media cleanup** — The previous image is removed from your WordPress Media Library — keeps your media folder clean and saves storage space

### 1.0.5

YouTube video embeds now work on WordPress.

- **YouTube videos fixed** — YouTube videos were not appearing on published WordPress posts. Videos are now properly converted to native WordPress embed blocks before publishing, so they show up as responsive YouTube players on your site. Previously published posts need to be republished to pick up this fix.

### 1.0.4

Content formatting and image fixes.

- **Headings after images fixed** — Headings that appeared immediately after an image were showing as plain text instead of proper headings. They now display correctly.
- **Unwanted image captions removed** — Image descriptions were showing as visible text below every image on the page. They are now used for accessibility only (screen readers) and no longer display as captions.

### 1.0.3

Content formatting fixes.

- **Heading formatting fixed** — Some headings were appearing as plain text instead of proper headings. They now render correctly on WordPress.
- **Citation links fixed** — Citation references in blog posts were being malformed during publishing. They now display as clean numbered references.

### 1.0.2

Content formatting and SEO structured data hotfix release.

- **In-text citations** — `[1]`, `[2]` markers now render as clickable superscript links that scroll to the matching reference in the References section
- **References section** — Converted to a styled numbered list with anchor IDs (`#ref-1`, `#ref-2`) for citation linking; supports all reference styles (url_title, harvard_apa6, mla9, etc.)
- **External links** — All external links now open in a new tab with `target="_blank"` and `rel="noopener noreferrer"`
- **FAQ structured data (JSON-LD)** — FAQ question-answer pairs are extracted from blog content and injected into the post `<head>` for Google FAQ rich results
- **Theme-consistent styling** — CSS is injected for citations, references, and tables so they display correctly across all WordPress themes
- **HTML element support** — `<sup>` for citations, `<ol>`/`<li>` with `id` attributes, and `<a>` with `target`/`rel` are now preserved in published post content
- **Active sites filter** — WordPress site dropdown now only shows active/connected sites

### 1.0.1

Hotfix for content rendering in published posts.

- **YouTube embeds** — Were being stripped during publishing; now render as embedded players on your site
- **Data tables** — Were displaying as raw Markdown text; now output as formatted HTML tables

### 1.0.0

Major release — security hardening, reliability improvements, and full tag management.

#### Security
- **Webhook signatures** — Disconnect and deactivation notifications are signed with HMAC-SHA256 (`X-DraftSEO-Signature`, `X-DraftSEO-Timestamp` headers); the API key is the signing secret and is never transmitted in plain text
- **Replay protection** — Signed requests include a Unix timestamp; requests older than 5 minutes are rejected
- **API keys encrypted at rest** — AES-256-CBC with a unique IV per key, derived from the WordPress site's auth salt

#### Publishing & REST API
- **Tags endpoint** — `GET /wp-json/draftseo/v1/tags` added for tag sync, matching the existing `/users` and `/categories` endpoints
- **Server-side input validation** — `/publish` and `/update` routes validate and sanitise all params before the handler runs
- **Structured error responses** — All errors return specific codes (`rest_forbidden`, `rest_missing_param`, `rest_publish_error`, etc.) for better debugging
- **Bidirectional disconnect** — Disconnecting from DraftSEO.AI calls `/remote-disconnect` to clear connection settings on the plugin side automatically

#### Reliability
- **Non-JSON response handling** — Gracefully handles HTML maintenance pages, WAF blocks, and 503 responses instead of failing silently
- **Sync timeouts** — All sync requests have a configurable timeout; no more indefinitely hung connections
- **Error isolation** — Individual site connection errors in the multi-site view do not affect other connected sites

#### Performance
- **Parallel sync** — Users, categories, and tags are fetched simultaneously instead of sequentially
- **Smart retries** — 4xx errors (401, 403, 400, 422) fail immediately without retrying; only 5xx server errors trigger retries

#### Tag Management
- Auto-create WordPress tags from AI-generated keywords at publish time (configurable, 1–10 tags)
- Select from existing WordPress tags, or create new ones on the fly during publishing

#### Image Handling
- All images downloaded directly to your WordPress Media Library
- Alt text from DraftSEO.AI preserved as WordPress image alt text
- Featured image set automatically; post content image URLs updated from DraftSEO.AI CDN to local Media Library URLs

#### Usability
- "Settings" quick-link added to the Plugins page for faster access to plugin configuration

### 0.2.0 (Initial Beta)
- One-click blog publishing from DraftSEO.ai
- Automatic image import from DraftSEO.ai
- SEO metadata transfer
- WordPress category sync
- Auto-create tags from keywords
- Multiple post status options
- Content cleanup and formatting
- Secure API key encryption
- Background image processing for large blogs
- Remote disconnect synchronization
- OAuth-based connection flow
