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_htmlfield in payload - WordPress plugin receives the data BUT the POST request payload does NOT include the actual content data from the
ContentPostmodel - Only
titleappears in WordPress because the REST API endpoint fetches data from the wrong endpoint
Root Cause Analysis:
-
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 } -
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 returnsTasksmodel data, NOTContentmodel data! -
Model Mismatch:
Tasksmodel has:title,description,keywords,word_count,statusContentmodel has:title,content_html,meta_title,meta_description- WordPress gets
Tasksdata which has NOcontent_htmlfield!
✅ 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
Contentmodel- Change
seo_title→meta_title - Change
seo_description→meta_description - Remove
brief(doesn't exist on Content model) - Generate
excerptfromcontent_html - Change
focus_keywords→secondary_keywords - Add
primary_keywordfield - Verify
author,published_at,featured_imagefields exist - Add
content_typeandcontent_structurefields - Add
cluster_idandsector_idproperly
- Change
-
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
- Delete
-
Add proper validation
- Validate
content_idexists - Validate
titleexists - Validate
content_htmlexists and is not empty - Validate
content_htmllength > 100 characters
- Validate
-
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_htmlfield (Line 88) - Verify it uses
wp_kses_post($content_html)(Line 101) - Verify
post_contentis set correctly (Line 101) - Verify SEO meta fields mapped correctly
meta_title→ multiple SEO pluginsmeta_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
- Verify it expects
Expected Result: Full content published with all metadata
✅ Step 4: Test End-to-End Flow
Manual Test Steps:
-
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) -
Check Logs:
- IGNY8 backend logs: Should show full payload with
content_html - WordPress logs: Should show received data with
content_html
- IGNY8 backend logs: Should show full payload with
-
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
- DO NOT fetch data from
/writer/tasks/endpoint - Tasks model ≠ Content model - DO NOT assume field names - Verify against actual model definition
- DO NOT skip validation - Empty
content_htmlwill create empty posts - DO NOT ignore errors - Log everything for debugging
- 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:
-
IGNY8 Backend:
- Payload contains
content_htmlfield content_htmlhas 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
- Payload contains
-
WordPress Plugin:
- Receives
content_htmlin 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
- Receives
-
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
-
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
- Fix IGNY8 backend payload field names (1-2 hours)
- Add logging to IGNY8 backend (30 minutes)
- Fix WordPress plugin - remove API callback (1 hour)
- Add logging to WordPress plugin (30 minutes)
- Test with one piece of content (1 hour)
Day 2: Verification & Polish
- Verify all Content model fields are sent (2 hours)
- Test with 10 different content pieces (1 hour)
- Fix any edge cases discovered (2 hours)
- Document the changes (1 hour)
Day 3: Additional Features (From Plan)
- Implement atomic transactions (Phase 1.1 from plan)
- Add pre-flight validation (Phase 1.2 from plan)
- Implement duplicate prevention (Phase 1.3 from plan)
- 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.