# 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) ```python 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) ```php // 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):** ```php // 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:** ```php // 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):** ```python # 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):** ```python 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:** ```python # 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: ```python # 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:** ```php 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:** ```python # 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():** ```php // 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_title` → `meta_title` - [ ] Change `seo_description` → `meta_description` - [ ] Remove `brief` (doesn't exist on Content model) - [ ] Generate `excerpt` from `content_html` - [ ] Change `focus_keywords` → `secondary_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:** ```python # 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:** ```php // 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 ```python # 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 ```python # Add after response = requests.post(...) print(f"HTTP Status: {response.status_code}") print(f"Response: {response.text[:500]}") ``` ### Debug Point 3: WordPress Reception ```php // 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 ```php // 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 = "
Full HTML content...
" ↓ Prepare payload content_data['content_html'] = "Full HTML content...
" ↓ JSON serialize {"content_html": "Full HTML content...
"} ↓ HTTP POST WordPress receives: content_html in POST body ↓ Parse JSON $content_data['content_html'] = "Full HTML content...
" ↓ Create post wp_insert_post(['post_content' => wp_kses_post($content_html)]) ↓ Database insert wp_posts.post_content = "Full HTML content...
" ``` ### Current Broken Flow: ``` Content Model (DB) ↓ ORM fetch content.content_html = "Full HTML content...
" ↓ Prepare payload content_data['content_html'] = "Full HTML content...
" ↓ 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 6. Verify all Content model fields are sent (2 hours) 7. Test with 10 different content pieces (1 hour) 8. Fix any edge cases discovered (2 hours) 9. Document the changes (1 hour) ### Day 3: Additional Features (From Plan) 10. Implement atomic transactions (Phase 1.1 from plan) 11. Add pre-flight validation (Phase 1.2 from plan) 12. Implement duplicate prevention (Phase 1.3 from plan) 13. Add post-publish verification (Phase 1.4 from plan) --- ## 🎯 FINAL VALIDATION TEST Run this test after all fixes: ```python # 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="This is test content with bold text.
", 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.**