# 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 = "<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
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="<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.**
