Files
igny8/docs/wp/WORDPRESS-INTEGRATION-REFACTOR-PLAN-2025-12-01.md
2025-12-03 18:12:01 +05:00

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

  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:

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

  1. 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
}
  1. 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:

  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:

# 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()
  • 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: "<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:

  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.