65 KiB
WordPress Integration Complete Refactor Plan
Date: December 1, 2025
Status: 🔴 Critical Issues Identified - Requires Complete Fix
Author: GitHub Copilot
Executive Summary
The WordPress integration is partially working but has critical data flow and synchronization issues:
✅ What's Working
- ✅ Backend sends HTTP requests successfully to WordPress
- ✅ WordPress creates posts without errors
- ✅ Authentication (API key) works correctly
- ✅ Content title and HTML are being sent
❌ What's Broken
- Categories/Tags/Images Not Publishing - WordPress logs show "No categories/tags/images in content_data" despite IGNY8 sending them
- Status Sync Broken - Content stays in "review" after WordPress publish, should change to "published"
- Sync Button Republishes Everything - Should only update counts, not republish all content
- Field Name Mismatches - IGNY8 sends different field names than WordPress plugin expects
Issue Analysis & Root Causes
Issue 1: Categories/Tags/Images Not Appearing in WordPress
Current Behavior:
- IGNY8 Backend logs:
Categories: 1,Tags: 4,Featured image: Yes - WordPress Plugin logs:
⚠️ No categories in content_data,⚠️ No tags in content_data,⚠️ No featured image in content_data - Result: 28 posts created, all "Uncategorized", no tags, no images
Root Cause: Field name mismatch between IGNY8 payload and WordPress plugin expectations
IGNY8 Backend Sends:
# File: backend/igny8_core/tasks/wordpress_publishing.py
content_data = {
'categories': categories, # Array of category names
'tags': tags, # Array of tag names
'featured_image_url': featured_image_url, # Image URL
'gallery_images': gallery_images # Array of image URLs
}
WordPress Plugin Expects (OLD CODE):
// File: igny8-wp-plugin/includes/class-igny8-rest-api.php line 509-515
error_log('Categories: ' . (isset($content_data['categories']) ? json_encode($content_data['categories']) : 'MISSING'));
error_log('Tags: ' . (isset($content_data['tags']) ? json_encode($content_data['tags']) : 'MISSING'));
// File: igny8-wp-plugin/sync/igny8-to-wp.php line 200-225
if (!empty($content_data['categories'])) {
// Process categories
}
if (!empty($content_data['tags'])) {
// Process tags
}
Analysis: WordPress plugin DOES expect the correct field names! The issue is that the data is being sent correctly, but the WordPress plugin logs show "MISSING" - this suggests the data is being lost somewhere in the request pipeline.
Hypothesis: The issue is likely in how the WordPress REST endpoint receives the JSON data. Let me check the actual endpoint implementation.
Issue 2: Status Not Syncing from WordPress → IGNY8
Current Behavior:
- User publishes content from IGNY8 Writer page
- Content status in IGNY8:
review - WordPress creates post with status:
publish - Expected: IGNY8 status should change to
published - Actual: IGNY8 status stays
review
Root Cause: WordPress plugin sends webhook but IGNY8 only updates status if WordPress status changes from draft → publish
Current Webhook Flow:
WordPress Plugin (after post creation)
↓
Sends webhook: POST /api/v1/integration/webhooks/wordpress/status/
↓
IGNY8 Backend: webhooks.py line 156-161
- Only updates status if: post_status == 'publish' AND old_status != 'published'
- Problem: Content comes from IGNY8 with status='review', should update to 'published'
Fix Required: Update webhook logic to properly sync status regardless of previous state
Issue 3: Sync Button Republishes Everything
Current Behavior:
- User clicks "Sync Now" in Site Settings → Content Types tab
- Frontend calls:
integrationApi.syncIntegration(integration_id, 'incremental') - Backend endpoint:
POST /v1/integration/integrations/{id}/sync/ - Actual result: Republishes ALL published content to WordPress (100 items limit)
Expected Behavior:
- Should only fetch WordPress site structure (post counts, taxonomy counts)
- Should NOT republish content
- Should update the Content Types table with fresh counts
Root Cause: Sync service syncs content instead of just fetching metadata
Current Sync Implementation:
# File: backend/igny8_core/business/integration/services/content_sync_service.py line 90-155
def _sync_to_wordpress(self, integration, content_types):
# Gets all Content.objects with status='publish'
# Publishes up to 100 items via WordPressAdapter
# This is WRONG for "Sync Now" button - should only fetch metadata
Fix Required: Separate "sync metadata" from "publish content" operations
Complete Data Flow Analysis
Current Flow: IGNY8 → WordPress (Publishing)
┌──────────────────────────────────────────────────────────────────────────┐
│ 1. User Action: Click "Publish to WordPress" on Writer Review page │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 2. Frontend: WordPressPublish.tsx │
│ - Calls: POST /v1/publisher/publish/ │
│ - Body: { content_id: 123, destinations: ['wordpress'] } │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 3. Backend: PublisherViewSet.publish() (publisher/views.py) │
│ - Validates content exists │
│ - Calls: PublisherService.publish_content() │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 4. Backend: PublisherService.publish_content() │
│ - For each destination (wordpress): │
│ - Get SiteIntegration config │
│ - Call: WordPressAdapter.publish() │
│ - Create PublishingRecord │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 5. Backend: WordPressAdapter.publish() (adapters/wordpress_adapter.py) │
│ - Checks auth method (API key vs username/password) │
│ - Calls: _publish_via_api_key() │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 6. Backend: Celery Task - publish_content_to_wordpress() │
│ - Extract categories from taxonomy_terms (taxonomy_type='category') │
│ - Extract tags from taxonomy_terms (taxonomy_type='tag') + keywords │
│ - Extract images from Images model │
│ - Convert image paths: /data/app/.../public/images/... → │
│ https://app.igny8.com/images/... │
│ - Build payload with categories[], tags[], featured_image_url, etc. │
│ - Send: POST {site_url}/wp-json/igny8/v1/publish │
│ - Header: X-IGNY8-API-Key: {api_key} │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 7. WordPress: REST Endpoint /igny8/v1/publish (class-igny8-rest-api.php) │
│ - Receives JSON payload │
│ - Validates API key │
│ ⚠️ ISSUE: Logs show "No categories/tags/images in content_data" │
│ - Calls: igny8_create_wordpress_post_from_task($content_data) │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 8. WordPress: igny8_create_wordpress_post_from_task() (igny8-to-wp.php) │
│ - Creates WordPress post with wp_insert_post() │
│ - Processes categories: igny8_process_categories() │
│ - Processes tags: igny8_process_tags() │
│ - Sets featured image: igny8_set_featured_image() │
│ ⚠️ ISSUE: Empty arrays passed because data missing │
│ - Returns: WordPress post ID │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 9. WordPress: Should send webhook back to IGNY8 │
│ - Endpoint: POST /api/v1/integration/webhooks/wordpress/status/ │
│ - Body: { content_id, post_id, post_status: 'publish', ... } │
│ ⚠️ ISSUE: Webhook logic only updates if old_status != 'published' │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 10. Backend: Webhook updates Content model │
│ - Sets external_id = WordPress post_id │
│ - Sets external_url = WordPress post URL │
│ ❌ ISSUE: Status stays 'review' instead of changing to 'published' │
└──────────────────────────────────────────────────────────────────────────┘
Current Flow: "Sync Now" Button
┌──────────────────────────────────────────────────────────────────────────┐
│ 1. User Action: Click "Sync Now" in Site Settings → Content Types │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 2. Frontend: Settings.tsx line 398 │
│ - Calls: integrationApi.syncIntegration(integration_id, 'incremental')│
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 3. Frontend: integration.api.ts line 92 │
│ - Sends: POST /v1/integration/integrations/{id}/sync/ │
│ - Body: { sync_type: 'incremental' } │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 4. Backend: IntegrationViewSet.sync() (integration/views.py line 218) │
│ - Calls: SyncService.sync(integration, direction='both') │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 5. Backend: SyncService.sync() (services/sync_service.py) │
│ - Calls: _sync_to_external() + _sync_from_external() │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 6. Backend: ContentSyncService._sync_to_wordpress() │
│ ❌ ISSUE: Queries ALL Content.objects with status='publish' │
│ ❌ ISSUE: Publishes up to 100 content items to WordPress │
│ ❌ This should NOT happen - should only fetch metadata! │
└─────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────▼────────────────────────────────────────┐
│ 7. Expected Behavior: │
│ ✅ Should only call: /wp-json/igny8/v1/site-metadata/ │
│ ✅ Should fetch: post_types counts, taxonomies counts │
│ ✅ Should update: ContentTypes table in Settings UI │
│ ✅ Should NOT republish any content │
└──────────────────────────────────────────────────────────────────────────┘
Detailed Issue Breakdown
Issue #1: Categories/Tags/Images Missing
Evidence from Logs:
IGNY8 Backend (wordpress_publishing.py):
[publish_content_to_wordpress] Categories: 1
[publish_content_to_wordpress] - Outdoor Living Design
[publish_content_to_wordpress] Tags: 4
[publish_content_to_wordpress] - outdoor lighting ideas
[publish_content_to_wordpress] - outdoor patio design
[publish_content_to_wordpress] Featured image: Yes
[publish_content_to_wordpress] Gallery images: 2
WordPress Plugin (error_log):
Categories: MISSING
Tags: MISSING
Featured Image: MISSING
⚠️ No categories in content_data
⚠️ No tags in content_data
⚠️ No featured image in content_data
Diagnostic Steps:
- Check IGNY8 payload before sending:
# File: backend/igny8_core/tasks/wordpress_publishing.py line 220-246
content_data = {
'categories': categories, # ✅ Correctly populated
'tags': tags, # ✅ Correctly populated
'featured_image_url': featured_image_url, # ✅ Correctly populated
}
- Check WordPress endpoint receives data:
// File: igny8-wp-plugin/includes/class-igny8-rest-api.php line 497-515
$content_data = $request->get_json_params();
error_log('Categories: ' . (isset($content_data['categories']) ? json_encode($content_data['categories']) : 'MISSING'));
// Shows: MISSING
Hypothesis: The issue is in how WordPress receives the JSON payload. Possible causes:
- Content-Type header not set correctly
- JSON parsing fails in WordPress
- Field names are being transformed during transmission
- WordPress REST API strips certain fields
Testing Required:
- Add raw request body logging in WordPress plugin
- Verify Content-Type header is
application/json - Check if WordPress REST API filters are interfering
Issue #2: Status Sync Logic Incorrect
Current Webhook Code:
# File: backend/igny8_core/modules/integration/webhooks.py line 147-161
old_status = content.status # 'review'
post_status = 'publish' # From WordPress
# Map WordPress status to IGNY8 status
status_map = {
'publish': 'published',
'draft': 'draft',
'pending': 'review',
}
igny8_status = status_map.get(post_status, 'review') # 'published'
# ❌ ISSUE: Only updates if WordPress status changed from draft to publish
if post_status == 'publish' and old_status != 'published':
content.status = 'published' # ✅ This SHOULD run
else:
# ❌ Status stays 'review'
Expected Logic:
# Should always update to mapped status
content.status = igny8_status # 'published'
Issue #3: Sync Republishes Content
Current Sync Service:
# File: backend/igny8_core/business/integration/services/content_sync_service.py line 90-155
def _sync_to_wordpress(self, integration, content_types):
# ❌ Gets all published content
content_query = Content.objects.filter(
account=integration.account,
site=integration.site,
source='igny8',
status='publish'
)
# ❌ Publishes each one
for content in content_query[:100]:
result = adapter.publish(content, destination_config)
Expected Behavior:
def sync_wordpress_metadata(self, integration):
"""Fetch WordPress site structure (counts only)"""
# ✅ Should call: /wp-json/igny8/v1/site-metadata/
# ✅ Should return: post_types, taxonomies, counts
# ❌ Should NOT publish any content
Complete Fix Plan
Fix #1: Solve Categories/Tags/Images Issue
Step 1.1: Add Debug Logging to WordPress Plugin
File: igny8-wp-plugin/includes/class-igny8-rest-api.php
public function publish_content_to_wordpress($request) {
// ADD: Log raw request body
$raw_body = $request->get_body();
error_log('========== RAW REQUEST BODY ==========');
error_log($raw_body);
error_log('======================================');
// Existing code
$content_data = $request->get_json_params();
// ADD: Log parsed JSON
error_log('========== PARSED JSON ==========');
error_log(print_r($content_data, true));
error_log('=================================');
// Rest of the function...
}
Step 1.2: Verify IGNY8 Sends Correct Headers
File: backend/igny8_core/tasks/wordpress_publishing.py
headers = {
'X-IGNY8-API-Key': api_key,
'Content-Type': 'application/json', # ✅ Verify this is set
}
# ADD: Log full request details
publish_logger.info(f"Request URL: {wordpress_url}")
publish_logger.info(f"Request Headers: {headers}")
publish_logger.info(f"Request Body: {json.dumps(content_data, indent=2)}")
Step 1.3: Fix WordPress REST Endpoint (if needed)
If WordPress REST API filters are stripping fields, add this to plugin:
add_filter('rest_request_before_callbacks', function($response, $handler, $request) {
// Allow all fields for IGNY8 endpoints
if (strpos($request->get_route(), '/igny8/v1/') === 0) {
return $response; // Don't filter IGNY8 requests
}
return $response;
}, 10, 3);
Fix #2: Correct Status Sync Logic
File: backend/igny8_core/modules/integration/webhooks.py
Current Code (lines 147-161):
# Map WordPress status to IGNY8 status
status_map = {
'publish': 'published',
'draft': 'draft',
'pending': 'review',
'private': 'published',
'trash': 'draft',
'future': 'review',
}
igny8_status = status_map.get(post_status, 'review')
# Update content
old_status = content.status
# ❌ WRONG: Only updates if WordPress status changed from draft to publish
if post_status == 'publish' and old_status != 'published':
content.status = 'published'
Fixed Code:
# Map WordPress status to IGNY8 status
status_map = {
'publish': 'published',
'draft': 'draft',
'pending': 'review',
'private': 'published',
'trash': 'draft',
'future': 'review',
}
igny8_status = status_map.get(post_status, 'review')
# Update content
old_status = content.status
# ✅ FIXED: Always update to mapped status when WordPress publishes
# Only skip update if already in the target status
if content.status != igny8_status:
content.status = igny8_status
logger.info(f"[wordpress_status_webhook] Status updated: {old_status} → {content.status}")
Fix #3: Separate Sync Metadata from Publish Content
Step 3.1: Create New Sync Metadata Service
File: backend/igny8_core/business/integration/services/sync_metadata_service.py (NEW)
"""
Sync Metadata Service
Fetches WordPress site structure (post types, taxonomies, counts) without publishing content
"""
import logging
import requests
from typing import Dict, Any
from igny8_core.business.integration.models import SiteIntegration
logger = logging.getLogger(__name__)
class SyncMetadataService:
"""
Service for syncing WordPress site metadata (counts only, no content publishing)
"""
def sync_wordpress_structure(
self,
integration: SiteIntegration
) -> Dict[str, Any]:
"""
Fetch WordPress site structure (post types, taxonomies, counts).
Does NOT publish or sync any content.
Args:
integration: SiteIntegration instance
Returns:
dict: {
'success': True/False,
'post_types': {...},
'taxonomies': {...},
'message': '...'
}
"""
try:
# Get WordPress site URL and API key
site_url = integration.config_json.get('site_url', '')
credentials = integration.get_credentials()
api_key = credentials.get('api_key', '')
if not site_url or not api_key:
return {
'success': False,
'error': 'Missing site_url or api_key in integration config'
}
# Call WordPress metadata endpoint
metadata_url = f"{site_url}/wp-json/igny8/v1/site-metadata/"
headers = {
'X-IGNY8-API-Key': api_key,
'Content-Type': 'application/json',
}
logger.info(f"[SyncMetadataService] Fetching metadata from: {metadata_url}")
response = requests.get(
metadata_url,
headers=headers,
timeout=30
)
if response.status_code != 200:
logger.error(f"[SyncMetadataService] WordPress returned {response.status_code}: {response.text}")
return {
'success': False,
'error': f'WordPress API error: {response.status_code}',
'details': response.text
}
# Parse response
data = response.json()
if not data.get('success'):
return {
'success': False,
'error': data.get('error', 'Unknown error'),
'message': data.get('message', '')
}
metadata = data.get('data', {})
logger.info(f"[SyncMetadataService] Successfully fetched metadata:")
logger.info(f" - Post types: {len(metadata.get('post_types', {}))}")
logger.info(f" - Taxonomies: {len(metadata.get('taxonomies', {}))}")
return {
'success': True,
'post_types': metadata.get('post_types', {}),
'taxonomies': metadata.get('taxonomies', {}),
'message': 'WordPress site structure synced successfully'
}
except requests.exceptions.Timeout:
logger.error(f"[SyncMetadataService] Timeout connecting to WordPress")
return {
'success': False,
'error': 'Timeout connecting to WordPress'
}
except Exception as e:
logger.error(f"[SyncMetadataService] Error syncing metadata: {str(e)}", exc_info=True)
return {
'success': False,
'error': str(e)
}
Step 3.2: Update IntegrationViewSet to Use New Service
File: backend/igny8_core/modules/integration/views.py
@action(detail=True, methods=['post'])
def sync(self, request, pk=None):
"""
Trigger synchronization with integrated platform.
POST /api/v1/integration/integrations/{id}/sync/
Request body:
{
"direction": "metadata", # 'metadata', 'to_external', 'from_external', 'both'
"content_types": ["blog_post", "page"] # Optional
}
"""
integration = self.get_object()
direction = request.data.get('direction', 'metadata')
content_types = request.data.get('content_types')
# NEW: Handle metadata sync separately
if direction == 'metadata':
from igny8_core.business.integration.services.sync_metadata_service import SyncMetadataService
metadata_service = SyncMetadataService()
result = metadata_service.sync_wordpress_structure(integration)
else:
# Existing content sync logic
sync_service = SyncService()
result = sync_service.sync(integration, direction=direction, content_types=content_types)
response_status = status.HTTP_200_OK if result.get('success') else status.HTTP_400_BAD_REQUEST
return success_response(result, request=request, status_code=response_status)
Step 3.3: Update Frontend to Request Metadata Sync
File: frontend/src/pages/Sites/Settings.tsx
const handleManualSync = async () => {
setSyncLoading(true);
try {
if (wordPressIntegration && wordPressIntegration.id) {
// CHANGED: Request metadata sync only (not content publishing)
const res = await integrationApi.syncIntegration(
wordPressIntegration.id,
'metadata' // ✅ NEW: Sync metadata only, don't republish content
);
if (res && res.success) {
toast.success('WordPress structure synced successfully');
setTimeout(() => loadContentTypes(), 1500);
} else {
toast.error(res?.message || 'Sync failed');
}
} else {
toast.error('No integration configured');
}
} catch (err: any) {
toast.error(`Sync failed: ${err?.message || String(err)}`);
} finally {
setSyncLoading(false);
}
};
File: frontend/src/services/integration.api.ts
async syncIntegration(
integrationId: number,
syncType: 'metadata' | 'incremental' | 'full' = 'metadata' // ✅ Changed default
): Promise<{ success: boolean; message: string; synced_count?: number }> {
return await fetchAPI(`/v1/integration/integrations/${integrationId}/sync/`, {
method: 'POST',
body: JSON.stringify({
direction: syncType === 'metadata' ? 'metadata' : 'both', // ✅ NEW
sync_type: syncType // Keep for backward compatibility
}),
});
}
Implementation Checklist
Phase 1: Diagnostic & Discovery (1-2 hours)
-
Task 1.1: Add raw request body logging to WordPress plugin
- File:
igny8-wp-plugin/includes/class-igny8-rest-api.php - Add logging before and after
$request->get_json_params()
- File:
-
Task 1.2: Verify IGNY8 sends correct headers
- File:
backend/igny8_core/tasks/wordpress_publishing.py - Log complete request details (URL, headers, body)
- File:
-
Task 1.3: Test publishing and capture full logs
- IGNY8 logs:
docker-compose -f docker-compose.app.yml logs -f backend - WordPress logs: Check
/wp-content/debug.logor plugin logs
- IGNY8 logs:
-
Task 1.4: Identify exact issue
- Compare sent payload with received payload
- Identify which fields are lost/transformed
Phase 2: Fix Categories/Tags/Images (2-3 hours)
-
Task 2.1: Fix WordPress REST endpoint if needed
- Add REST API filter bypass for IGNY8 endpoints
- Test with raw cURL request to verify fields are received
-
Task 2.2: Verify field processing functions
- Test
igny8_process_categories()with sample data - Test
igny8_process_tags()with sample data - Test
igny8_set_featured_image()with sample URL
- Test
-
Task 2.3: End-to-end test
- Publish content from IGNY8
- Verify categories appear in WordPress admin
- Verify tags appear in WordPress admin
- Verify featured image appears
Phase 3: Fix Status Sync (1 hour)
-
Task 3.1: Update webhook status logic
- File:
backend/igny8_core/modules/integration/webhooks.py - Change condition from
if post_status == 'publish' and old_status != 'published' - To:
if content.status != igny8_status
- File:
-
Task 3.2: Test status sync
- Content with status='review' in IGNY8
- Publish to WordPress (status='publish')
- Verify IGNY8 status changes to 'published'
-
Task 3.3: Verify webhook is being sent
- Check WordPress plugin sends webhook after post creation
- File:
igny8-wp-plugin/sync/igny8-to-wp.phpline 300-350 - Ensure
igny8_send_status_webhook()is called
Phase 4: Fix Sync Button (2-3 hours)
-
Task 4.1: Create SyncMetadataService
- File:
backend/igny8_core/business/integration/services/sync_metadata_service.py - Implement
sync_wordpress_structure()method
- File:
-
Task 4.2: Update IntegrationViewSet
- File:
backend/igny8_core/modules/integration/views.py - Add
direction='metadata'handling
- File:
-
Task 4.3: Update frontend
- File:
frontend/src/pages/Sites/Settings.tsx - Change sync call to use 'metadata' direction
- File:
frontend/src/services/integration.api.ts - Update
syncIntegration()signature
- File:
-
Task 4.4: Test sync button
- Click "Sync Now" in Settings
- Verify it only fetches counts
- Verify it does NOT republish content
- Verify Content Types table updates
Phase 5: Comprehensive Testing (2 hours)
-
Task 5.1: Test complete publishing flow
- Create new content in Writer
- Add categories, tags, images
- Publish to WordPress
- Verify all data appears correctly
-
Task 5.2: Test status sync
- Content starts in 'review'
- Publishes to WordPress
- Verify status changes to 'published'
-
Task 5.3: Test sync button
- Configure new WordPress integration
- Click "Sync Now"
- Verify counts update
- Verify no content is republished
-
Task 5.4: Test error handling
- Invalid API key
- WordPress site offline
- Missing required fields
Expected Results After Fix
1. Publishing Content to WordPress
Before Fix:
✅ Title appears
✅ Content HTML appears
❌ Categories: "Uncategorized"
❌ Tags: None
❌ Featured Image: None
❌ Status in IGNY8: stays 'review'
After Fix:
✅ Title appears
✅ Content HTML appears
✅ Categories: "Outdoor Living Design" (and any others)
✅ Tags: "outdoor lighting ideas", "outdoor patio design", etc.
✅ Featured Image: Displays correctly
✅ Gallery Images: Added to post
✅ SEO Title: Set in Yoast/SEOPress/AIOSEO
✅ SEO Description: Set in Yoast/SEOPress/AIOSEO
✅ Status in IGNY8: changes to 'published'
2. Sync Now Button
Before Fix:
❌ Republishes all published content (up to 100 items)
❌ Creates duplicate posts in WordPress
❌ Takes 30+ seconds to complete
✅ Updates Content Types table
After Fix:
✅ Only fetches WordPress site structure
✅ Updates post type counts
✅ Updates taxonomy counts
✅ Does NOT republish any content
✅ Completes in < 5 seconds
✅ Updates Content Types table
3. Status Sync
Before Fix:
IGNY8 Content Status: 'review'
↓
Publish to WordPress
↓
WordPress Post Status: 'publish'
↓
Webhook sent to IGNY8
↓
❌ IGNY8 Content Status: 'review' (unchanged)
After Fix:
IGNY8 Content Status: 'review'
↓
Publish to WordPress
↓
WordPress Post Status: 'publish'
↓
Webhook sent to IGNY8
↓
✅ IGNY8 Content Status: 'published' (updated correctly)
Complete Flow Diagrams
After Fix: Publishing Flow
┌───────────────────────────────────────────────────────────────────────────┐
│ STEP 1: User Action │
│ ──────────────────────────────────────────────────────────────────────── │
│ User clicks "Publish to WordPress" on Writer Review page │
│ Content ID: 123 │
│ Content Status: 'review' │
│ Categories: ['Outdoor Living Design'] │
│ Tags: ['outdoor lighting ideas', 'outdoor patio design', ...] │
│ Images: 1 featured, 2 gallery │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 2: Frontend Request │
│ ──────────────────────────────────────────────────────────────────────── │
│ POST /v1/publisher/publish/ │
│ Body: { │
│ content_id: 123, │
│ destinations: ['wordpress'] │
│ } │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 3: Backend Processing │
│ ──────────────────────────────────────────────────────────────────────── │
│ PublisherViewSet → PublisherService → WordPressAdapter │
│ ✅ Queues Celery task: publish_content_to_wordpress.delay(123, 456) │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 4: Celery Task Execution │
│ ──────────────────────────────────────────────────────────────────────── │
│ ✅ Extract categories from taxonomy_terms (taxonomy_type='category') │
│ Result: ['Outdoor Living Design'] │
│ │
│ ✅ Extract tags from taxonomy_terms + keywords │
│ Result: ['outdoor lighting ideas', 'outdoor patio design', ...] │
│ │
│ ✅ Extract images from Images model │
│ Featured: /data/app/.../featured.jpg │
│ Gallery: [/data/app/.../gallery1.jpg, /data/app/.../gallery2.jpg] │
│ │
│ ✅ Convert image URLs │
│ Featured: https://app.igny8.com/images/featured.jpg │
│ Gallery: [https://app.igny8.com/images/gallery1.jpg, ...] │
│ │
│ ✅ Build payload: │
│ { │
│ content_id: 123, │
│ title: "10 Outdoor Living Design Ideas", │
│ content_html: "<p>Full content...</p>", │
│ categories: ["Outdoor Living Design"], │
│ tags: ["outdoor lighting ideas", ...], │
│ featured_image_url: "https://app.igny8.com/images/featured.jpg", │
│ gallery_images: ["https://app.igny8.com/images/gallery1.jpg", ...], │
│ seo_title: "10 Best Outdoor Living Design Ideas 2025", │
│ seo_description: "Discover the top outdoor living...", │
│ primary_keyword: "outdoor living design", │
│ secondary_keywords: ["outdoor patio design", ...] │
│ } │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 5: Send to WordPress │
│ ──────────────────────────────────────────────────────────────────────── │
│ POST https://homeg8.com/wp-json/igny8/v1/publish │
│ Headers: │
│ X-IGNY8-API-Key: ***abc123 │
│ Content-Type: application/json │
│ Body: {payload from above} │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 6: WordPress Receives Request │
│ ──────────────────────────────────────────────────────────────────────── │
│ ✅ Validates API key │
│ ✅ Parses JSON payload │
│ ✅ Logs received data (FIXED - all fields present) │
│ Categories: ["Outdoor Living Design"] │
│ Tags: ["outdoor lighting ideas", ...] │
│ Featured Image: "https://app.igny8.com/images/featured.jpg" │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 7: WordPress Creates Post │
│ ──────────────────────────────────────────────────────────────────────── │
│ ✅ wp_insert_post() creates post │
│ Post ID: 789 │
│ Post Status: 'publish' │
│ │
│ ✅ igny8_process_categories() processes categories │
│ - Finds "Outdoor Living Design" (or creates if missing) │
│ - Returns category ID: [12] │
│ - Assigns to post with wp_set_post_terms() │
│ │
│ ✅ igny8_process_tags() processes tags │
│ - Finds/creates each tag │
│ - Returns tag IDs: [45, 67, 89, 91] │
│ - Assigns to post with wp_set_post_terms() │
│ │
│ ✅ igny8_set_featured_image() downloads and sets image │
│ - Downloads from https://app.igny8.com/images/featured.jpg │
│ - Creates attachment in WordPress media library │
│ - Sets as featured image with set_post_thumbnail() │
│ │
│ ✅ igny8_set_gallery_images() processes gallery │
│ - Downloads each gallery image │
│ - Creates attachments │
│ - Adds to post gallery meta │
│ │
│ ✅ SEO metadata set in Yoast/SEOPress/AIOSEO │
│ - _yoast_wpseo_title: "10 Best Outdoor Living Design Ideas 2025" │
│ - _yoast_wpseo_metadesc: "Discover the top outdoor living..." │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 8: WordPress Sends Webhook │
│ ──────────────────────────────────────────────────────────────────────── │
│ POST https://api.igny8.com/api/v1/integration/webhooks/wordpress/status/ │
│ Headers: │
│ X-IGNY8-API-Key: ***abc123 │
│ Body: { │
│ content_id: 123, │
│ post_id: 789, │
│ post_status: "publish", │
│ post_url: "https://homeg8.com/outdoor-living-design-ideas/", │
│ post_title: "10 Outdoor Living Design Ideas", │
│ site_url: "https://homeg8.com" │
│ } │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 9: IGNY8 Webhook Handler (FIXED) │
│ ──────────────────────────────────────────────────────────────────────── │
│ ✅ Validates API key │
│ ✅ Finds Content by content_id: 123 │
│ ✅ Maps WordPress status to IGNY8 status │
│ 'publish' → 'published' │
│ │
│ ✅ FIXED: Updates status regardless of previous value │
│ OLD CODE: if post_status == 'publish' and old_status != 'published' │
│ NEW CODE: if content.status != igny8_status │
│ │
│ ✅ Updates Content model: │
│ - status: 'review' → 'published' ✅ │
│ - external_id: 789 │
│ - external_url: "https://homeg8.com/outdoor-living-design-ideas/" │
│ - metadata['wordpress_status']: 'publish' │
│ - metadata['last_wp_sync']: "2025-12-01T15:30:00Z" │
│ │
│ ✅ Creates SyncEvent record for audit trail │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 10: Results │
│ ──────────────────────────────────────────────────────────────────────── │
│ ✅ WordPress Post Created: │
│ - Title: "10 Outdoor Living Design Ideas" │
│ - Category: "Outdoor Living Design" ✅ │
│ - Tags: "outdoor lighting ideas", "outdoor patio design", ... ✅ │
│ - Featured Image: Displayed ✅ │
│ - Gallery: 2 images attached ✅ │
│ - SEO: Title and description set ✅ │
│ - Status: Published ✅ │
│ │
│ ✅ IGNY8 Content Updated: │
│ - Status: 'published' ✅ │
│ - External ID: 789 ✅ │
│ - External URL: "https://homeg8.com/..." ✅ │
│ - WP Status Badge: Shows "Published" ✅ │
└───────────────────────────────────────────────────────────────────────────┘
After Fix: Sync Now Button Flow
┌───────────────────────────────────────────────────────────────────────────┐
│ STEP 1: User Action │
│ ──────────────────────────────────────────────────────────────────────── │
│ User navigates to: Settings → Integrations → WordPress → Content Types │
│ Clicks: "Sync Now" button │
│ Purpose: Update post counts and taxonomy counts from WordPress │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 2: Frontend Request (FIXED) │
│ ──────────────────────────────────────────────────────────────────────── │
│ POST /v1/integration/integrations/{id}/sync/ │
│ Body: { │
│ direction: 'metadata' ✅ CHANGED from 'both' │
│ } │
│ │
│ OLD CODE: direction: 'both' → republishes all content ❌ │
│ NEW CODE: direction: 'metadata' → only fetches structure ✅ │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 3: Backend IntegrationViewSet (FIXED) │
│ ──────────────────────────────────────────────────────────────────────── │
│ ✅ NEW: Checks direction parameter │
│ if direction == 'metadata': │
│ # Use new SyncMetadataService │
│ metadata_service = SyncMetadataService() │
│ result = metadata_service.sync_wordpress_structure(integration) │
│ else: │
│ # Use existing SyncService for content sync │
│ sync_service = SyncService() │
│ result = sync_service.sync(integration, direction) │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 4: SyncMetadataService (NEW) │
│ ──────────────────────────────────────────────────────────────────────── │
│ ✅ Gets WordPress site URL from integration.config_json['site_url'] │
│ ✅ Gets API key from integration.get_credentials()['api_key'] │
│ │
│ ✅ Calls WordPress metadata endpoint: │
│ GET https://homeg8.com/wp-json/igny8/v1/site-metadata/ │
│ Headers: │
│ X-IGNY8-API-Key: ***abc123 │
│ │
│ ✅ Does NOT query Content.objects │
│ ✅ Does NOT publish any content │
│ ✅ Does NOT call WordPressAdapter │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 5: WordPress Returns Metadata │
│ ──────────────────────────────────────────────────────────────────────── │
│ Response: { │
│ success: true, │
│ data: { │
│ post_types: { │
│ "post": { label: "Posts", count: 28 }, │
│ "page": { label: "Pages", count: 12 }, │
│ "product": { label: "Products", count: 156 } │
│ }, │
│ taxonomies: { │
│ "category": { label: "Categories", count: 8 }, │
│ "post_tag": { label: "Tags", count: 45 } │
│ }, │
│ plugin_connection_enabled: true, │
│ two_way_sync_enabled: true │
│ } │
│ } │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 6: Backend Returns to Frontend │
│ ──────────────────────────────────────────────────────────────────────── │
│ Response: { │
│ success: true, │
│ post_types: { ... }, │
│ taxonomies: { ... }, │
│ message: "WordPress site structure synced successfully" │
│ } │
│ │
│ ⏱️ Total time: < 5 seconds (was 30+ seconds before) │
└─────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────────────▼─────────────────────────────────────────┐
│ STEP 7: Frontend Updates UI │
│ ──────────────────────────────────────────────────────────────────────── │
│ ✅ Shows success toast: "WordPress structure synced successfully" │
│ ✅ Calls loadContentTypes() to refresh table │
│ ✅ Content Types table updates with fresh counts: │
│ - Posts: 28 │
│ - Pages: 12 │
│ - Products: 156 │
│ - Categories: 8 │
│ - Tags: 45 │
│ │
│ ✅ No content was republished │
│ ✅ No duplicate posts created │
│ ✅ Fast response time │
└───────────────────────────────────────────────────────────────────────────┘
Summary
This refactor plan addresses all three critical issues:
- Categories/Tags/Images Missing - Diagnostic approach to identify exact issue, then fix field parsing
- Status Sync Broken - Simple logic fix in webhook handler
- Sync Republishes Everything - New SyncMetadataService separates metadata fetch from content publishing
All fixes are backwards-compatible and follow existing code patterns. Total implementation time: 8-11 hours.