# 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": "

Introduction

...

", "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 igny8-bridge # Activate plugin in WordPress admin # Go to Settings → IGNY8 API # Enter credentials: Email, Password, API Key ``` **IGNY8 Backend:** ```bash cd igny8/backend pip install -r requirements.txt python manage.py migrate python manage.py runserver # Start Celery (for async tasks) celery -A igny8_core worker -l info ``` **3. Test Publishing Flow** ```bash # 1. Create Content in IGNY8 # 2. Click Publish in Review page # 3. Check browser console for logs # 4. Check Django logs for backend flow # 5. Check WordPress for created post # 6. Edit post in WordPress # 7. Check IGNY8 for updated status ``` ### Key Files to Modify **To Change Publishing Payload:** - `igny8_core/business/publishing/services/adapters/wordpress_adapter.py` (IGNY8) - `sync/igny8-to-wp.php` → `igny8_create_wordpress_post_from_task()` (WordPress) **To Add New Meta Field:** - `wordpress_adapter.py` → Add to payload - `igny8-to-wp.php` → Add to `meta_input` array - Update this document's meta fields table **To Add New REST Endpoint:** - `includes/class-igny8-rest-api.php` → `register_routes()` - Add permission callback - Add handler method **To Change Status Mapping:** - `sync/igny8-to-wp.php` → `igny8_map_igny8_status_to_wp()` - `sync/post-sync.php` → `igny8_map_wp_status_to_igny8()` ### Testing Checklist **IGNY8 → WordPress Publishing** - [ ] Create content in IGNY8 Writer - [ ] Content has title, content_html, cluster - [ ] Click Publish button - [ ] Verify post created in WordPress - [ ] Check post has correct title and content - [ ] Verify SEO meta fields populated - [ ] Check featured image attached - [ ] Verify categories/tags assigned - [ ] Confirm cluster/sector taxonomies set - [ ] Check all `_igny8_*` meta fields present **WordPress → IGNY8 sync** - [ ] Create IGNY8 content and publish to WP - [ ] Edit post in WordPress (change status to draft) - [ ] Check IGNY8 task status updated to 'draft' - [ ] Publish post in WordPress - [ ] Check IGNY8 task status updated to 'completed' - [ ] Verify `wordpress_status` field shows 'publish' **Site Metadata Discovery** - [ ] Call `GET /wp-json/igny8/v1/site-metadata/` - [ ] Verify post types listed - [ ] Check taxonomies included - [ ] Confirm counts accurate - [ ] Check WooCommerce detected (if installed) ### Code Snippets **WordPress: Get IGNY8 Content ID from Post** ```php $content_id = get_post_meta($post_id, '_igny8_content_id', true); if ($content_id) { echo "This post is IGNY8 Content ID: " . $content_id; } ``` **WordPress: Check if Post is IGNY8-Managed** ```php function is_igny8_post($post_id) { $task_id = get_post_meta($post_id, '_igny8_task_id', true); return !empty($task_id); } ``` **IGNY8: Get WordPress Post ID from Content** ```python from igny8_core.business.content.models import Content content = Content.objects.get(id=123) if content.external_id: print(f"WordPress Post ID: {content.external_id}") print(f"WordPress URL: {content.external_url}") ``` **IGNY8: Manually Trigger Publish** ```python from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress # Queue Celery task publish_content_to_wordpress.delay( content_id=123, site_integration_id=1 ) ``` **WordPress: Call IGNY8 API** ```php $api = new Igny8API(); $response = $api->get('/writer/tasks/123/'); if ($response['success']) { $task = $response['data']; echo $task['title']; } ``` ### Useful Queries **WordPress: Find all IGNY8-managed posts** ```sql SELECT p.ID, p.post_title, pm.meta_value as content_id FROM wp_posts p INNER JOIN wp_postmeta pm ON p.ID = pm.post_id WHERE pm.meta_key = '_igny8_content_id' ORDER BY p.ID DESC; ``` **IGNY8: Find all content published to WordPress** ```sql SELECT id, title, external_id, external_url, sync_status FROM igny8_content WHERE external_id IS NOT NULL ORDER BY updated_at DESC; ``` **WordPress: Find posts missing IGNY8 sync** ```sql SELECT p.ID, p.post_title FROM wp_posts p LEFT JOIN wp_postmeta pm ON p.ID = pm.post_id AND pm.meta_key = '_igny8_content_id' WHERE p.post_type = 'post' AND p.post_status = 'publish' AND pm.meta_value IS NULL; ``` ### Integration Roadmap **Phase 1: Core Publishing (✅ Complete)** - [x] IGNY8 → WordPress content publishing - [x] WordPress → IGNY8 status sync - [x] Authentication (JWT + API Key) - [x] SEO metadata sync - [x] Featured images - [x] Taxonomies (categories, tags, clusters, sectors) **Phase 2: Enhanced Sync (🔄 In Progress)** - [x] Site metadata discovery - [ ] Automated content import from WordPress - [ ] Bulk publishing - [ ] Scheduled publishing - [ ] Content update sync (detect WordPress edits) **Phase 3: Advanced Features (📋 Planned)** - [ ] Internal linking recommendations - [ ] Content optimization scoring - [ ] Link graph sync - [ ] Automated interlinking - [ ] Content analytics sync **Phase 4: Automation (📋 Planned)** - [ ] Webhooks (IGNY8 → WordPress events) - [ ] WP-CLI commands - [ ] Automated content refresh - [ ] SEO score monitoring - [ ] Broken link detection --- ## Appendix: Data Flow Diagrams ### Complete Publishing Architecture ``` ┌────────────────────────────────────────────────────────────────┐ │ IGNY8 SaaS Platform │ ├────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Frontend (React) │ │ │ │ ──────────────── │ │ │ │ Review.tsx → Publish Button │ │ │ │ POST /api/v1/publisher/publish/ │ │ │ │ {content_id: 123, destinations: ['wordpress']} │ │ │ └────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ API Layer (Django REST Framework) │ │ │ │ ────────────────────────────────── │ │ │ │ PublisherViewSet.publish() │ │ │ │ - Validate request │ │ │ │ - Call PublisherService │ │ │ └────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Business Logic (Services) │ │ │ │ ───────────────────────────── │ │ │ │ PublisherService.publish_content() │ │ │ │ 1. Load Content from DB │ │ │ │ 2. Create PublishingRecord │ │ │ │ 3. Get SiteIntegration config │ │ │ │ 4. Call WordPressAdapter │ │ │ └────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Adapter Layer (Platform-Specific) │ │ │ │ ────────────────────────────────── │ │ │ │ WordPressAdapter.publish() │ │ │ │ - Detect auth method (API key vs user/pass) │ │ │ │ - Prepare payload │ │ │ │ - POST to WordPress REST API │ │ │ │ - Handle response │ │ │ └────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ │ HTTPS Request │ │ │ POST /wp-json/igny8/v1/publish-content/ │ │ │ X-IGNY8-API-KEY: *** │ └───────────────────────┼─────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────────┐ │ WordPress Site (yoursite.com) │ ├────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ WordPress REST API │ │ │ │ ────────────────── │ │ │ │ /wp-json/igny8/v1/publish-content/ │ │ │ │ - Validate API key │ │ │ │ - Parse request │ │ │ │ - Call content creation handler │ │ │ └────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Content Handler (PHP) │ │ │ │ ───────────────────────── │ │ │ │ igny8_create_wordpress_post_from_task() │ │ │ │ 1. Resolve post type │ │ │ │ 2. Prepare post data │ │ │ │ 3. wp_insert_post() │ │ │ │ 4. Import SEO metadata │ │ │ │ 5. Import featured image │ │ │ │ 6. Set taxonomies │ │ │ │ 7. Store IGNY8 meta fields │ │ │ └────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ WordPress Database │ │ │ │ ────────────────── │ │ │ │ wp_posts │ │ │ │ - ID, post_title, post_content, post_status │ │ │ │ │ │ │ │ wp_postmeta │ │ │ │ - _igny8_content_id, _igny8_task_id, _igny8_cluster_id │ │ │ │ - _igny8_meta_title, _igny8_meta_description │ │ │ │ │ │ │ │ wp_term_relationships │ │ │ │ - Categories, Tags, Clusters, Sectors │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` --- **End of Document** For questions or issues, consult: - Main documentation: `/docs/MASTER_REFERENCE.md` - Plugin docs: `/igny8-wp-integration/docs/WORDPRESS-PLUGIN-INTEGRATION.md` - Publishing fixes: `/igny8/FIXES-PUBLISH-FAILURE.md` - GitHub Issues: [Repository Issues](https://github.com/alorig/igny8-wp-integration/issues)