diff --git a/backend/igny8_core/tasks/wordpress_publishing.py b/backend/igny8_core/tasks/wordpress_publishing.py index 94abb0ee..651e1e0f 100644 --- a/backend/igny8_core/tasks/wordpress_publishing.py +++ b/backend/igny8_core/tasks/wordpress_publishing.py @@ -57,7 +57,8 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int # Extract site info for logging context site_id = site_integration.site.id if site_integration.site else 'unknown' - site_domain = site_integration.base_url.replace('https://', '').replace('http://', '').split('/')[0] if site_integration.base_url else 'unknown' + base_url = site_integration.config_json.get('site_url', '') or site_integration.config_json.get('base_url', '') + site_domain = base_url.replace('https://', '').replace('http://', '').split('/')[0] if base_url else 'unknown' log_prefix = f"[{site_id}-{site_domain}]" publish_logger.info(f" ✅ Content loaded:") @@ -70,8 +71,9 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int publish_logger.info(f" ✅ Integration loaded:") publish_logger.info(f" {log_prefix} Platform: {site_integration.platform}") publish_logger.info(f" {log_prefix} Site: {site_integration.site.name}") - publish_logger.info(f" {log_prefix} Base URL: {site_integration.base_url}") - publish_logger.info(f" {log_prefix} API Key: {'***' + site_integration.api_key[-4:] if site_integration.api_key else 'None'}") + publish_logger.info(f" {log_prefix} Base URL: {base_url}") + api_key = site_integration.get_credentials().get('api_key', '') + publish_logger.info(f" {log_prefix} API Key: {'***' + api_key[-4:] if api_key else 'None'}") except (Content.DoesNotExist, SiteIntegration.DoesNotExist) as e: publish_logger.error(f" ❌ Database lookup failed: {e}") return {"success": False, "error": str(e)} @@ -253,15 +255,28 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int publish_logger.info(f" {log_prefix} Sector ID: {content_data['sector_id']}") # STEP 8: Send API request to WordPress - wordpress_url = f"{site_integration.base_url}/wp-json/igny8/v1/publish" + base_url = site_integration.config_json.get('site_url', '') or site_integration.config_json.get('base_url', '') + api_key = site_integration.get_credentials().get('api_key', '') + + if not base_url: + error_msg = "No base_url/site_url configured in integration" + publish_logger.error(f" {log_prefix} ❌ {error_msg}") + return {"success": False, "error": error_msg} + + if not api_key: + error_msg = "No API key configured in integration" + publish_logger.error(f" {log_prefix} ❌ {error_msg}") + return {"success": False, "error": error_msg} + + wordpress_url = f"{base_url}/wp-json/igny8/v1/publish" headers = { - 'X-IGNY8-API-Key': site_integration.api_key, + 'X-IGNY8-API-Key': api_key, 'Content-Type': 'application/json', } publish_logger.info(f"{log_prefix} STEP 8: Sending POST request to WordPress...") api_logger.info(f"{log_prefix} API REQUEST: POST {wordpress_url}") - api_logger.info(f" {log_prefix} Headers: X-IGNY8-API-Key: ***{site_integration.api_key[-4:]}") + api_logger.info(f" {log_prefix} Headers: X-IGNY8-API-Key: ***{api_key[-4:]}") api_logger.info(f" {log_prefix} Payload: {json.dumps(content_data, indent=2)[:500]}...") try: diff --git a/docs/WORDPRESS-INTEGRATION-REFACTOR-PLAN-2025-12-01.md b/docs/WORDPRESS-INTEGRATION-REFACTOR-PLAN-2025-12-01.md new file mode 100644 index 00000000..bf4839c0 --- /dev/null +++ b/docs/WORDPRESS-INTEGRATION-REFACTOR-PLAN-2025-12-01.md @@ -0,0 +1,1143 @@ +# 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.