Files
igny8/plugins/wordpress/source/igny8-wp-bridge/docs/COMPLETE-PUBLICATION-AUDIT.md
2026-01-13 09:23:54 +00:00

911 lines
36 KiB
Markdown

# Complete IGNY8 → WordPress Content Publication Audit
**Date:** November 29, 2025
**Scope:** End-to-end analysis of content publishing from IGNY8 backend to WordPress plugin
---
## Table of Contents
1. [Publication Flow Architecture](#publication-flow-architecture)
2. [Publication Triggers](#publication-triggers)
3. [Data Fields & Mappings](#data-fields--mappings)
4. [WordPress Storage Locations](#wordpress-storage-locations)
5. [Sync Functions & Triggers](#sync-functions--triggers)
6. [Status Mapping](#status-mapping)
7. [Technical Deep Dive](#technical-deep-dive)
---
## Publication Flow Architecture
### High-Level Flow Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ IGNY8 BACKEND (Django) │
│ │
│ 1. Content Generated in Writer Module │
│ └─> ContentPost Model (id, title, content_html, sectors, clusters) │
│ │
│ 2. Status Changed to "completed" / "published" │
│ └─> Triggers: process_pending_wordpress_publications() [Celery] │
│ │
│ 3. Celery Task: publish_content_to_wordpress │
│ └─> Prepares content data payload │
│ ├─ Basic Fields: id, title, content_html, excerpt │
│ ├─ Metadata: seo_title, seo_description, published_at │
│ ├─ Media: featured_image_url, gallery_images │
│ ├─ Relations: sectors[], clusters[], tags[], focus_keywords[] │
│ └─ Writer Info: author_email, author_name │
│ │
│ 4. REST API Call (POST) │
│ └─> http://wordpress.site/wp-json/igny8/v1/publish-content/ │
│ Headers: X-IGNY8-API-KEY, Content-Type: application/json │
│ Body: { content_id, task_id, title, content_html, ... } │
│ │
└──────────────────────────────────────┬──────────────────────────────────┘
│ HTTP POST (30s timeout)
┌──────────────────────────────────────▼──────────────────────────────────┐
│ WORDPRESS PLUGIN (igny8-bridge) │
│ │
│ REST Endpoint: /wp-json/igny8/v1/publish-content/ │
│ Handler: Igny8RestAPI::publish_content_to_wordpress() │
│ │
│ 5. Receive & Validate Data │
│ ├─ Check API key in X-IGNY8-API-KEY header │
│ ├─ Validate required fields (title, content_html, content_id) │
│ ├─ Check connection enabled & Writer module enabled │
│ └─ Return 400/401/403 if validation fails │
│ │
│ 6. Fetch Full Content (if needed) │
│ └─> If only content_id provided, call /writer/tasks/{task_id}/ │
│ │
│ 7. Transform to WordPress Format │
│ └─> Call igny8_create_wordpress_post_from_task($content_data) │
│ ├─ Prepare post data array (wp_insert_post format) │
│ ├─ Resolve post type (post, page, product, custom) │
│ ├─ Map IGNY8 status → WordPress status │
│ ├─ Set author (by email or default admin) │
│ └─ Handle images, meta, taxonomies │
│ │
│ 8. Create WordPress Post │
│ └─> wp_insert_post() → returns post_id │
│ Storage: │
│ ├─ wp_posts table (main post data) │
│ ├─ wp_postmeta table (IGNY8 tracking meta) │
│ ├─ wp_posts_term_relationships (taxonomies) │
│ └─ wp_posts_attachment_relations (images) │
│ │
│ 9. Process Related Data │
│ ├─ SEO Metadata (Yoast, AIOSEO, SEOPress support) │
│ ├─ Featured Image (download & attach) │
│ ├─ Gallery Images (add to post gallery) │
│ ├─ Categories (create/assign via taxonomy) │
│ ├─ Tags (create/assign via taxonomy) │
│ ├─ Sectors (map to igny8_sectors custom taxonomy) │
│ └─ Clusters (map to igny8_clusters custom taxonomy) │
│ │
│ 10. Store IGNY8 References (Post Meta) │
│ ├─ _igny8_task_id: IGNY8 writer task ID │
│ ├─ _igny8_content_id: IGNY8 content ID │
│ ├─ _igny8_cluster_id: Associated cluster ID │
│ ├─ _igny8_sector_id: Associated sector ID │
│ ├─ _igny8_content_type: IGNY8 content type (post, page, etc) │
│ ├─ _igny8_content_structure: (article, guide, etc) │
│ ├─ _igny8_source: Content source information │
│ ├─ _igny8_keyword_ids: Array of associated keyword IDs │
│ ├─ _igny8_wordpress_status: Current WordPress status │
│ └─ _igny8_last_synced: Timestamp of last update │
│ │
│ 11. Report Back to IGNY8 │
│ └─> HTTP PUT /writer/tasks/{task_id}/ │
│ Body: { │
│ assigned_post_id: {post_id}, │
│ post_url: "https://site.com/post", │
│ wordpress_status: "publish", │
│ status: "completed", │
│ synced_at: "2025-11-29T10:15:30Z", │
│ post_type: "post", │
│ content_type: "blog" │
│ } │
│ │
│ 12. Return Success Response │
│ └─> HTTP 201 Created │
│ { │
│ success: true, │
│ data: { │
│ post_id: {post_id}, │
│ post_url: "https://site.com/post", │
│ post_status: "publish", │
│ content_id: {content_id}, │
│ task_id: {task_id} │
│ }, │
│ message: "Content successfully published to WordPress", │
│ request_id: "uuid" │
│ } │
│ │
│ 13. Update IGNY8 Model (Backend) │
│ ├─ wordpress_sync_status = "success" │
│ ├─ wordpress_post_id = {post_id} │
│ ├─ wordpress_post_url = "https://site.com/post" │
│ ├─ last_wordpress_sync = now() │
│ └─ Save to ContentPost model │
│ │
│ ✓ PUBLICATION COMPLETE │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Publication Triggers
### Trigger 1: Celery Scheduled Task (Every 5 minutes)
**Function:** `process_pending_wordpress_publications()` in `igny8_core/tasks/wordpress_publishing.py`
**Trigger Mechanism:**
```python
# Runs periodically (configured in celerybeat)
@shared_task
def process_pending_wordpress_publications() -> Dict[str, Any]:
"""
Process all content items pending WordPress publication
Runs every 5 minutes
"""
pending_content = ContentPost.objects.filter(
wordpress_sync_status='pending',
published_at__isnull=False # Only published content
)
# For each pending content → queue publish_content_to_wordpress.delay()
```
**When Triggered:**
- Content status becomes `completed` and `published_at` is set
- Content not yet sent to WordPress (`wordpress_sync_status == 'pending'`)
- Runs automatically every 5 minutes via Celery Beat
---
### Trigger 2: Direct REST API Call (Manual/IGNY8 Frontend)
**Endpoint:** `POST /wp-json/igny8/v1/publish-content/`
**Handler:** `Igny8RestAPI::publish_content_to_wordpress()`
**When Called:**
- Manual publication from IGNY8 frontend UI
- Admin triggers "Publish to WordPress" action
- Via IGNY8 backend integration workflow
---
### Trigger 3: Webhook from IGNY8 (Event-Based)
**Handler:** `Igny8Webhooks::handle_task_published()` in `includes/class-igny8-webhooks.php`
**When Triggered:**
- IGNY8 sends webhook when task status → `completed`
- Event type: `task.published` or `content.published`
- Real-time notification from IGNY8 backend
---
## Data Fields & Mappings
### Complete Field Mapping Table
| IGNY8 Field | IGNY8 Type | WordPress Storage | WordPress Field/Meta | Notes |
|---|---|---|---|---|
| **Core Content** | | | | |
| `id` | int | postmeta | `_igny8_task_id` OR `_igny8_content_id` | Primary identifier |
| `title` | string | posts | `post_title` | Post title |
| `content_html` | string | posts | `post_content` | Main content (HTML) |
| `content` | string | posts | `post_content` | Fallback if `content_html` missing |
| `brief` / `excerpt` | string | posts | `post_excerpt` | Post excerpt |
| **Status & Publishing** | | | | |
| `status` | enum | posts | `post_status` | See Status Mapping table |
| `published_at` | datetime | posts | `post_date` | Publication date |
| `status` | string | postmeta | `_igny8_wordpress_status` | WP status snapshot |
| **Content Classification** | | | | |
| `content_type` | string | postmeta | `_igny8_content_type` | Type: post, page, article, blog |
| `content_structure` | string | postmeta | `_igny8_content_structure` | Structure: article, guide, etc |
| `post_type` | string | posts | `post_type` | WordPress post type |
| **Relationships** | | | | |
| `cluster_id` | int | postmeta | `_igny8_cluster_id` | Primary cluster |
| `sector_id` | int | postmeta | `_igny8_sector_id` | Primary sector |
| `clusters[]` | array | tax | `igny8_clusters` | Custom taxonomy terms |
| `sectors[]` | array | tax | `igny8_sectors` | Custom taxonomy terms |
| `keyword_ids[]` | array | postmeta | `_igny8_keyword_ids` | Array of keyword IDs |
| **Categories & Tags** | | | | |
| `categories[]` | array | tax | `category` | Standard WP categories |
| `tags[]` | array | tax | `post_tag` | Standard WP tags |
| **Author** | | | | |
| `author_email` | string | posts | `post_author` | Map to WP user by email |
| `author_name` | string | posts | `post_author` | Fallback if email not found |
| **Media** | | | | |
| `featured_image_url` | string | postmeta | `_thumbnail_id` | Downloaded & attached |
| `featured_image` | object | postmeta | `_thumbnail_id` | Object with URL, alt text |
| `gallery_images[]` | array | postmeta | `_igny8_gallery_images` | Array of image URLs/data |
| **SEO Metadata** | | | | |
| `seo_title` | string | postmeta | Yoast: `_yoast_wpseo_title` | SEO plugin support |
| | | postmeta | AIOSEO: `_aioseo_title` | All-in-One SEO |
| | | postmeta | SEOPress: `_seopress_titles_title` | SEOPress |
| | | postmeta | Generic: `_igny8_meta_title` | Fallback |
| `seo_description` | string | postmeta | Yoast: `_yoast_wpseo_metadesc` | Meta description |
| | | postmeta | AIOSEO: `_aioseo_description` | All-in-One SEO |
| | | postmeta | SEOPress: `_seopress_titles_desc` | SEOPress |
| | | postmeta | Generic: `_igny8_meta_description` | Fallback |
| **Additional Fields** | | | | |
| `source` | string | postmeta | `_igny8_source` | Content source |
| `focus_keywords[]` | array | postmeta | `_igny8_focus_keywords` | SEO keywords |
| **Sync Metadata** | | | | |
| `task_id` | int | postmeta | `_igny8_task_id` | IGNY8 task ID |
| `content_id` | int | postmeta | `_igny8_content_id` | IGNY8 content ID |
| (generated) | — | postmeta | `_igny8_last_synced` | Last sync timestamp |
| (generated) | — | postmeta | `_igny8_brief_cached_at` | Brief cache timestamp |
---
### Data Payload Sent from IGNY8 to WordPress
**HTTP Request Format:**
```http
POST /wp-json/igny8/v1/publish-content/ HTTP/1.1
Host: wordpress.site
Content-Type: application/json
X-IGNY8-API-KEY: {{api_key_from_wordpress_plugin}}
```
---
## WordPress Storage Locations
### 1. WordPress Posts Table (`wp_posts`)
**Core post data stored directly in posts table:**
| Column | IGNY8 Source | Example Value |
|--------|---|---|
| `ID` | (generated by WP) | 1842 |
| `post_title` | `title` | "Advanced SEO Strategies for 2025" |
| `post_content` | `content_html` / `content` | `<p>HTML content...</p>` |
| `post_excerpt` | `excerpt` / `brief` | "Learn SEO strategies..." |
| `post_status` | `status` (mapped) | `publish` |
| `post_type` | Resolved from `content_type` | `post` |
| `post_author` | `author_email` (lookup user ID) | `3` (admin user ID) |
| `post_date` | `published_at` | `2025-11-29 10:15:30` |
| `post_date_gmt` | `published_at` (GMT) | `2025-11-29 10:15:30` |
**Retrieval Query:**
```php
$post = get_post($post_id);
echo $post->post_title; // "Advanced SEO Strategies for 2025"
echo $post->post_content; // HTML content
echo $post->post_status; // "publish"
```
---
### 2. WordPress Post Meta Table (`wp_postmeta`)
**IGNY8 tracking and metadata stored as post meta:**
| Meta Key | Meta Value | Example | Purpose |
|----------|-----------|---------|---------|
| `_igny8_task_id` | int | `15` | Link to IGNY8 writer task |
| `_igny8_content_id` | int | `42` | Link to IGNY8 content |
| `_igny8_cluster_id` | int | `12` | Primary cluster reference |
| `_igny8_sector_id` | int | `5` | Primary sector reference |
| `_igny8_content_type` | string | `"blog"` | IGNY8 content type |
| `_igny8_content_structure` | string | `"article"` | Content structure type |
| `_igny8_source` | string | `"writer_module"` | Content origin |
| `_igny8_keyword_ids` | serialized array | `a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}` | Associated keywords |
| `_igny8_wordpress_status` | string | `"publish"` | Last known WP status |
| `_igny8_last_synced` | datetime | `2025-11-29 10:15:30` | Last sync timestamp |
| `_igny8_task_brief` | JSON string | `{...}` | Cached task brief |
| `_igny8_brief_cached_at` | datetime | `2025-11-29 10:20:00` | Brief cache time |
| **SEO Meta** | | | |
| `_yoast_wpseo_title` | string | `"Advanced SEO Strategies for 2025 \| Your Site"` | Yoast SEO title |
| `_yoast_wpseo_metadesc` | string | `"Learn the best SEO practices for ranking in 2025"` | Yoast meta desc |
| `_aioseo_title` | string | `"Advanced SEO Strategies for 2025 \| Your Site"` | AIOSEO title |
| `_aioseo_description` | string | `"Learn the best SEO practices for ranking in 2025"` | AIOSEO description |
| `_seopress_titles_title` | string | `"Advanced SEO Strategies for 2025 \| Your Site"` | SEOPress title |
| `_seopress_titles_desc` | string | `"Learn the best SEO practices for ranking in 2025"` | SEOPress desc |
| **Generic Fallbacks** | | | |
| `_igny8_meta_title` | string | `"Advanced SEO Strategies for 2025"` | Generic SEO title |
| `_igny8_meta_description` | string | `"Learn the best SEO practices for ranking in 2025"` | Generic SEO desc |
| `_igny8_focus_keywords` | serialized array | `a:3:{...}` | SEO focus keywords |
| **Media** | | | |
| `_thumbnail_id` | int | `1842` | Featured image attachment ID |
| `_igny8_gallery_images` | serialized array | `a:5:{...}` | Gallery image attachment IDs |
**Retrieval Query:**
```php
// Get IGNY8 metadata
$task_id = get_post_meta($post_id, '_igny8_task_id', true); // 15
$content_id = get_post_meta($post_id, '_igny8_content_id', true); // 42
$cluster_id = get_post_meta($post_id, '_igny8_cluster_id', true); // 12
$keyword_ids = get_post_meta($post_id, '_igny8_keyword_ids', true); // array
// Get SEO metadata
$seo_title = get_post_meta($post_id, '_yoast_wpseo_title', true);
$seo_desc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true);
// Get last sync info
$last_synced = get_post_meta($post_id, '_igny8_last_synced', true);
```
---
### 3. WordPress Taxonomies (`wp_terms` & `wp_term_relationships`)
**Categories and Tags:**
```sql
-- Categories
SELECT * FROM wp_terms t
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy = 'category' AND tr.object_id = {post_id};
-- Tags
SELECT * FROM wp_terms t
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy = 'post_tag' AND tr.object_id = {post_id};
```
**Retrieval Query:**
```php
// Get categories
$categories = wp_get_post_terms($post_id, 'category', array('fields' => 'all'));
foreach ($categories as $cat) {
echo $cat->name; // "Digital Marketing"
echo $cat->slug; // "digital-marketing"
}
// Get tags
$tags = wp_get_post_terms($post_id, 'post_tag', array('fields' => 'all'));
foreach ($tags as $tag) {
echo $tag->name; // "seo"
echo $tag->slug; // "seo"
}
```
**Custom Taxonomies (IGNY8-specific):**
```php
// Sectors taxonomy
wp_set_post_terms($post_id, [5, 8], 'igny8_sectors');
// Clusters taxonomy
wp_set_post_terms($post_id, [12, 15], 'igny8_clusters');
// Retrieval
$sectors = wp_get_post_terms($post_id, 'igny8_sectors', array('fields' => 'all'));
$clusters = wp_get_post_terms($post_id, 'igny8_clusters', array('fields' => 'all'));
```
---
### 4. Featured Image (Post Attachment)
**Process:**
1. Download image from `featured_image_url`
2. Upload to WordPress media library
3. Create attachment post
4. Set `_thumbnail_id` post meta to attachment ID
**Storage:**
```php
// Query featured image
$thumbnail_id = get_post_thumbnail_id($post_id);
$image_url = wp_get_attachment_image_url($thumbnail_id, 'full');
$image_alt = get_post_meta($thumbnail_id, '_wp_attachment_image_alt', true);
// In HTML
echo get_the_post_thumbnail($post_id, 'medium');
```
---
### 5. Gallery Images
**Storage Method:**
- Downloaded images stored as attachments
- Image IDs stored in `_igny8_gallery_images` post meta
- Can be serialized array or JSON
```php
// Store gallery images
$gallery_ids = [1842, 1843, 1844, 1845, 1846]; // 5 images max
update_post_meta($post_id, '_igny8_gallery_images', $gallery_ids);
// Retrieve gallery images
$gallery_ids = get_post_meta($post_id, '_igny8_gallery_images', true);
foreach ($gallery_ids as $img_id) {
echo wp_get_attachment_image($img_id, 'medium');
}
```
---
## Sync Functions & Triggers
### Core Sync Functions
#### 1. `publish_content_to_wordpress()` [IGNY8 Backend - Celery Task]
**File:** `igny8_core/tasks/wordpress_publishing.py`
**Trigger:** Every 5 minutes via Celery Beat
**Flow:**
```python
@shared_task(bind=True, max_retries=3)
def publish_content_to_wordpress(self, content_id: int, site_integration_id: int,
task_id: Optional[int] = None) -> Dict[str, Any]:
# 1. Get ContentPost and SiteIntegration models
# 2. Check if already published (wordpress_sync_status == 'success')
# 3. Set status to 'syncing'
# 4. Prepare content_data payload
# 5. POST to WordPress REST API
# 6. Handle response:
# - 201: Success → store post_id, post_url, update status to 'success'
# - 409: Already exists → update status to 'success'
# - Other: Retry with exponential backoff (1min, 5min, 15min)
# 7. Update ContentPost model
return {"success": True, "wordpress_post_id": post_id, "wordpress_post_url": url}
```
**Retry Logic:**
- Max retries: 3
- Backoff: 1 minute, 5 minutes, 15 minutes
- After max retries: Set status to `failed`
---
#### 2. `igny8_create_wordpress_post_from_task()` [WordPress Plugin]
**File:** `sync/igny8-to-wp.php`
**Trigger:**
- Called from REST API endpoint
- Called from webhook handler
- Called from manual sync
**Flow:**
```php
function igny8_create_wordpress_post_from_task($content_data, $allowed_post_types = array()) {
// 1. Resolve post type (post, page, product, custom)
// 2. Check if post type is enabled
// 3. Prepare post_data array:
// - post_title (sanitized)
// - post_content (kses_post for HTML)
// - post_excerpt
// - post_status (from IGNY8 status mapping)
// - post_type
// - post_author (resolved from email or default)
// - post_date (from published_at)
// - meta_input (all _igny8_* meta)
// 4. wp_insert_post() → get post_id
// 5. Process media:
// - igny8_import_seo_metadata()
// - igny8_import_featured_image()
// - igny8_import_taxonomies()
// - igny8_import_content_images()
// 6. Assign custom taxonomies (sectors, clusters)
// 7. Assign categories and tags
// 8. Store IGNY8 references in post meta
// 9. Update IGNY8 task via API (PUT /writer/tasks/{id}/)
// 10. Return post_id
}
```
---
#### 3. `igny8_sync_igny8_tasks_to_wp()` [WordPress Plugin - Batch Sync]
**File:** `sync/igny8-to-wp.php`
**Trigger:**
- Manual sync button in admin
- Scheduled cron job (optional)
- Initial site setup
**Flow:**
```php
function igny8_sync_igny8_tasks_to_wp($filters = array()) {
// 1. Check connection enabled & authenticated
// 2. Get enabled post types
// 3. Build API endpoint: /writer/tasks/?site_id={id}&status={status}&cluster_id={id}
// 4. GET from IGNY8 API → get tasks array
// 5. For each task:
// a. Check if post exists (by _igny8_task_id meta)
// b. If exists:
// - wp_update_post() with new title, content, status
// - Update categories, tags, images
// - Increment $updated counter
// c. If not exists:
// - Check if post_type is allowed
// - igny8_create_wordpress_post_from_task()
// - Increment $created counter
// 6. Return { success, created, updated, failed, skipped, total }
}
```
---
### WordPress Hooks (Two-Way Sync)
#### Hook 1: `save_post` [WordPress → IGNY8]
**File:** `docs/WORDPRESS-PLUGIN-INTEGRATION.md` & implementation in plugin
**When Triggered:** Post is saved (any status change)
**Actions:**
```php
add_action('save_post', function($post_id) {
// 1. Check if IGNY8-managed (has _igny8_task_id)
// 2. Get task_id from post meta
// 3. Map WordPress status → IGNY8 status
// 4. PUT /writer/tasks/{task_id}/ with:
// - status: mapped IGNY8 status
// - assigned_post_id: WordPress post ID
// - post_url: permalink
}, 10, 1);
```
**Status Map:**
- `publish``completed`
- `draft``draft`
- `pending``pending`
- `private``completed`
- `trash``archived`
- `future``scheduled`
---
#### Hook 2: `publish_post` [WordPress → IGNY8 + Keywords]
**File:** `docs/WORDPRESS-PLUGIN-INTEGRATION.md`
**When Triggered:** Post changes to `publish` status
**Actions:**
```php
add_action('publish_post', function($post_id) {
// 1. Get _igny8_task_id from post meta
// 2. GET /writer/tasks/{task_id}/ to get cluster_id
// 3. GET /planner/keywords/?cluster_id={cluster_id}
// 4. For each keyword: PUT /planner/keywords/{id}/ { status: 'mapped' }
// 5. Update task status to 'completed'
}, 10, 1);
```
---
#### Hook 3: `transition_post_status` [WordPress → IGNY8]
**File:** `sync/hooks.php` & `docs/WORDPRESS-PLUGIN-INTEGRATION.md`
**When Triggered:** Post status changes
**Actions:**
```php
add_action('transition_post_status', function($new_status, $old_status, $post) {
if ($new_status === $old_status) return;
$task_id = get_post_meta($post->ID, '_igny8_task_id', true);
if (!$task_id) return;
// Map status and PUT to IGNY8
$igny8_status = igny8_map_wp_status_to_igny8($new_status);
$api->put("/writer/tasks/{$task_id}/", [
'status' => $igny8_status,
'assigned_post_id' => $post->ID,
'post_url' => get_permalink($post->ID)
]);
}, 10, 3);
```
---
#### Hook 4: Webhook Handler [IGNY8 → WordPress]
**File:** `includes/class-igny8-webhooks.php`
**Endpoint:** `POST /wp-json/igny8/v1/webhook/`
**Webhook Event Types:**
- `task.published` / `task.completed`
- `content.published`
**Handler:**
```php
public function handle_task_published($data) {
$task_id = $data['task_id'];
// Check if post exists (by _igny8_task_id)
$existing_posts = get_posts([
'meta_key' => '_igny8_task_id',
'meta_value' => $task_id,
'post_type' => 'any',
'posts_per_page' => 1
]);
if (!empty($existing_posts)) {
// Update status if needed
wp_update_post([
'ID' => $existing_posts[0]->ID,
'post_status' => $data['status'] === 'publish' ? 'publish' : 'draft'
]);
} else {
// Create new post
$api->get("/writer/tasks/{$task_id}/");
igny8_create_wordpress_post_from_task($content_data, $enabled_post_types);
}
}
```
---
## Status Mapping
### IGNY8 Status ↔ WordPress Status
| IGNY8 Status | WordPress Status | Description | Sync Direction |
|---|---|---|---|
| `draft` | `draft` | Content is draft | ↔ Bidirectional |
| `completed` | `publish` | Content published/completed | ↔ Bidirectional |
| `pending` | `pending` | Content pending review | ↔ Bidirectional |
| `scheduled` | `future` | Content scheduled for future | → IGNY8 only |
| `archived` | `trash` | Content archived/deleted | → IGNY8 only |
| (WP publish) | `publish` | WordPress post published | → IGNY8 (mapped to `completed`) |
**Mapping Functions:**
```php
// IGNY8 → WordPress
function igny8_map_igny8_status_to_wp($igny8_status) {
$map = [
'completed' => 'publish',
'draft' => 'draft',
'pending' => 'pending',
'scheduled' => 'future',
'archived' => 'trash'
];
return $map[$igny8_status] ?? 'draft';
}
// WordPress → IGNY8
function igny8_map_wp_status_to_igny8($wp_status) {
$map = [
'publish' => 'completed',
'draft' => 'draft',
'pending' => 'pending',
'private' => 'completed',
'trash' => 'archived',
'future' => 'scheduled'
];
return $map[$wp_status] ?? 'draft';
}
```
---
## Technical Deep Dive
### API Authentication Flow
**IGNY8 Backend → WordPress:**
1. WordPress Admin stores API key: `Settings → IGNY8 → API Key`
- Stored in `igny8_api_key` option
- May be encrypted if `igny8_get_secure_option()` available
2. WordPress Plugin stores in REST API response:
- `GET /wp-json/igny8/v1/status` returns `has_api_key: true/false`
3. IGNY8 Backend stores WordPress API key:
- In `Site.wp_api_key` field (SINGLE source of truth)
- Sent in every request as `X-IGNY8-API-KEY` header
- Note: SiteIntegration model is for sync tracking, NOT authentication
4. WordPress Plugin validates:
```php
public function check_permission($request) {
$header_api_key = $request->get_header('x-igny8-api-key');
$stored_api_key = igny8_get_secure_option('igny8_api_key');
if ($stored_api_key && hash_equals($stored_api_key, $header_api_key)) {
return true; // Authenticated
}
}
```
---
### Error Handling & Retry Logic
**IGNY8 Backend Celery Task Retries:**
```python
@shared_task(bind=True, max_retries=3)
def publish_content_to_wordpress(self, content_id, ...):
try:
response = requests.post(wordpress_url, json=content_data, timeout=30)
if response.status_code == 201:
# Success
content.wordpress_sync_status = 'success'
content.save()
return {"success": True}
elif response.status_code == 409:
# Conflict - content already exists
content.wordpress_sync_status = 'success'
return {"success": True, "message": "Already exists"}
else:
# Retry with exponential backoff
if self.request.retries < self.max_retries:
countdown = 60 * (5 ** self.request.retries) # 1min, 5min, 15min
raise self.retry(countdown=countdown, exc=Exception(error_msg))
else:
# Max retries reached
content.wordpress_sync_status = 'failed'
content.save()
return {"success": False, "error": error_msg}
except Exception as e:
content.wordpress_sync_status = 'failed'
content.save()
return {"success": False, "error": str(e)}
```
**WordPress Plugin Response Codes:**
```
201 Created → Success, post created
409 Conflict → Content already exists (OK)
400 Bad Request → Missing required fields
401 Unauthorized → Invalid API key
403 Forbidden → Connection disabled
404 Not Found → Endpoint not found
500 Server Error → Internal WP error
```
---
### Cache & Performance
**Transients (5-minute cache):**
```php
// Site metadata caching
$cache_key = 'igny8_site_metadata_v1';
$cached = get_transient($cache_key);
if ($cached !== false) {
return $cached; // Use cache
}
// Cache for 5 minutes
set_transient($cache_key, $data, 300);
```
**Query Optimization:**
```php
// Batch checking for existing posts
$existing_posts = get_posts([
'meta_key' => '_igny8_task_id',
'meta_value' => $task_id,
'posts_per_page' => 1,
'fields' => 'ids' // Only get IDs, not full post objects
]);
```
---
### Logging & Debugging
**Enable Debug Logging:**
```php
// In wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('IGNY8_DEBUG', true); // Custom plugin debug flag
```
**Log Locations:**
- WordPress: `/wp-content/debug.log`
- IGNY8 Backend: `logs/` directory (Django settings)
**Example Logs:**
```
[2025-11-29 10:15:30] IGNY8: Created WordPress post 1842 from task 15
[2025-11-29 10:15:31] IGNY8: Updated task 15 with WordPress post ID 1842
[2025-11-29 10:15:35] IGNY8: Synced post 1842 status to task 15: completed
```
---
## Summary Table: Complete End-to-End Field Flow
| Step | IGNY8 Field | Transmitted As | WordPress Storage | Retrieval Method |
|---|---|---|---|---|
| 1 | Content ID | `content_id` in JSON | `_igny8_content_id` meta | `get_post_meta($pid, '_igny8_content_id')` |
| 2 | Title | `title` in JSON | `post_title` column | `get_the_title($post_id)` |
| 3 | Content HTML | `content_html` in JSON | `post_content` column | `get_the_content()` or `$post->post_content` |
| 4 | Status | `status` in JSON (mapped) | `post_status` column | `get_post_status($post_id)` |
| 5 | Author Email | `author_email` in JSON | Lookup user ID → `post_author` | `get_the_author_meta('email', $post->post_author)` |
| 6 | Task ID | `task_id` in JSON | `_igny8_task_id` meta | `get_post_meta($pid, '_igny8_task_id')` |
| 7 | Cluster ID | `cluster_id` in JSON | `_igny8_cluster_id` meta | `get_post_meta($pid, '_igny8_cluster_id')` |
| 8 | Categories | `categories[]` in JSON | `category` taxonomy | `wp_get_post_terms($pid, 'category')` |
| 9 | SEO Title | `seo_title` in JSON | Multiple meta keys | `get_post_meta($pid, '_yoast_wpseo_title')` |
| 10 | Featured Image | `featured_image_url` in JSON | `_thumbnail_id` meta | `get_post_thumbnail_id($post_id)` |
---
## Conclusion
The IGNY8 → WordPress integration is a **robust, bidirectional sync** system with:
**Multiple entry points** (Celery tasks, REST APIs, webhooks)
**Comprehensive field mapping** (50+ data points synchronized)
**Flexible storage** (posts, postmeta, taxonomies, attachments)
**Error handling & retries** (exponential backoff up to 3 retries)
**Status synchronization** (6-way bidirectional status mapping)
**Media handling** (featured images, galleries, SEO metadata)
**Two-way sync hooks** (WordPress changes → IGNY8, IGNY8 changes → WordPress)
**Authentication** (API key validation on every request)
The system ensures data consistency across both platforms while maintaining independence and allowing manual overrides where needed.
---
**Generated:** 2025-11-29
**Audit Scope:** Complete publication workflow analysis
**Coverage:** IGNY8 Backend + WordPress Plugin integration