# 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 1. **Categories/Tags/Images Not Publishing** - WordPress logs show "No categories/tags/images in content_data" despite IGNY8 sending them 2. **Status Sync Broken** - Content stays in "review" after WordPress publish, should change to "published" 3. **Sync Button Republishes Everything** - Should only update counts, not republish all content 4. **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:** ```python # 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):** ```php // 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:** ```python # 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:** 1. **Check IGNY8 payload before sending:** ```python # 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 } ``` 2. **Check WordPress endpoint receives data:** ```php // 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:** 1. Add raw request body logging in WordPress plugin 2. Verify Content-Type header is `application/json` 3. Check if WordPress REST API filters are interfering ### Issue #2: Status Sync Logic Incorrect **Current Webhook Code:** ```python # 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:** ```python # Should always update to mapped status content.status = igny8_status # 'published' ``` ### Issue #3: Sync Republishes Content **Current Sync Service:** ```python # 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:** ```python 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` ```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` ```python 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: ```php 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):** ```python # 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:** ```python # 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) ```python """ 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` ```python @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` ```typescript 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` ```typescript 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()` - [ ] **Task 1.2:** Verify IGNY8 sends correct headers - File: `backend/igny8_core/tasks/wordpress_publishing.py` - Log complete request details (URL, headers, body) - [ ] **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.log` or plugin 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 - [ ] **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` - [ ] **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.php` line 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 - [ ] **Task 4.2:** Update IntegrationViewSet - File: `backend/igny8_core/modules/integration/views.py` - Add `direction='metadata'` handling - [ ] **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 - [ ] **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: "

Full content...

", │ │ 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: 1. **Categories/Tags/Images Missing** - Diagnostic approach to identify exact issue, then fix field parsing 2. **Status Sync Broken** - Simple logic fix in webhook handler 3. **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.