911 lines
36 KiB
Markdown
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
|