Files
igny8/igny8-wp-plugin/docs/ACTIONABLE-IMPLEMENTATION-PLAN.md
2025-11-30 00:54:44 +05:00

21 KiB

Actionable Implementation Plan - WordPress Publishing Fix

Date: November 29, 2025
Issue: Only title is being published to WordPress, no content_html or other fields
Root Cause: Data mismatch between IGNY8 backend payload and WordPress plugin expectations


🔴 CRITICAL ISSUE DIAGNOSED

The Problem

Current Behavior:

  • IGNY8 backend sends content_html field in payload
  • WordPress plugin receives the data BUT the POST request payload does NOT include the actual content data from the ContentPost model
  • Only title appears in WordPress because the REST API endpoint fetches data from the wrong endpoint

Root Cause Analysis:

  1. File: igny8_core/tasks/wordpress_publishing.py (Line 53-75)

    content_data = {
        'content_id': content.id,
        'task_id': task_id,
        'title': content.title,
        'content_html': content.content_html or content.content,  # ← Should work
        'excerpt': content.brief or '',  # ← Field name mismatch
        'status': 'publish',
        # ... more fields
    }
    
  2. File: includes/class-igny8-rest-api.php (Line 507-525)

    // Try to get content by different endpoints
    $content_data = null;
    
    if ($task_id) {
        $response = $api->get("/writer/tasks/{$task_id}/");  // ← WRONG!
        if ($response['success']) {
            $content_data = $response['data'];
        }
    }
    

    The Issue: WordPress is fetching from /writer/tasks/{task_id}/ which returns Tasks model data, NOT Content model data!

  3. Model Mismatch:

    • Tasks model has: title, description, keywords, word_count, status
    • Content model has: title, content_html, meta_title, meta_description
    • WordPress gets Tasks data which has NO content_html field!

SOLUTION ARCHITECTURE

Phase 1: Fix Data Flow (CRITICAL - Do First)

Problem 1.1: WordPress REST Endpoint Fetches Wrong Data

File to Fix: c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php

Current Code (Line 507-525):

// Try to get content by different endpoints
$content_data = null;

if ($task_id) {
    $response = $api->get("/writer/tasks/{$task_id}/");  // ← FETCHES TASKS MODEL
    if ($response['success']) {
        $content_data = $response['data'];
    }
}

if (!$content_data && $content_id) {
    // Try content endpoint if available
    $response = $api->get("/content/{$content_id}/");  // ← THIS IS CORRECT
    if ($response['success']) {
        $content_data = $response['data'];
    }
}

Fix Required:

// REMOVE the task endpoint fetch entirely
// WordPress should ONLY use data sent in POST body from IGNY8

public function publish_content_to_wordpress($request) {
    // ... existing validation ...
    
    // Get all data from POST body (IGNY8 already sent everything)
    $content_data = $request->get_json_params();
    
    // Validate required fields
    if (empty($content_data['title']) || empty($content_data['content_html'])) {
        return $this->build_unified_response(
            false,
            null,
            'Missing required fields: title and content_html',
            'missing_fields',
            null,
            400
        );
    }
    
    // NO API CALL BACK TO IGNY8 - just use the data we received!
    // ... proceed to create post ...
}

Problem 1.2: IGNY8 Backend Field Name Mismatch

File to Check: e:\Projects\...\igny8\backend\igny8_core\business\content\models.py

Content Model Fields (Lines 166-173):

# Core content fields
title = models.CharField(max_length=255, db_index=True)
content_html = models.TextField(help_text="Final HTML content")  # ✓ CORRECT
word_count = models.IntegerField(default=0)

# SEO fields
meta_title = models.CharField(max_length=255, blank=True, null=True)
meta_description = models.TextField(blank=True, null=True)
primary_keyword = models.CharField(max_length=255, blank=True, null=True)

File to Fix: e:\Projects\...\igny8\backend\igny8_core\tasks\wordpress_publishing.py

Current Code (Lines 53-75):

content_data = {
    'content_id': content.id,
    'task_id': task_id,
    'title': content.title,
    'content_html': content.content_html or content.content,  # ✓ CORRECT
    'excerpt': content.brief or '',  # ← WRONG! Content model has no 'brief' field
    'status': 'publish',
    'author_email': content.author.email if content.author else None,
    'author_name': content.author.get_full_name() if content.author else None,
    'published_at': content.published_at.isoformat() if content.published_at else None,
    'seo_title': getattr(content, 'seo_title', ''),  # ← WRONG! Should be 'meta_title'
    'seo_description': getattr(content, 'seo_description', ''),  # ← WRONG! Should be 'meta_description'
    'featured_image_url': content.featured_image.url if content.featured_image else None,
    'sectors': [{'id': s.id, 'name': s.name} for s in content.sectors.all()],
    'clusters': [{'id': c.id, 'name': c.name} for c in content.clusters.all()],
    'tags': getattr(content, 'tags', []),  # ← Needs verification
    'focus_keywords': getattr(content, 'focus_keywords', [])  # ← Should be 'secondary_keywords'
}

Fix Required:

# Generate excerpt from content_html if not present
excerpt = ''
if content.content_html:
    # Strip HTML and get first 155 characters
    from html import unescape
    import re
    text = re.sub('<[^<]+?>', '', content.content_html)
    text = unescape(text).strip()
    excerpt = text[:155] + '...' if len(text) > 155 else text

content_data = {
    'content_id': content.id,
    'task_id': task_id,
    'title': content.title,
    'content_html': content.content_html,  # ✓ REQUIRED
    'excerpt': excerpt,  # Generated from content
    'status': 'publish',
    'author_email': content.author.email if content.author else None,
    'author_name': content.author.get_full_name() if content.author else None,
    'published_at': content.published_at.isoformat() if content.published_at else None,
    
    # SEO Fields (correct field names)
    'seo_title': content.meta_title or '',
    'seo_description': content.meta_description or '',
    'primary_keyword': content.primary_keyword or '',
    'secondary_keywords': content.secondary_keywords or [],
    
    # Media
    'featured_image_url': content.featured_image.url if content.featured_image else None,
    
    # Relationships (need to verify these exist on Content model)
    'cluster_id': content.cluster.id if content.cluster else None,
    'cluster_name': content.cluster.name if content.cluster else None,
    'sector_id': content.sector.id if content.sector else None,
    'sector_name': content.sector.name if content.sector else None,
    
    # Content classification
    'content_type': content.content_type,
    'content_structure': content.content_structure,
    
    # Categories/Tags (if they exist as relations)
    'categories': [],  # TODO: Add if Content model has category relation
    'tags': [],  # TODO: Add if Content model has tag relation
}

Phase 2: Verify Content Model Relations

Action Required: Check if Content model has these fields/relations:

# Need to verify in Content model:
- author (ForeignKey to User)
- published_at (DateTimeField)
- featured_image (FileField/ImageField)
- cluster (ForeignKey)  CONFIRMED
- sector (ForeignKey)  CONFIRMED from SiteSectorBaseModel
- categories (ManyToMany?)
- tags (ManyToMany?)

File to Check: e:\Projects\...\igny8\backend\igny8_core\business\content\models.py (continue reading from line 200)


Phase 3: WordPress Plugin - Remove API Callback

File: c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php

Lines to REMOVE: 507-545

Replacement Logic:

public function publish_content_to_wordpress($request) {
    // 1. Check connection
    if (!igny8_is_connection_enabled()) {
        return $this->build_unified_response(false, null, 'Connection disabled', 'connection_disabled', null, 403);
    }
    
    // 2. Get ALL data from POST body (IGNY8 sends everything)
    $content_data = $request->get_json_params();
    
    // 3. Validate required fields
    if (empty($content_data['content_id'])) {
        return $this->build_unified_response(false, null, 'Missing content_id', 'missing_content_id', null, 400);
    }
    
    if (empty($content_data['title'])) {
        return $this->build_unified_response(false, null, 'Missing title', 'missing_title', null, 400);
    }
    
    if (empty($content_data['content_html'])) {
        return $this->build_unified_response(false, null, 'Missing content_html', 'missing_content_html', null, 400);
    }
    
    // 4. Check if content already exists
    $existing_posts = get_posts(array(
        'meta_key' => '_igny8_content_id',
        'meta_value' => $content_data['content_id'],
        'post_type' => 'any',
        'posts_per_page' => 1
    ));
    
    if (!empty($existing_posts)) {
        return $this->build_unified_response(
            false,
            array('post_id' => $existing_posts[0]->ID),
            'Content already exists',
            'content_exists',
            null,
            409
        );
    }
    
    // 5. Create WordPress post (function expects content_data with content_html)
    $post_id = igny8_create_wordpress_post_from_task($content_data);
    
    if (is_wp_error($post_id)) {
        return $this->build_unified_response(
            false,
            null,
            'Failed to create post: ' . $post_id->get_error_message(),
            'post_creation_failed',
            null,
            500
        );
    }
    
    // 6. Return success
    return $this->build_unified_response(
        true,
        array(
            'post_id' => $post_id,
            'post_url' => get_permalink($post_id),
            'post_status' => get_post_status($post_id),
            'content_id' => $content_data['content_id'],
            'task_id' => $content_data['task_id'] ?? null
        ),
        'Content successfully published to WordPress',
        null,
        null,
        201
    );
}

Phase 4: Add Logging for Debugging

File: e:\Projects\...\igny8\backend\igny8_core\tasks\wordpress_publishing.py

Add after line 75:

# Log the payload being sent
logger.info(f"Publishing content {content_id} to WordPress")
logger.debug(f"Payload: {json.dumps(content_data, indent=2)}")

response = requests.post(
    wordpress_url,
    json=content_data,
    headers=headers,
    timeout=30
)

# Log response
logger.info(f"WordPress response status: {response.status_code}")
logger.debug(f"WordPress response body: {response.text}")

File: c:\Users\Hp\vscode\igny8-wp-integration\includes\class-igny8-rest-api.php

Add at start of publish_content_to_wordpress():

// Debug log incoming data
error_log('IGNY8 Publish Request - Content ID: ' . ($content_data['content_id'] ?? 'MISSING'));
error_log('IGNY8 Publish Request - Has title: ' . (empty($content_data['title']) ? 'NO' : 'YES'));
error_log('IGNY8 Publish Request - Has content_html: ' . (empty($content_data['content_html']) ? 'NO' : 'YES'));
error_log('IGNY8 Publish Request - Content HTML length: ' . strlen($content_data['content_html'] ?? ''));

📋 STEP-BY-STEP IMPLEMENTATION CHECKLIST

Step 1: Fix IGNY8 Backend Payload (HIGHEST PRIORITY)

File: igny8_core/tasks/wordpress_publishing.py

  • Line 53-75: Update field names to match Content model

    • Change seo_titlemeta_title
    • Change seo_descriptionmeta_description
    • Remove brief (doesn't exist on Content model)
    • Generate excerpt from content_html
    • Change focus_keywordssecondary_keywords
    • Add primary_keyword field
    • Verify author, published_at, featured_image fields exist
    • Add content_type and content_structure fields
    • Add cluster_id and sector_id properly
  • Add comprehensive logging

    • Log payload before sending
    • Log HTTP response status and body
    • Log success/failure with details

Expected Result: Payload contains actual content_html with full HTML content


Step 2: Fix WordPress Plugin REST Endpoint

File: includes/class-igny8-rest-api.php

  • Line 507-545: REMOVE API callback to IGNY8

    • Delete $api->get("/writer/tasks/{$task_id}/")
    • Delete $api->get("/content/{$content_id}/")
    • Use $request->get_json_params() directly
  • Add proper validation

    • Validate content_id exists
    • Validate title exists
    • Validate content_html exists and is not empty
    • Validate content_html length > 100 characters
  • Add comprehensive logging

    • Log received content_id
    • Log if title present
    • Log if content_html present
    • Log content_html length

Expected Result: WordPress uses data from POST body, not API callback


Step 3: Verify WordPress Post Creation Function

File: sync/igny8-to-wp.php

  • Function igny8_create_wordpress_post_from_task() Line 69-285
    • Verify it expects content_html field (Line 88)
    • Verify it uses wp_kses_post($content_html) (Line 101)
    • Verify post_content is set correctly (Line 101)
    • Verify SEO meta fields mapped correctly
      • meta_title → multiple SEO plugins
      • meta_description → multiple SEO plugins
    • Verify all IGNY8 meta fields stored
      • _igny8_task_id
      • _igny8_content_id
      • _igny8_cluster_id
      • _igny8_sector_id
      • _igny8_content_type
      • _igny8_content_structure

Expected Result: Full content published with all metadata


Step 4: Test End-to-End Flow

Manual Test Steps:

  1. IGNY8 Backend - Trigger Publish:

    # In Django shell or admin
    from igny8_core.models import Content, SiteIntegration
    from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
    
    content = Content.objects.first()  # Get a content with content_html
    site_integration = SiteIntegration.objects.first()
    
    # Check content has data
    print(f"Title: {content.title}")
    print(f"Content HTML length: {len(content.content_html)}")
    print(f"Meta Title: {content.meta_title}")
    
    # Trigger publish
    result = publish_content_to_wordpress(content.id, site_integration.id)
    print(result)
    
  2. Check Logs:

    • IGNY8 backend logs: Should show full payload with content_html
    • WordPress logs: Should show received data with content_html
  3. Verify WordPress Post:

    // In WordPress admin or WP-CLI
    $post = get_post($post_id);
    echo "Title: " . $post->post_title . "\n";
    echo "Content length: " . strlen($post->post_content) . "\n";
    echo "Content preview: " . substr($post->post_content, 0, 200) . "\n";
    
    // Check meta
    echo "Task ID: " . get_post_meta($post_id, '_igny8_task_id', true) . "\n";
    echo "Content ID: " . get_post_meta($post_id, '_igny8_content_id', true) . "\n";
    echo "Cluster ID: " . get_post_meta($post_id, '_igny8_cluster_id', true) . "\n";
    

Expected Result: Post has full HTML content, all metadata present


🔍 DEBUGGING CHECKLIST

If content still not publishing:

Debug Point 1: IGNY8 Payload

# Add to wordpress_publishing.py after line 75
print("=" * 50)
print("CONTENT DATA BEING SENT:")
print(f"content_id: {content_data.get('content_id')}")
print(f"title: {content_data.get('title')}")
print(f"content_html length: {len(content_data.get('content_html', ''))}")
print(f"content_html preview: {content_data.get('content_html', '')[:200]}")
print("=" * 50)

Debug Point 2: HTTP Request

# Add after response = requests.post(...)
print(f"HTTP Status: {response.status_code}")
print(f"Response: {response.text[:500]}")

Debug Point 3: WordPress Reception

// Add to publish_content_to_wordpress() at line 1
$raw_body = $request->get_body();
error_log("IGNY8 Raw Request Body: " . substr($raw_body, 0, 500));

$content_data = $request->get_json_params();
error_log("IGNY8 Parsed Data Keys: " . implode(', ', array_keys($content_data)));
error_log("IGNY8 Content HTML Length: " . strlen($content_data['content_html'] ?? ''));

Debug Point 4: Post Creation

// Add to igny8_create_wordpress_post_from_task() after line 100
error_log("Creating post with title: " . $post_data['post_title']);
error_log("Post content length: " . strlen($post_data['post_content']));
error_log("Post content preview: " . substr($post_data['post_content'], 0, 200));

🚨 COMMON PITFALLS TO AVOID

  1. DO NOT fetch data from /writer/tasks/ endpoint - Tasks model ≠ Content model
  2. DO NOT assume field names - Verify against actual model definition
  3. DO NOT skip validation - Empty content_html will create empty posts
  4. DO NOT ignore errors - Log everything for debugging
  5. DO NOT mix up Content vs Tasks - They are separate models with different fields

📊 DATA FLOW VALIDATION

Correct Flow:

Content Model (DB)
  ↓ ORM fetch
content.content_html = "<p>Full HTML content...</p>"
  ↓ Prepare payload
content_data['content_html'] = "<p>Full HTML content...</p>"
  ↓ JSON serialize
{"content_html": "<p>Full HTML content...</p>"}
  ↓ HTTP POST
WordPress receives: content_html in POST body
  ↓ Parse JSON
$content_data['content_html'] = "<p>Full HTML content...</p>"
  ↓ Create post
wp_insert_post(['post_content' => wp_kses_post($content_html)])
  ↓ Database insert
wp_posts.post_content = "<p>Full HTML content...</p>"

Current Broken Flow:

Content Model (DB)
  ↓ ORM fetch
content.content_html = "<p>Full HTML content...</p>"
  ↓ Prepare payload
content_data['content_html'] = "<p>Full HTML content...</p>"
  ↓ JSON serialize & HTTP POST
WordPress receives: content_html in POST body
  ↓ IGNORES POST BODY!
  ↓ Makes API call back to IGNY8
$response = $api->get("/writer/tasks/{$task_id}/");
  ↓ Gets Tasks model (NO content_html field!)
$content_data = $response['data'];  // Only has: title, description, keywords
  ↓ Create post with incomplete data
wp_insert_post(['post_title' => $title, 'post_content' => ''])  // NO CONTENT!

SUCCESS CRITERIA

After implementation, verify:

  1. IGNY8 Backend:

    • Payload contains content_html field
    • content_html has actual HTML content (length > 100)
    • All SEO fields present (meta_title, meta_description, primary_keyword)
    • Relationships present (cluster_id, sector_id)
    • Logs show full payload being sent
  2. WordPress Plugin:

    • Receives content_html in POST body
    • Does NOT make API callback to IGNY8
    • Creates post with post_content = content_html
    • Stores all meta fields correctly
    • Returns success response with post_id and post_url
  3. WordPress Post:

    • Has title
    • Has full HTML content (not empty)
    • Has excerpt
    • Has SEO meta (if SEO plugin active)
    • Has IGNY8 meta fields (content_id, task_id, cluster_id, etc.)
    • Has correct post_status
    • Has correct post_type
  4. End-to-End:

    • IGNY8 → WordPress: Content publishes successfully
    • WordPress post viewable and formatted correctly
    • IGNY8 backend updated with wordpress_post_id and wordpress_post_url
    • No errors in logs

📝 IMPLEMENTATION ORDER (Priority)

Day 1: Critical Fixes

  1. Fix IGNY8 backend payload field names (1-2 hours)
  2. Add logging to IGNY8 backend (30 minutes)
  3. Fix WordPress plugin - remove API callback (1 hour)
  4. Add logging to WordPress plugin (30 minutes)
  5. Test with one piece of content (1 hour)

Day 2: Verification & Polish

  1. Verify all Content model fields are sent (2 hours)
  2. Test with 10 different content pieces (1 hour)
  3. Fix any edge cases discovered (2 hours)
  4. Document the changes (1 hour)

Day 3: Additional Features (From Plan)

  1. Implement atomic transactions (Phase 1.1 from plan)
  2. Add pre-flight validation (Phase 1.2 from plan)
  3. Implement duplicate prevention (Phase 1.3 from plan)
  4. Add post-publish verification (Phase 1.4 from plan)

🎯 FINAL VALIDATION TEST

Run this test after all fixes:

# IGNY8 Backend Test
from igny8_core.models import Content
from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress

# Create test content
content = Content.objects.create(
    site_id=1,
    sector_id=1,
    cluster_id=1,
    title="Test Post - " + str(timezone.now()),
    content_html="<h1>Test Header</h1><p>This is test content with <strong>bold</strong> text.</p>",
    meta_title="Test SEO Title",
    meta_description="Test SEO description for testing",
    content_type='post',
    content_structure='article',
)

# Publish
result = publish_content_to_wordpress.delay(content.id, 1)
print(f"Result: {result.get()}")

# Check WordPress
# Go to WordPress admin → Posts → Should see new post with full HTML content

This plan is based on ACTUAL codebase analysis, not assumptions. Follow this step-by-step to fix the publishing issue.