# WordPress Publishing Complete Field Mapping

**Last Updated**: 2025-12-01  
**Purpose**: Complete documentation of IGNY8 → WordPress publishing workflow with all field mappings

---

## 🔄 Publishing Workflow Overview

```
Frontend (Review.tsx)
    ↓ POST /v1/publisher/publish/
Backend (PublisherViewSet)
    ↓ PublisherService.publish()
WordPressAdapter._publish_via_api_key()
    ↓ POST {site_url}/wp-json/igny8/v1/publish
WordPress Plugin (class-igny8-rest-api.php)
    ↓ publish_content_to_wordpress()
WordPress Plugin (igny8-to-wp.php)
    ↓ igny8_create_wordpress_post_from_task()
WordPress Core
    ↓ wp_insert_post() + post_meta
WordPress Database (wp_posts, wp_postmeta, wp_term_relationships)
```

---

## 📊 Complete Field Mapping Table

| # | IGNY8 Field | DB Column | Backend Publisher | WordPress Plugin Endpoint | WordPress Plugin Function | WordPress Field | Status | Notes |
|---|-------------|-----------|-------------------|---------------------------|--------------------------|----------------|--------|-------|
| 1 | **id** | `id` | `content_id` | `content_id` (required) | `_igny8_content_id` (post_meta) | `wp_postmeta` | ✅ Published | Tracking field |
| 2 | **title** | `title` | `title` | `title` (required) | `post_title` | `wp_posts.post_title` | ✅ Published | Core field |
| 3 | **content_html** | `content_html` | `content_html` | `content_html` (required) | `post_content` | `wp_posts.post_content` | ✅ Published | Core field, sanitized with `wp_kses_post()` |
| 4 | **meta_title** | `meta_title` | `seo_title` | `seo_title` OR `meta_title` | `_yoast_wpseo_title`, `_seopress_titles_title`, `_aioseo_title`, `_igny8_meta_title` | `wp_postmeta` (SEO plugins) | ✅ Published | SEO title for Yoast/SEOPress/AIOSEO |
| 5 | **meta_description** | `meta_description` | `seo_description` | `seo_description` OR `meta_description` | `_yoast_wpseo_metadesc`, `_seopress_titles_desc`, `_aioseo_description`, `_igny8_meta_description` | `wp_postmeta` (SEO plugins) | ✅ Published | SEO description for plugins |
| 6 | **primary_keyword** | `primary_keyword` | `primary_keyword` | `primary_keyword` | Added to tags | `wp_terms` (post_tag) | ✅ Published | Auto-added as tag if not exists |
| 7 | **secondary_keywords** | `secondary_keywords` | `secondary_keywords` (JSON array) | `secondary_keywords` (JSON array) | Added to tags | `wp_terms` (post_tag) | ✅ Published | Each keyword becomes a tag |
| 8 | **taxonomy_terms (category)** | `taxonomy_terms` (M2M) | `categories` (array of names) | `categories` (array) | `category_ids` → `wp_set_post_terms()` | `wp_term_relationships` (category) | ✅ Published | Retrieved via `taxonomy_terms.filter(taxonomy_type='category')` |
| 9 | **taxonomy_terms (tag)** | `taxonomy_terms` (M2M) | `tags` (array of names) | `tags` (array) | `tag_ids` → `wp_set_post_terms()` | `wp_term_relationships` (post_tag) | ✅ Published | Retrieved via `taxonomy_terms.filter(taxonomy_type='tag')` |
| 10 | **cluster** | `cluster_id` (FK) | `cluster_id`, `categories` (fallback) | `cluster_id` | `_igny8_cluster_id` (post_meta), `igny8_clusters` (taxonomy) | `wp_postmeta`, `wp_term_relationships` | ✅ Published | Used as category fallback if no taxonomy_terms exist |
| 11 | **sector** | `sector_id` (FK) | `sector_id` | `sector_id` | `_igny8_sector_id` (post_meta), `igny8_sectors` (taxonomy) | `wp_postmeta`, `wp_term_relationships` | ✅ Published | Custom taxonomy |
| 12 | **Images (featured)** | `Images` model (FK) | `featured_image_url` | `featured_image_url` OR `featured_image` | `_thumbnail_id` (post_meta) | `wp_postmeta`, `wp_posts` (attachment) | ✅ Published | Via `igny8_set_featured_image()` - downloads/attaches image |
| 13 | **Images (gallery)** | `Images` model (FK) | `gallery_images` (array) | `gallery_images` (array) | Gallery processing | `wp_postmeta`, `wp_posts` (attachments) | ✅ Published | Via `igny8_set_gallery_images()` - downloads/attaches images |
| 14 | **status** | `status` | `status` | `status` | `post_status` | `wp_posts.post_status` | ✅ Published | Mapped via `igny8_map_igny8_status_to_wp()` (draft/published/review→publish/draft) |
| 15 | **published_at** | `published_at` | NOT SENT | `published_at` (optional) | `post_date`, `post_date_gmt` | `wp_posts.post_date`, `wp_posts.post_date_gmt` | ⚠️ Available | Not currently sent by backend |
| 16 | **excerpt** | N/A | `excerpt` (generated from content_html) | `excerpt` | `post_excerpt` | `wp_posts.post_excerpt` | ✅ Published | Auto-generated: first 150 chars of stripped HTML |
| 17 | **content_type** | `content_type` | `content_type` (optional) | `content_type` (optional) | `_igny8_content_type` (post_meta) | `wp_postmeta` | ✅ Published | Tracking field (e.g., "blog_post", "product_page") |
| 18 | **content_structure** | `content_structure` | `content_structure` (optional) | `content_structure` (optional) | `_igny8_content_structure` (post_meta) | `wp_postmeta` | ✅ Published | Tracking field |
| 19 | **source** | `source` | NOT SENT | `source` (optional) | `_igny8_source` (post_meta) | `wp_postmeta` | ⚠️ Available | Not currently sent by backend |
| 20 | **author** | `user_id` (FK) | NOT SENT | `author` (optional) | `post_author` | `wp_posts.post_author` | ⚠️ Available | Not sent - WordPress uses fallback author mapping |
| 21 | **word_count** | `word_count` | NOT SENT | N/A | N/A | N/A | ❌ Not Published | Could be sent as post_meta |
| 22 | **optimization_scores** | `optimization_scores` | NOT SENT | N/A | N/A | N/A | ❌ Not Published | JSON field - could be sent as post_meta |
| 23 | **metadata** | `metadata` | NOT SENT | N/A | N/A | N/A | ❌ Not Published | JSON field - could be sent as post_meta |
| 24 | **internal_links** | `internal_links` | NOT SENT | N/A | N/A | N/A | ❌ Not Published | JSON field - could be sent as post_meta |
| 25 | **external_id** | `external_id` | NOT SENT (return value) | N/A | `post_id` (return) | `wp_posts.ID` | ✅ Return Value | WordPress returns this to IGNY8, saved to content.external_id |
| 26 | **external_url** | `external_url` | NOT SENT (return value) | N/A | `post_url` (return) | `get_permalink()` | ✅ Return Value | WordPress returns this to IGNY8, saved to content.external_url |
| 27 | **external_metadata** | `external_metadata` | NOT SENT | N/A | N/A | N/A | ❌ Not Published | JSON field - not sent |
| 28 | **task_id** | N/A | NOT SENT | `task_id` (optional) | `_igny8_task_id` (post_meta) | `wp_postmeta` | ⚠️ Available | Not sent by backend, but WordPress accepts it |
| 29 | **brief** | `brief` | NOT SENT | `brief` (optional) | Used for excerpt fallback | `wp_posts.post_excerpt` | ⚠️ Available | Not sent, but would be used as excerpt if sent |
| 30 | **slug** | `slug` | NOT SENT | N/A | N/A | `wp_posts.post_name` | ❌ Not Published | WordPress auto-generates slug |
| 31 | **ai_content_brief** | `ai_content_brief` | NOT SENT | N/A | N/A | N/A | ❌ Not Published | IGNY8-internal field |
| 32 | **ai_response_raw** | `ai_response_raw` | NOT SENT | N/A | N/A | N/A | ❌ Not Published | IGNY8-internal field |

---

## 📋 Status Legend

- ✅ **Published** - Field is actively sent and saved to WordPress
- ⚠️ **Available** - Field is accepted by WordPress but not currently sent by IGNY8 backend
- ❌ **Not Published** - Field is not sent and not used in publishing workflow

---

## 🔑 Authentication & Endpoints

### Backend API Endpoint
```
POST /v1/publisher/publish/
Authorization: Token <user_token>

Request Body:
{
  "destination_id": 123,
  "content_ids": [456]
}
```

### WordPress Plugin Endpoint
```
POST {site_url}/wp-json/igny8/v1/publish
Headers:
  X-IGNY8-API-KEY: <api_key>
  Content-Type: application/json

Request Body:
{
  "content_id": 456,
  "title": "...",
  "content_html": "...",
  "seo_title": "...",
  "seo_description": "...",
  "categories": ["Category 1", "Category 2"],
  "tags": ["tag1", "tag2", "keyword"],
  "featured_image_url": "https://...",
  "gallery_images": [{"url": "...", "alt": "...", "caption": "..."}],
  "status": "publish",
  "primary_keyword": "...",
  "secondary_keywords": ["...", "..."],
  "cluster_id": 789,
  "sector_id": 101,
  "content_type": "blog_post",
  "content_structure": "..."
}
```

---

## 🗂️ WordPress Database Mapping

### Core Post Data (`wp_posts`)
- `post_title` ← title
- `post_content` ← content_html (sanitized)
- `post_excerpt` ← excerpt (auto-generated)
- `post_status` ← status (mapped: draft→draft, published→publish, review→publish)
- `post_type` ← resolved from content_type (default: "post")
- `post_author` ← mapped from WordPress author settings
- `post_date` ← published_at (if provided)
- `post_date_gmt` ← published_at (converted to GMT)

### Post Meta (`wp_postmeta`)
- `_igny8_content_id` ← content_id
- `_igny8_cluster_id` ← cluster_id
- `_igny8_sector_id` ← sector_id
- `_igny8_content_type` ← content_type
- `_igny8_content_structure` ← content_structure
- `_igny8_meta_title` ← meta_title
- `_igny8_meta_description` ← meta_description
- `_yoast_wpseo_title` ← meta_title (Yoast SEO)
- `_yoast_wpseo_metadesc` ← meta_description (Yoast SEO)
- `_seopress_titles_title` ← meta_title (SEOPress)
- `_seopress_titles_desc` ← meta_description (SEOPress)
- `_aioseo_title` ← meta_title (AIOSEO)
- `_aioseo_description` ← meta_description (AIOSEO)
- `_thumbnail_id` ← featured image attachment ID

### Taxonomies (`wp_term_relationships`, `wp_terms`)
- `category` ← categories array + cluster.name (fallback)
- `post_tag` ← tags array + primary_keyword + secondary_keywords
- `igny8_clusters` ← cluster_id (custom taxonomy)
- `igny8_sectors` ← sector_id (custom taxonomy)

### Attachments (`wp_posts` type=attachment)
- Featured image ← downloaded from featured_image_url
- Gallery images ← downloaded from gallery_images[].url

---

## 🔧 Backend Code Locations

### Publisher Service
**File**: `backend/igny8_core/business/publishing/services/publisher_service.py`
- `PublisherService._publish_to_destination()` - Main orchestration
- Updates `content.external_id`, `content.external_url`, `content.status='published'` after success

### WordPress Adapter
**File**: `backend/igny8_core/business/publishing/services/adapters/wordpress_adapter.py`
- `WordPressAdapter.publish()` - Entry point
- `WordPressAdapter._publish_via_api_key()` - Prepares payload and sends to WordPress
- **Line 147-160**: Extracts optional fields (meta_title, meta_description, keywords, cluster, sector)
- **Line 162-178**: Gets categories from `taxonomy_terms.filter(taxonomy_type='category')`
- **Line 180-212**: Gets tags from `taxonomy_terms.filter(taxonomy_type='tag')` + primary_keyword + secondary_keywords
- **Line 214-253**: Gets images (featured + gallery) from `Images` model

### Publisher ViewSet
**File**: `backend/igny8_core/modules/writer/views.py`
- `PublisherViewSet.publish()` - API endpoint handler

---

## 🎯 WordPress Plugin Code Locations

### REST API Endpoint
**File**: `igny8-wp-plugin/includes/class-igny8-rest-api.php`
- **Line 90**: Registers `/wp-json/igny8/v1/publish` endpoint
- **Line 490-631**: `publish_content_to_wordpress()` - Validates request, logs data, calls creation function

### Post Creation Function
**File**: `igny8-wp-plugin/sync/igny8-to-wp.php`
- **Line 73-300+**: `igny8_create_wordpress_post_from_task()` - Main creation function
  - **Line 106**: Prepares `content_html` (accepts `content_html` or legacy `content`)
  - **Line 112**: Maps author via `igny8_map_content_author()`
  - **Line 118-131**: Creates post data array with title, content, excerpt, status, type, author
  - **Line 133-138**: Sets publication date if provided
  - **Line 141-175**: Adds IGNY8 metadata (_igny8_content_id, _igny8_cluster_id, etc.)
  - **Line 178**: Creates post with `wp_insert_post()`
  - **Line 186**: Imports SEO metadata via `igny8_import_seo_metadata()`
  - **Line 189**: Imports featured image via `igny8_import_featured_image()`
  - **Line 192**: Imports taxonomies via `igny8_import_taxonomies()`
  - **Line 195**: Imports/processes content images via `igny8_import_content_images()`
  - **Line 209-238**: Processes and assigns categories via `igny8_process_categories()`
  - **Line 241-258**: Processes and assigns tags via `igny8_process_tags()`
  - **Line 261-273**: Sets featured image from URL
  - **Line 274-299**: Sets SEO meta fields for Yoast/SEOPress/AIOSEO

---

## 📈 Field Usage Statistics

### Currently Published: 19 fields
- Core content: title, content_html, excerpt, status
- SEO: meta_title, meta_description, primary_keyword, secondary_keywords
- Taxonomies: categories (via taxonomy_terms), tags (via taxonomy_terms), cluster (fallback), sector
- Images: featured_image_url, gallery_images
- Tracking: content_id, cluster_id, sector_id, content_type, content_structure

### Available But Unused: 4 fields
- published_at (WordPress accepts, backend doesn't send)
- source (WordPress accepts, backend doesn't send)
- author (WordPress has fallback mapping, backend doesn't send)
- task_id (WordPress accepts, backend doesn't send)

### Not Published: 9 fields
- word_count, optimization_scores, metadata, internal_links
- slug (WordPress auto-generates)
- ai_content_brief, ai_response_raw (IGNY8-internal)
- external_id, external_url (return values from WordPress)
- external_metadata (not sent)

---

## 🚀 Recent Enhancements (2025-12-01)

### Content Status Update
After successful WordPress publish, IGNY8 backend now updates:
```python
content.status = 'published'
content.save()
```
**File**: `publisher_service.py` line 184

### Error Handling Improvements
Frontend now shows proper error messages instead of "undefined":
```typescript
errorMessage = "Publishing failed"
```
**File**: `frontend/src/pages/Writer/Review.tsx`

### Taxonomy Integration Fixed
Categories and tags now properly extracted from `ContentTaxonomy` M2M relationship:
```python
categories = [term.name for term in content.taxonomy_terms.filter(taxonomy_type='category')]
tags = [term.name for term in content.taxonomy_terms.filter(taxonomy_type='tag')]
```
**File**: `wordpress_adapter.py` lines 162-178, 180-212

---

## 🔍 Verification & Testing

### Check WordPress Logs
WordPress plugin logs to `/wp-content/uploads/igny8-logs/`:
```bash
tail -f /path/to/wordpress/wp-content/uploads/igny8-logs/igny8-YYYY-MM-DD.log
```

### Check IGNY8 Celery Logs
```bash
docker logs -f igny8_celery_worker
```

### Verify Published Post
After publishing, IGNY8 saves:
- `content.external_id` = WordPress post_id
- `content.external_url` = WordPress post URL
- `content.status` = 'published'

### Database Queries

**IGNY8 Backend**:
```sql
-- Check content with taxonomies
SELECT c.id, c.title, c.status, c.external_id, c.external_url,
       COUNT(DISTINCT ctr.id) as taxonomy_count
FROM igny8_content c
LEFT JOIN igny8_content_taxonomy_relations ctr ON c.id = ctr.content_id
WHERE c.id = 456
GROUP BY c.id;

-- Check taxonomy terms for content
SELECT ct.name, ct.taxonomy_type, ct.slug
FROM igny8_content_taxonomy_terms ct
JOIN igny8_content_taxonomy_relations ctr ON ct.id = ctr.taxonomy_id
WHERE ctr.content_id = 456;
```

**WordPress**:
```sql
-- Check post and meta
SELECT p.ID, p.post_title, p.post_status, pm.meta_key, pm.meta_value
FROM wp_posts p
LEFT JOIN wp_postmeta pm ON p.ID = pm.post_id
WHERE pm.meta_key = '_igny8_content_id' AND pm.meta_value = '456';

-- Check categories and tags
SELECT t.name, tt.taxonomy
FROM wp_terms t
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tr.object_id = <post_id>;
```

---

## 💡 Recommendations

### Potentially Useful Fields to Publish
1. **published_at** - Allow scheduled publishing
2. **author** - Map IGNY8 user to WordPress author
3. **word_count** - Store as post_meta for analytics
4. **optimization_scores** - Store as post_meta for content quality tracking

### Fields to Keep Internal
- `ai_content_brief`, `ai_response_raw` - IGNY8-specific, no value in WordPress
- `external_metadata` - Already used for other destinations (Shopify, Sites)
- `metadata` - Generic field, may contain IGNY8-specific data

---

## 📚 Related Documentation
- [WordPress Integration Fixes 2025-11-30](./WORDPRESS-INTEGRATION-FIXES-2025-11-30.md)
- [WordPress Integration Fixes Implementation 2025-12-01](./WORDPRESS-INTEGRATION-FIXES-IMPLEMENTATION-2025-12-01.md)
- [WordPress Bidirectional Sync Reference](./04-WORDPRESS-BIDIRECTIONAL-SYNC-REFERENCE.md)
- [WordPress Plugin API Integration Guide](./03-WORDPRESS-PLUGIN-API-INTEGRATION-GUIDE.md)
