# IGNY8 WordPress Integration Reference
**Version:** 1.0.0
**Last Updated:** November 29, 2025
**Purpose:** Complete reference for the bidirectional WordPress ↔ IGNY8 integration, covering architecture, data flows, sync mechanisms, and troubleshooting.
---
## Table of Contents
1. [Integration Overview](#integration-overview)
2. [Architecture](#architecture)
3. [WordPress Plugin Structure](#wordpress-plugin-structure)
4. [IGNY8 Backend Integration Points](#igny8-backend-integration-points)
5. [Authentication & Security](#authentication--security)
6. [Bidirectional Sync: Data Elements](#bidirectional-sync-data-elements)
7. [Publishing Flow (IGNY8 → WordPress)](#publishing-flow-igny8--wordpress)
8. [Sync Flow (WordPress → IGNY8)](#sync-flow-wordpress--igny8)
9. [REST API Endpoints](#rest-api-endpoints)
10. [Data Models & Meta Fields](#data-models--meta-fields)
11. [Status Mapping](#status-mapping)
12. [Troubleshooting Guide](#troubleshooting-guide)
13. [Developer Quick Reference](#developer-quick-reference)
---
## Integration Overview
The IGNY8 WordPress Bridge enables **bidirectional synchronization** between IGNY8 (SaaS platform) and WordPress sites, allowing content created in IGNY8 to be published to WordPress and WordPress content/metadata to be synced back to IGNY8.
### Core Capabilities
| Feature | Direction | Status |
|---------|-----------|--------|
| **Content Publishing** | IGNY8 → WP | ✅ Live |
| **Post Status Sync** | WP → IGNY8 | ✅ Live |
| **Taxonomy Sync** | WP ↔ IGNY8 | ✅ Live |
| **SEO Metadata** | IGNY8 → WP | ✅ Live |
| **Featured Images** | IGNY8 → WP | ✅ Live |
| **Site Discovery** | WP → IGNY8 | ✅ Live |
| **Keyword Tracking** | IGNY8 → WP | ✅ Live |
| **Internal Linking** | IGNY8 → WP | 🔄 Pending |
| **Content Optimization** | WP ↔ IGNY8 | 🔄 Pending |
### Integration Model
```
┌─────────────────────────────────────────────────────────────┐
│ IGNY8 SaaS Platform │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Django Backend (api.igny8.com) │ │
│ │ - Publisher Service │ │
│ │ - WordPress Adapter │ │
│ │ - Content Models │ │
│ │ - Integration Models │ │
│ │ - Celery Background Tasks │ │
│ └──────────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────────┘
│
│ REST API (JSON)
│ - JWT Auth + API Key
│ - Unified Response Format
│
┌───────────────────────▼─────────────────────────────────────┐
│ WordPress Site (client.com) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ IGNY8 Bridge Plugin │ │
│ │ - API Client (class-igny8-api.php) │ │
│ │ - REST Endpoints (/wp-json/igny8/v1/) │ │
│ │ - Sync Handlers (sync/*.php) │ │
│ │ - Post Meta Storage (_igny8_* fields) │ │
│ │ - Custom Taxonomies (igny8_clusters, igny8_sectors)│ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## Architecture
### High-Level Data Flow
```
IGNY8 → WordPress Publishing Flow:
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
│ Frontend │ -> │ Publisher │ -> │ Publisher │ -> │ WordPress │ -> │ WordPress │
│ (Review.tsx)│ │ ViewSet │ │ Service │ │ Adapter │ │ REST API │
│ Publish │ │ /publish/ │ │publish_content()│ │ publish() │ │ /igny8/v1/ │
│ Button │ │ │ │ │ │ │ │publish-content│
└─────────────┘ └──────────────┘ └─────────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ WordPress │
│ Post │
│ Created │
└──────────────┘
WordPress → IGNY8 Sync Flow:
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────────┐
│ WordPress │ -> │ WP Hook │ -> │ Igny8API │ -> │ IGNY8 API │
│ save_post │ │ post-sync.php│ │ PUT/POST │ │ /writer/tasks│
│ Event │ │ │ │ │ │ │
└─────────────┘ └──────────────┘ └─────────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Content │
│ Updated │
└──────────────┘
```
### Component Interaction
**IGNY8 Backend Components:**
- **PublisherViewSet** (`igny8_core/modules/publisher/views.py`) - REST API endpoint
- **PublisherService** (`igny8_core/business/publishing/services/publisher_service.py`) - Business logic
- **WordPressAdapter** (`igny8_core/business/publishing/services/adapters/wordpress_adapter.py`) - WP integration
- **Celery Tasks** (`igny8_core/tasks/wordpress_publishing.py`) - Async background publishing
- **Content Model** (`igny8_core/business/content/models.py`) - Content data
- **SiteIntegration Model** (`igny8_core/business/integration/models.py`) - WordPress connection config
**WordPress Plugin Components:**
- **Igny8API** (`includes/class-igny8-api.php`) - API client for IGNY8
- **Igny8RestAPI** (`includes/class-igny8-rest-api.php`) - REST endpoints for IGNY8 to call
- **Post Sync** (`sync/post-sync.php`) - WP → IGNY8 synchronization
- **IGNY8 to WP** (`sync/igny8-to-wp.php`) - IGNY8 → WP content creation
- **Hooks** (`sync/hooks.php`) - WordPress action/filter hooks
- **Admin** (`admin/settings.php`) - Plugin settings UI
---
## WordPress Plugin Structure
```
igny8-wp-integration/
├── igny8-bridge.php # Main plugin file
│ - Plugin bootstrap
│ - Registers all classes
│ - Defines constants
│ - Version: 1.0.0
│
├── includes/ # Core functionality
│ ├── class-igny8-api.php # API Client
│ │ - login() - JWT authentication
│ │ - refresh_token() - Token refresh
│ │ - get/post/put/delete() - HTTP methods
│ │ - parse_response() - Unified response handling
│ │
│ ├── class-igny8-rest-api.php # WordPress REST Endpoints
│ │ Endpoints:
│ │ - GET /wp-json/igny8/v1/post-by-content-id/{id}
│ │ - GET /wp-json/igny8/v1/post-by-task-id/{id}
│ │ - GET /wp-json/igny8/v1/post-status/{content_id}
│ │ - GET /wp-json/igny8/v1/site-metadata/
│ │ - GET /wp-json/igny8/v1/status
│ │ - POST /wp-json/igny8/v1/publish-content/
│ │
│ ├── class-igny8-site.php # Site data collection
│ ├── class-igny8-webhooks.php # Webhook handlers
│ ├── class-igny8-link-queue.php # Link processing
│ └── functions.php # Helper functions
│
├── sync/ # Synchronization
│ ├── hooks.php # WP hooks registration
│ │ - save_post
│ │ - publish_post
│ │ - transition_post_status
│ │
│ ├── post-sync.php # WP → IGNY8 sync
│ │ - igny8_sync_post_status_to_igny8()
│ │ - igny8_update_keywords_on_post_publish()
│ │ - igny8_map_wp_status_to_igny8()
│ │
│ ├── taxonomy-sync.php # Taxonomy sync
│ │ - Clusters (igny8_clusters)
│ │ - Sectors (igny8_sectors)
│ │ - Categories/Tags mapping
│ │
│ └── igny8-to-wp.php # IGNY8 → WP content
│ - igny8_create_wordpress_post_from_task()
│ - igny8_import_seo_metadata()
│ - igny8_import_featured_image()
│ - igny8_import_taxonomies()
│
├── admin/ # Admin interface
│ ├── settings.php # Settings page UI
│ │ - API credentials
│ │ - Connection status
│ │ - Post type toggles
│ │
│ ├── class-admin.php # Settings controller
│ ├── class-admin-columns.php # Custom post columns
│ └── class-post-meta-boxes.php # Post editor meta boxes
│
├── data/ # Data collection
│ ├── site-collection.php # Full site scan
│ ├── semantic-mapping.php # Semantic metadata
│ ├── link-graph.php # Internal link graph
│ └── woocommerce.php # WooCommerce integration
│
└── docs/ # Documentation
├── WORDPRESS-PLUGIN-INTEGRATION.md
├── AUTHENTICATION-AUDIT.md
└── SYNC-DATA-FLOW-DIAGRAM.md
```
### Key Plugin Classes
**Igny8API**
- **Purpose:** Communication with IGNY8 REST API
- **Location:** `includes/class-igny8-api.php`
- **Key Methods:**
- `login($email, $password)` - Authenticate and get JWT tokens
- `refresh_token()` - Refresh expired access token
- `get($endpoint)` - GET request with auto-retry on 401
- `post($endpoint, $data)` - POST request
- `put($endpoint, $data)` - PUT request (for updates)
- `parse_response($response)` - Parse unified response format
**Igny8RestAPI**
- **Purpose:** Provide REST endpoints for IGNY8 to call
- **Location:** `includes/class-igny8-rest-api.php`
- **Authentication:** API key via `X-IGNY8-API-KEY` header or Bearer token
- **Response Format:** Unified `{success, data, message, request_id}`
---
## IGNY8 Backend Integration Points
### Database Models
**Content Model** (`igny8_core/business/content/models.py`)
```python
class Content(SiteSectorBaseModel):
"""Main content storage in IGNY8"""
# Core fields
title = CharField(max_length=255)
content_html = TextField() # Final HTML content
word_count = IntegerField()
# SEO fields
meta_title = CharField(max_length=255)
meta_description = TextField()
primary_keyword = CharField(max_length=255)
secondary_keywords = JSONField(default=list)
# Content classification
cluster = ForeignKey('planner.Clusters') # Required
content_type = CharField(choices=['post', 'page', 'product', 'taxonomy'])
content_structure = CharField(choices=['article', 'guide', 'comparison', ...])
# WordPress sync fields
external_id = CharField() # WordPress post ID
external_url = URLField() # WordPress post URL
external_type = CharField() # WordPress post type
sync_status = CharField() # Sync status
# Status
status = CharField(choices=['draft', 'review', 'published'])
source = CharField(choices=['igny8', 'wordpress'])
```
**SiteIntegration Model** (`igny8_core/business/integration/models.py`)
```python
class SiteIntegration(AccountBaseModel):
"""WordPress connection configuration"""
site = ForeignKey('Site')
platform = CharField(choices=['wordpress', 'shopify', 'custom'])
# Configuration
config_json = JSONField() # {site_url, endpoints, ...}
credentials_json = JSONField() # {api_key, username, password}
# Sync tracking
is_active = BooleanField(default=True)
sync_enabled = BooleanField(default=False)
last_sync_at = DateTimeField()
sync_status = CharField(choices=['success', 'failed', 'pending', 'syncing'])
```
**PublishingRecord Model** (`igny8_core/business/publishing/models.py`)
```python
class PublishingRecord(SiteSectorBaseModel):
"""Track content publishing to destinations"""
content = ForeignKey('writer.Content')
destination = CharField() # 'wordpress', 'sites', 'shopify'
destination_id = CharField() # External platform ID
destination_url = URLField() # Published URL
status = CharField(choices=['pending', 'publishing', 'published', 'failed'])
published_at = DateTimeField()
error_message = TextField()
metadata = JSONField()
```
### Backend Services
**PublisherService** (`igny8_core/business/publishing/services/publisher_service.py`)
- **Purpose:** Main publishing orchestrator
- **Key Method:** `publish_content(content_id, destinations, account)`
- **Flow:**
1. Lookup Content by ID
2. For each destination → call `_publish_to_destination()`
3. Create PublishingRecord
4. Get adapter (WordPressAdapter)
5. Get SiteIntegration config
6. Call adapter.publish()
7. Update PublishingRecord status
**WordPressAdapter** (`igny8_core/business/publishing/services/adapters/wordpress_adapter.py`)
- **Purpose:** WordPress-specific publishing logic
- **Authentication:** Auto-detects API key vs username/password
- **Key Methods:**
- `publish(content, destination_config)` - Main publish method
- `_publish_via_api_key()` - Uses `/wp-json/igny8/v1/publish-content/`
- `_publish_via_username_password()` - Uses standard WP REST API
- **Payload:**
```python
{
'content_id': content.id,
'title': content.title,
'content_html': content.content_html,
'content_type': content.content_type,
'content_structure': content.content_structure,
'meta_title': content.meta_title,
'meta_description': content.meta_description,
'status': content.status,
'cluster_id': content.cluster.id,
'sector_id': content.sector.id,
'featured_image': {...},
'categories': [...],
'tags': [...]
}
```
**Celery Tasks** (`igny8_core/tasks/wordpress_publishing.py`)
- **Purpose:** Async background publishing
- **Task:** `publish_content_to_wordpress(content_id, site_integration_id)`
- **Features:**
- Retry logic (3 retries, exponential backoff)
- Duplicate check (prevents re-publishing)
- Updates Content.external_id and external_url
- Comprehensive logging
---
## Authentication & Security
### IGNY8 → WordPress Authentication
**Method 1: API Key (Preferred)**
- **Storage:** `SiteIntegration.credentials_json['api_key']`
- **WordPress Storage:** `igny8_api_key` option (encrypted if available)
- **Header:** `X-IGNY8-API-KEY: {api_key}` or `Authorization: Bearer {api_key}`
- **Endpoint:** `/wp-json/igny8/v1/publish-content/`
- **Security:** API key validated in `Igny8RestAPI::check_permission()`
**Method 2: Username/Password (Legacy)**
- **Storage:** `SiteIntegration.credentials_json['username']` and `password`
- **Flow:** Uses standard WordPress REST API authentication
- **Limitation:** Requires Application Password or JWT plugin
### WordPress → IGNY8 Authentication
**JWT Token Flow**
1. **Login:** `POST /api/v1/auth/login/` with `{email, password}`
2. **Response:** `{access, refresh}` tokens
3. **Storage:** `igny8_access_token` and `igny8_refresh_token` WordPress options
4. **Usage:** `Authorization: Bearer {access_token}` header
5. **Refresh:** On 401, call `POST /api/v1/auth/refresh/` with `{refresh}`
6. **Auto-retry:** `Igny8API` class automatically refreshes and retries on 401
**Credentials Required (WordPress Settings)**
- **Email:** `igny8_email` option
- **Password:** Stored temporarily for token refresh
- **API Key:** `igny8_api_key` option (for incoming requests from IGNY8)
---
## Bidirectional Sync: Data Elements
### IGNY8 → WordPress (Publishing)
**Content Fields Synced:**
| IGNY8 Field | WordPress Destination | Notes |
|-------------|----------------------|-------|
| `title` | `post_title` | Sanitized |
| `content_html` | `post_content` | Passed through `wp_kses_post()` |
| `meta_title` | `_yoast_wpseo_title`
`_seopress_titles_title`
`_aioseo_title`
`_igny8_meta_title` | Multi-SEO plugin support |
| `meta_description` | `_yoast_wpseo_metadesc`
`_seopress_titles_desc`
`_aioseo_description`
`_igny8_meta_description` | Multi-SEO plugin support |
| `status` | `post_status` | Mapped via `igny8_map_igny8_status_to_wp()` |
| `content_type` | `post_type` | `post`, `page`, `product` |
| `content_structure` | `_igny8_content_structure` | Meta field for tracking |
| `cluster.id` | `_igny8_cluster_id`
`igny8_clusters` taxonomy | Both meta and taxonomy term |
| `sector.id` | `_igny8_sector_id`
`igny8_sectors` taxonomy | Both meta and taxonomy term |
| `task_id` | `_igny8_task_id` | Task reference |
| `id` (Content) | `_igny8_content_id` | Content reference |
| `primary_keyword` | `_igny8_primary_keyword` | SEO keyword |
| `secondary_keywords` | `_igny8_secondary_keywords` | JSON array |
| `featured_image.url` | Featured Image (attachment) | Downloaded and attached |
| `gallery_images[]` | `_igny8_gallery_images` | Image attachments |
| `categories[]` | `category` taxonomy | Standard WP categories |
| `tags[]` | `post_tag` taxonomy | Standard WP tags |
**Post Meta Fields Created:**
```php
// IGNY8 identification
_igny8_task_id // Task ID from IGNY8
_igny8_content_id // Content ID from IGNY8
_igny8_cluster_id // Cluster ID
_igny8_sector_id // Sector ID
_igny8_source // 'igny8' or 'wordpress'
// Content metadata
_igny8_content_type // 'post', 'page', 'product'
_igny8_content_structure // 'article', 'guide', etc.
// SEO fields (generic)
_igny8_meta_title // SEO title
_igny8_meta_description // SEO description
_igny8_primary_keyword // Primary keyword
_igny8_secondary_keywords // JSON array
// Sync tracking
_igny8_wordpress_status // WordPress post_status
_igny8_last_synced // Last sync timestamp
_igny8_brief_cached_at // Brief cache timestamp
_igny8_task_brief // Cached task brief
```
### WordPress → IGNY8 (Status Sync)
**Fields Synced on Post Save:**
| WordPress Field | IGNY8 Endpoint | IGNY8 Field | Trigger |
|----------------|----------------|-------------|---------|
| `post_status` | `PUT /writer/tasks/{id}/` | `status` (mapped)
`wordpress_status` (raw) | `save_post` hook |
| `ID` | `PUT /writer/tasks/{id}/` | `assigned_post_id` | `save_post` hook |
| `permalink` | `PUT /writer/tasks/{id}/` | `post_url` | `save_post` hook |
| `post_type` | `PUT /writer/tasks/{id}/` | `post_type` | `save_post` hook |
| Post updated | `PUT /writer/tasks/{id}/` | `synced_at` | `save_post` hook |
**Sync Conditions:**
- Only syncs if post has `_igny8_task_id` meta (IGNY8-managed posts)
- Skips autosaves and revisions
- Only runs if connection is enabled (`igny8_is_connection_enabled()`)
- Implemented in `sync/post-sync.php`
### Site Metadata Discovery (WordPress → IGNY8)
**Endpoint:** `GET /wp-json/igny8/v1/site-metadata/`
**Data Collected:**
```json
{
"success": true,
"data": {
"site_info": {
"name": "Site Name",
"url": "https://example.com",
"description": "Site description",
"language": "en_US",
"timezone": "America/New_York"
},
"post_types": [
{
"name": "post",
"label": "Posts",
"count": 150,
"supports": ["title", "editor", "thumbnail", "excerpt"]
},
{
"name": "page",
"label": "Pages",
"count": 25
},
{
"name": "product",
"label": "Products",
"count": 80
}
],
"taxonomies": [
{
"name": "category",
"label": "Categories",
"count": 20,
"terms": [...]
},
{
"name": "post_tag",
"label": "Tags",
"count": 150
}
],
"plugins": {
"woocommerce": true,
"yoast_seo": true,
"advanced_custom_fields": false
}
}
}
```
**Cache:** Transient `igny8_site_metadata_v1` (5 minutes)
---
## Publishing Flow (IGNY8 → WordPress)
### Complete Flow (Frontend to WordPress)
**Step 1: User Clicks Publish Button**
- **File:** `frontend/src/pages/Writer/Review.tsx`
- **Method:** `handlePublishSingle()`
- **Action:** `POST /api/v1/publisher/publish/`
- **Payload:** `{content_id, destinations: ['wordpress']}`
**Step 2: Publisher API Endpoint**
- **File:** `igny8_core/modules/publisher/views.py`
- **Class:** `PublisherViewSet`
- **Method:** `publish()` action
- **Logging:** `🚀 Publish request received`
- **Action:** Call `PublisherService.publish_content()`
**Step 3: Publisher Service**
- **File:** `igny8_core/business/publishing/services/publisher_service.py`
- **Method:** `publish_content(content_id, destinations, account)`
- **Steps:**
1. Load Content from database
2. For each destination, call `_publish_to_destination()`
3. Create `PublishingRecord` (status='pending')
4. Get adapter for destination
5. Lookup `SiteIntegration` for site
6. Merge config: `{**config_json, **credentials_json, 'site_url': ...}`
7. Call `adapter.publish(content, config)`
8. Update `PublishingRecord` (status='published' or 'failed')
**Step 4: WordPress Adapter**
- **File:** `igny8_core/business/publishing/services/adapters/wordpress_adapter.py`
- **Method:** `publish(content, destination_config)`
- **Auto-detection:**
```python
if config.get('api_key'):
return _publish_via_api_key(...)
else:
return _publish_via_username_password(...)
```
**Step 4a: API Key Method (Preferred)**
- **Method:** `_publish_via_api_key()`
- **Endpoint:** `POST {site_url}/wp-json/igny8/v1/publish-content/`
- **Headers:** `X-IGNY8-API-KEY: {api_key}`
- **Payload:**
```json
{
"content_id": 123,
"title": "Post Title",
"content_html": "
...
", "content_type": "post", "content_structure": "article", "meta_title": "SEO Title", "meta_description": "SEO Description", "status": "published", "cluster_id": 45, "sector_id": 12, "featured_image": {...}, "categories": [...], "tags": [...] } ``` - **Response:** `{success, data: {post_id, url}}` **Step 5: WordPress REST Endpoint** - **File:** `includes/class-igny8-rest-api.php` - **Method:** `publish_content_to_wordpress()` - **Endpoint:** `/wp-json/igny8/v1/publish-content/` - **Permission:** `check_permission()` validates API key - **Action:** Call `igny8_create_wordpress_post_from_task($content_data)` **Step 6: WordPress Post Creation** - **File:** `sync/igny8-to-wp.php` - **Function:** `igny8_create_wordpress_post_from_task()` - **Steps:** 1. Resolve post type from content_type 2. Map author (via `igny8_map_content_author()`) 3. Prepare post data 4. `wp_insert_post()` - Create WordPress post 5. Import SEO metadata (`igny8_import_seo_metadata()`) 6. Import featured image (`igny8_import_featured_image()`) 7. Import taxonomies (`igny8_import_taxonomies()`) 8. Set cluster/sector taxonomy terms 9. Set categories/tags 10. Store IGNY8 meta fields 11. Update IGNY8 task via API (`PUT /writer/tasks/{id}/`) - **Return:** WordPress post ID or WP_Error **Step 7: Response Propagation** - WordPress → WordPress Adapter → Publisher Service → Publisher ViewSet → Frontend - **Frontend Success:** ```javascript console.log('[Review.handlePublishSingle] ✅ Publish successful:', response) toast.success('Published to WordPress!') ``` ### Alternative: Celery Background Task **When:** If using async publishing (currently available but not default) **Trigger:** `publish_content_to_wordpress.delay(content_id, site_integration_id)` **File:** `igny8_core/tasks/wordpress_publishing.py` **Benefits:** - Non-blocking (user doesn't wait) - Retry logic (3 attempts with exponential backoff) - Duplicate check (prevents re-publishing same content) **Flow:** 1. Load Content and SiteIntegration from DB 2. Check if already published (has external_id) 3. Prepare payload 4. POST to WordPress API 5. Update Content.external_id and external_url 6. Log success/failure --- ## Sync Flow (WordPress → IGNY8) ### Post Status Sync **Trigger:** WordPress `save_post`, `publish_post`, `transition_post_status` hooks **File:** `sync/post-sync.php` **Function:** `igny8_sync_post_status_to_igny8($post_id, $post, $update)` **Flow:** 1. Check if connection enabled 2. Skip autosaves and revisions 3. Check if IGNY8-managed (has `_igny8_task_id` meta) 4. Get `$task_id` from post meta 5. Get WordPress `post_status` 6. Map to IGNY8 status: `igny8_map_wp_status_to_igny8($post_status)` 7. Prepare update payload: ```php [ 'status' => $task_status, // IGNY8 mapped status 'assigned_post_id' => $post_id, 'post_url' => get_permalink($post_id), 'wordpress_status' => $post_status, // Raw WP status 'synced_at' => current_time('mysql'), 'content_id' => $content_id // If available ] ``` 8. `PUT /writer/tasks/{task_id}/` via Igny8API 9. Update `_igny8_wordpress_status` and `_igny8_last_synced` meta **Logged in WordPress:** ``` IGNY8: Synced post 123 status (publish) to task 456 ``` ### Keyword Status Update on Publish **Trigger:** Post published **Function:** `igny8_update_keywords_on_post_publish($post_id)` **Flow:** 1. Get task_id from post meta 2. Get task details: `GET /writer/tasks/{task_id}/` 3. Extract cluster_id and keyword_ids 4. Update keyword statuses: `PUT /planner/keywords/bulk-update/` 5. Mark keywords as "published" or "in_content" --- ## REST API Endpoints ### IGNY8 Backend Endpoints (Called by WordPress) | Method | Endpoint | Purpose | Request | Response | |--------|----------|---------|---------|----------| | POST | `/api/v1/auth/login/` | Get JWT tokens | `{email, password}` | `{access, refresh}` | | POST | `/api/v1/auth/refresh/` | Refresh access token | `{refresh}` | `{access, refresh}` | | GET | `/api/v1/writer/tasks/{id}/` | Get task details | - | Task object | | PUT | `/api/v1/writer/tasks/{id}/` | Update task status | `{status, assigned_post_id, post_url, wordpress_status}` | Updated task | | GET | `/api/v1/writer/tasks/{id}/brief/` | Get task brief | - | Brief content | | PUT | `/api/v1/planner/keywords/bulk-update/` | Update keyword statuses | `{keyword_ids, status}` | Updated keywords | | POST | `/api/v1/system/sites/{id}/import/` | Import site data | Site metadata payload | Import result | ### WordPress REST Endpoints (Called by IGNY8) | Method | Endpoint | Purpose | Auth | Response | |--------|----------|---------|------|----------| | GET | `/wp-json/igny8/v1/post-by-content-id/{id}` | Get post by Content ID | API key | `{success, data: {post_id, title, status, url}}` | | GET | `/wp-json/igny8/v1/post-by-task-id/{id}` | Get post by Task ID | API key | `{success, data: {post_id, title, status, url}}` | | GET | `/wp-json/igny8/v1/post-status/{content_id}` | Get post status | API key | `{success, data: {wordpress_status, igny8_status}}` | | GET | `/wp-json/igny8/v1/site-metadata/` | Get site structure | API key | `{success, data: {site_info, post_types, taxonomies}}` | | GET | `/wp-json/igny8/v1/status` | Plugin health check | Public | `{success, data: {connected, api_key_present}}` | | POST | `/wp-json/igny8/v1/publish-content/` | Publish content from IGNY8 | API key | `{success, data: {post_id, url}}` | **Authentication:** - **API Key:** `X-IGNY8-API-KEY` header or `Authorization: Bearer {api_key}` - **Validation:** `Igny8RestAPI::check_permission()` compares with `igny8_api_key` option --- ## Data Models & Meta Fields ### WordPress Post Meta Taxonomy **All IGNY8 meta fields use `_igny8_` prefix** ```php // IGNY8 References _igny8_task_id // INT - IGNY8 Task ID _igny8_content_id // INT - IGNY8 Content ID _igny8_cluster_id // INT - IGNY8 Cluster ID _igny8_sector_id // INT - IGNY8 Sector ID // Content Classification _igny8_content_type // STRING - 'post', 'page', 'product', 'taxonomy' _igny8_content_structure // STRING - 'article', 'guide', 'comparison', 'review', etc. _igny8_source // STRING - 'igny8' or 'wordpress' // SEO Fields (Generic - not plugin-specific) _igny8_meta_title // STRING - SEO meta title _igny8_meta_description // TEXT - SEO meta description _igny8_primary_keyword // STRING - Primary SEO keyword _igny8_secondary_keywords // JSON - ["keyword1", "keyword2"] // Keywords _igny8_keyword_ids // JSON - [1, 2, 3] // Sync Tracking _igny8_wordpress_status // STRING - WordPress post_status at last sync _igny8_last_synced // DATETIME - Last sync timestamp _igny8_brief_cached_at // DATETIME - Brief cache timestamp _igny8_task_brief // JSON - Cached task brief data // Images _igny8_gallery_images // JSON - Gallery image IDs ``` ### WordPress Custom Taxonomies **Created by Plugin:** ```php // IGNY8 Clusters register_taxonomy('igny8_clusters', ['post', 'page'], [ 'label' => 'IGNY8 Clusters', 'hierarchical' => true, 'show_in_rest' => true ]); // IGNY8 Sectors register_taxonomy('igny8_sectors', ['post', 'page'], [ 'label' => 'IGNY8 Sectors', 'hierarchical' => true, 'show_in_rest' => true ]); ``` **Term Meta:** - `_igny8_cluster_id` - IGNY8 cluster ID - `_igny8_sector_id` - IGNY8 sector ID ### WordPress Options ```php // Authentication igny8_email // User email for IGNY8 account igny8_access_token // JWT access token igny8_refresh_token // JWT refresh token igny8_api_key // API key (encrypted if available) // Connection igny8_connection_enabled // BOOL - Enable/disable sync igny8_connection_status // STRING - 'connected', 'disconnected', 'error' // Settings igny8_allowed_post_types // JSON - ['post', 'page', 'product'] igny8_auto_import_enabled // BOOL - Auto-import content from IGNY8 igny8_sync_mode // STRING - 'manual', 'auto' // Cache igny8_site_metadata_v1 // Transient (5 min) - Cached site metadata ``` ### IGNY8 Database Tables **Content Table** (`igny8_content`) - Primary table for all content - Fields: title, content_html, meta_title, meta_description, cluster_id, sector_id, status, external_id, external_url **Tasks Table** (`igny8_tasks`) - Content generation tasks - Fields: title, cluster_id, status, assigned_post_id, post_url **SiteIntegration Table** (`igny8_site_integrations`) - WordPress connection configs - Fields: site_id, platform, config_json, credentials_json, is_active, sync_status **PublishingRecord Table** (`igny8_publishing_records`) - Track all publish operations - Fields: content_id, destination, destination_id, destination_url, status, published_at --- ## Status Mapping ### IGNY8 Status → WordPress Status **Function:** `igny8_map_igny8_status_to_wp($igny8_status)` (WordPress) | IGNY8 Status | WordPress Status | Notes | |--------------|------------------|-------| | `completed` | `publish` | Published and live | | `draft` | `draft` | Draft/unpublished | | `pending` | `pending` | Pending review | | `scheduled` | `future` | Scheduled for future | | `archived` | `trash` | Trashed | | *(other)* | `draft` | Default fallback | ### WordPress Status → IGNY8 Status **Function:** `igny8_map_wp_status_to_igny8($wp_status)` (WordPress) | WordPress Status | IGNY8 Status | Notes | |-----------------|--------------|-------| | `publish` | `completed` | Published | | `future` | `completed` | Scheduled (counts as completed) | | `private` | `completed` | Private but complete | | `pending` | `pending` | Pending review | | `draft` | `draft` | Draft | | `auto-draft` | `draft` | Auto-draft | | `trash` | `archived` | Trashed | | *(other)* | `draft` | Default fallback | **Important:** WordPress sends raw `wordpress_status` AND mapped IGNY8 `status` to backend --- ## Troubleshooting Guide ### Common Issues #### 1. "Failed to publish: undefined" **Symptoms:** - Error message in frontend when clicking publish - No post created in WordPress **Diagnosis Steps:** 1. **Check browser console:** ```javascript [Review.handlePublishSingle] Starting publish for content: {id, title} [Review.handlePublishSingle] Calling API endpoint /v1/publisher/publish/ ``` 2. **Check Django logs:** ``` [PublisherViewSet.publish] 🚀 Publish request received: content_id=X [PublisherService.publish_content] 📄 Content found: title='...' [WordPressAdapter.publish] 🎬 Starting WordPress publish ``` 3. **Check WordPress logs:** ``` IGNY8: Creating WordPress post from IGNY8 content IGNY8: Post created with ID 123 ``` **Common Causes:** - ❌ SiteIntegration missing API key → Check `credentials_json['api_key']` - ❌ WordPress plugin not active → Verify plugin activated - ❌ API key mismatch → Compare IGNY8 and WordPress `api_key` - ❌ `site_url` not set → Check `SiteIntegration.config_json['site_url']` - ❌ Content missing `content_html` → Content.content_html is empty **Fixes:** - Ensure SiteIntegration has both `config_json['site_url']` and `credentials_json['api_key']` - Verify WordPress endpoint accessible: `GET {site_url}/wp-json/igny8/v1/status` - Check Content model has `content_html` field populated #### 2. Posts Not Syncing Back to IGNY8 **Symptoms:** - Edit post in WordPress, status not updated in IGNY8 - Post published in WordPress, keyword status not changing in IGNY8 **Diagnosis:** 1. Check if post has `_igny8_task_id`: `get_post_meta($post_id, '_igny8_task_id', true)` 2. Check connection enabled: `get_option('igny8_connection_enabled')` 3. Check WordPress error logs for API failures **Common Causes:** - Post not IGNY8-managed (missing `_igny8_task_id`) - Connection disabled in settings - Access token expired and refresh failed - Network/firewall blocking outbound requests **Fixes:** - Only IGNY8-created posts sync (have `_igny8_task_id`) - Enable connection in Settings → IGNY8 API - Re-authenticate: delete `igny8_access_token` and `igny8_refresh_token`, save settings again - Check server can reach `api.igny8.com` #### 3. SEO Metadata Not Appearing **Symptoms:** - Meta title/description not in Yoast/RankMath/SEOPress **Diagnosis:** 1. Check if generic meta exists: `get_post_meta($post_id, '_igny8_meta_title')` 2. Check SEO plugin-specific meta: `get_post_meta($post_id, '_yoast_wpseo_title')` **Common Causes:** - Content model missing `meta_title` or `meta_description` - SEO plugin not detected during import **Fixes:** - Ensure Content has `meta_title` and `meta_description` populated - Function `igny8_import_seo_metadata()` writes to all major SEO plugins - Manually copy from `_igny8_meta_title` to SEO plugin field #### 4. Images Not Importing **Symptoms:** - Featured image not set - Images in content not downloaded **Diagnosis:** 1. Check if `featured_image` in payload 2. Check WordPress upload permissions 3. Check `igny8_import_featured_image()` logs **Common Causes:** - Image URL not accessible (403, 404) - WordPress upload directory not writable - `allow_url_fopen` disabled in PHP **Fixes:** - Verify image URLs publicly accessible - Check `wp-content/uploads/` permissions (775 or 755) - Enable `allow_url_fopen` in `php.ini` #### 5. Duplicate Posts Created **Symptoms:** - Same content published multiple times in WordPress - Multiple posts with same `_igny8_content_id` **Diagnosis:** 1. Check if Celery task has duplicate check 2. Check if `external_id` set on Content after first publish **Common Causes:** - Celery task retrying after network error - Content.external_id not being saved - Multiple publish button clicks **Fixes:** - Celery task has duplicate check: `if content.external_id: return` - Ensure WordPressAdapter saves `external_id` after successful publish - Disable publish button after first click (frontend) ### Debug Mode **Enable WordPress Debug Logging:** ```php // wp-config.php define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); define('IGNY8_DEBUG', true); // Plugin-specific ``` **Enable Django Logging:** ```python # settings.py LOGGING = { 'version': 1, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'igny8_core.business.publishing': { 'handlers': ['console'], 'level': 'DEBUG', }, }, } ``` **Check Logs:** - WordPress: `wp-content/debug.log` - Django: Console output or configured log file - Browser: Console (F12 → Console tab) ### Health Check Endpoints **WordPress Plugin Status:** ```bash GET https://yoursite.com/wp-json/igny8/v1/status ``` Response: ```json { "success": true, "data": { "plugin_version": "1.0.0", "connected": true, "api_key_present": true, "token_present": true, "connection_enabled": true } } ``` **IGNY8 API Health:** ```bash GET https://api.igny8.com/api/v1/health/ ``` ### Common WordPress Errors **Error:** `IGNY8 API key not authenticated` - **Cause:** API key missing or invalid - **Fix:** Settings → IGNY8 API → Enter API key **Error:** `IGNY8 connection is disabled` - **Cause:** Connection toggle disabled in settings - **Fix:** Settings → IGNY8 API → Enable "Enable Connection" **Error:** `Post not found for this content ID` - **Cause:** WordPress post with `_igny8_content_id` doesn't exist - **Fix:** Content not yet published to WordPress or meta field missing --- ## Developer Quick Reference ### Quick Start: New Developer Onboarding **1. Understand the Architecture** - Read this document (WORDPRESS_INTEGRATION_REFERENCE.md) - Read MASTER_REFERENCE.md for overall IGNY8 architecture - Review `docs/WORDPRESS-PLUGIN-INTEGRATION.md` for plugin details **2. Setup Local Environment** **WordPress Plugin:** ```bash # Clone plugin cd wp-content/plugins/ git clone