From 7357846527a4af432d0f8fe40ad7b2f9163774ee Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Mon, 1 Dec 2025 06:47:13 +0000 Subject: [PATCH] docs --- docs/WORDPRESS-PUBLISHING-FIELD-MAPPING.md | 334 +++++++++ docs/WP-PLUGIN-REFACTOR-PLAN.md | 744 +++++++++++++++++++++ docs/WP-PUBLISHING-SIMPLE-REFERENCE.md | 253 +++++++ igny8-wp-plugin/igny8-bridge.php | 25 +- 4 files changed, 1339 insertions(+), 17 deletions(-) create mode 100644 docs/WORDPRESS-PUBLISHING-FIELD-MAPPING.md create mode 100644 docs/WP-PLUGIN-REFACTOR-PLAN.md create mode 100644 docs/WP-PUBLISHING-SIMPLE-REFERENCE.md diff --git a/docs/WORDPRESS-PUBLISHING-FIELD-MAPPING.md b/docs/WORDPRESS-PUBLISHING-FIELD-MAPPING.md new file mode 100644 index 00000000..29226acc --- /dev/null +++ b/docs/WORDPRESS-PUBLISHING-FIELD-MAPPING.md @@ -0,0 +1,334 @@ +# 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 + +Request Body: +{ + "destination_id": 123, + "content_ids": [456] +} +``` + +### WordPress Plugin Endpoint +``` +POST {site_url}/wp-json/igny8/v1/publish +Headers: + X-IGNY8-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 = ; +``` + +--- + +## πŸ’‘ 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) diff --git a/docs/WP-PLUGIN-REFACTOR-PLAN.md b/docs/WP-PLUGIN-REFACTOR-PLAN.md new file mode 100644 index 00000000..75aae02f --- /dev/null +++ b/docs/WP-PLUGIN-REFACTOR-PLAN.md @@ -0,0 +1,744 @@ +# IGNY8 WordPress Plugin - Complete Refactor Plan + +**Created**: 2025-12-01 +**Scope**: Simplify to one-way publishing only, remove all automatic sync, fix broken features + +--- + +## 🎯 Refactor Goals + +1. **One-Way Publishing Only**: IGNY8 β†’ WordPress, no bidirectional sync +2. **Immediate Response**: WordPress returns all IDs right after post creation +3. **Clean UI**: Proper meta boxes for keywords, SEO, and tracking data +4. **Pure Taxonomies**: Cluster/sector as taxonomies only (remove post_meta duplication) +5. **Working Images**: Fix gallery image saving +6. **User Control**: Draft vs publish setting in WP admin +7. **Remove Complexity**: Delete all automatic sync, cron jobs, hooks + +--- + +## πŸ“‹ Refactor Tasks + +### Phase 1: Remove Automatic Sync (Clean Up) + +#### Task 1.1: Delete Sync Hooks +**File**: `sync/hooks.php` +**Action**: Delete entire file +**Reason**: All automatic sync hooks removed + +**Files to remove:** +``` +sync/hooks.php (DELETE) +sync/post-sync.php (DELETE - bidirectional sync) +sync/taxonomy-sync.php (DELETE - bidirectional sync) +``` + +**Code to remove from other files:** +- Remove `require_once 'sync/hooks.php'` from main plugin file +- Remove all cron job registrations +- Remove all `save_post`, `publish_post`, `transition_post_status` hooks + +--- + +#### Task 1.2: Remove Brief Meta Box +**File**: `admin/class-post-meta-boxes.php` +**Changes**: +```php +// REMOVE these lines from add_meta_boxes() method: +add_meta_box( + 'igny8-planner-brief', + __('IGNY8 Planner Brief', 'igny8-bridge'), + array($this, 'render_planner_brief_box'), + $post_type, + 'side', + 'default' +); + +// REMOVE entire method: +public function render_planner_brief_box($post) { ... } + +// REMOVE AJAX handlers: +add_action('wp_ajax_igny8_fetch_planner_brief', array($this, 'fetch_planner_brief')); +add_action('wp_ajax_igny8_refresh_planner_task', array($this, 'refresh_planner_task')); + +// REMOVE methods: +public function fetch_planner_brief() { ... } +public function refresh_planner_task() { ... } +``` + +**Reason**: No brief data exists in IGNY8 + +--- + +#### Task 1.3: Clean Up task_id References +**Investigation needed**: Determine if `_igny8_task_id` is: +- Writer task (remove completely) +- Celery task for async operations (keep for tracking) + +**Action**: If writer task, remove all references to `_igny8_task_id` + +--- + +### Phase 2: Fix Core Publishing + +#### Task 2.1: Fix Gallery Images Function +**File**: `sync/igny8-to-wp.php` +**Current**: Line 290 calls `igny8_set_gallery_images()` but function is named `igny8_set_image_gallery()` + +**Fix**: +```php +// Option 1: Rename function call +if (!empty($content_data['gallery_images'])) { + Igny8_Logger::info("{$log_prefix} STEP: Setting gallery with " . count($content_data['gallery_images']) . " images"); + igny8_set_image_gallery($post_id, $content_data['gallery_images']); // Changed from igny8_set_gallery_images +} + +// OR Option 2: Add alias function +function igny8_set_gallery_images($post_id, $gallery_images) { + return igny8_set_image_gallery($post_id, $gallery_images); +} +``` + +**Test**: Verify gallery images are saved to `_igny8_gallery_images` post_meta + +--- + +#### Task 2.2: Fix Cluster/Sector Storage +**File**: `sync/igny8-to-wp.php` +**Current**: Lines 141-175 save cluster_id and sector_id as post_meta + +**Remove these lines**: +```php +// REMOVE (lines ~163-175): +if (!empty($content_data['cluster_id'])) { + $post_data['meta_input']['_igny8_cluster_id'] = $content_data['cluster_id']; +} + +if (!empty($content_data['sector_id'])) { + $post_data['meta_input']['_igny8_sector_id'] = $content_data['sector_id']; +} +``` + +**Keep only taxonomy assignment** (lines ~195-230): +```php +// KEEP: This correctly assigns taxonomies +if (!empty($content_data['cluster_id'])) { + $cluster_terms = get_terms(array( + 'taxonomy' => 'igny8_clusters', + 'meta_key' => '_igny8_cluster_id', + 'meta_value' => $content_data['cluster_id'], + 'hide_empty' => false + )); + + if (!is_wp_error($cluster_terms) && !empty($cluster_terms)) { + wp_set_post_terms($post_id, array($cluster_terms[0]->term_id), 'igny8_clusters'); + } +} +// Same for sector... +``` + +**Issue**: This searches for terms by meta_key, but terms need to exist first! + +**Better approach**: +```php +if (!empty($content_data['cluster_id'])) { + // Get cluster name from IGNY8 (need to send cluster_name in payload) + $cluster_name = $content_data['cluster_name'] ?? ''; + + if (!empty($cluster_name)) { + $term = wp_insert_term($cluster_name, 'igny8_clusters', array( + 'slug' => sanitize_title($cluster_name) + )); + + if (!is_wp_error($term)) { + // Store IGNY8 cluster_id as term meta for future lookups + update_term_meta($term['term_id'], '_igny8_cluster_id', $content_data['cluster_id']); + wp_set_post_terms($post_id, array($term['term_id']), 'igny8_clusters'); + } + } +} +``` + +**Backend change needed**: `wordpress_adapter.py` must send `cluster_name` and `sector_name` in payload + +--- + +#### Task 2.3: Add Draft/Publish Setting +**File**: `admin/settings.php` +**Add new setting field**: + +```php +// In settings registration (around line ~100): +add_settings_field( + 'igny8_default_post_status', + __('Default Post Status', 'igny8-bridge'), + 'igny8_render_default_post_status_field', + 'igny8-settings', + 'igny8_settings_section' +); + +// Add field renderer: +function igny8_render_default_post_status_field() { + $status = get_option('igny8_default_post_status', 'draft'); + ?> +
+
+ +

+ +

+
+ igny8_map_igny8_status_to_wp($content_data['status'] ?? 'draft'), + +// NEW: +'post_status' => get_option('igny8_default_post_status', 'draft'), +``` + +--- + +#### Task 2.4: Return All Term IDs Immediately +**File**: `includes/class-igny8-rest-api.php` +**Modify `publish_content_to_wordpress()` return** (around line 615): + +```php +// After post creation (line ~605), collect term IDs: +$term_ids = array( + 'categories' => array(), + 'tags' => array(), + 'igny8_clusters' => array(), + 'igny8_sectors' => array() +); + +// Get assigned category IDs +$category_terms = wp_get_post_terms($post_id, 'category', array('fields' => 'ids')); +if (!is_wp_error($category_terms)) { + $term_ids['categories'] = $category_terms; +} + +// Get assigned tag IDs +$tag_terms = wp_get_post_terms($post_id, 'post_tag', array('fields' => 'ids')); +if (!is_wp_error($tag_terms)) { + $term_ids['tags'] = $tag_terms; +} + +// Get assigned cluster IDs +$cluster_terms = wp_get_post_terms($post_id, 'igny8_clusters', array('fields' => 'ids')); +if (!is_wp_error($cluster_terms)) { + $term_ids['igny8_clusters'] = $cluster_terms; +} + +// Get assigned sector IDs +$sector_terms = wp_get_post_terms($post_id, 'igny8_sectors', array('fields' => 'ids')); +if (!is_wp_error($sector_terms)) { + $term_ids['igny8_sectors'] = $sector_terms; +} + +// Return enhanced response: +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_id, + 'task_id' => $task_id, + 'term_ids' => $term_ids // NEW + ), + 'Content successfully published to WordPress', + null, + null, + 201 +); +``` + +**Backend change needed**: `wordpress_adapter.py` must capture and save `term_ids` from response + +--- + +### Phase 3: Add Custom Meta Boxes + +#### Task 3.1: Add IGNY8 Keywords Meta Box +**File**: `admin/class-post-meta-boxes.php` +**Add meta box registration**: + +```php +public function add_meta_boxes() { + $post_types = array('post', 'page', 'product'); + + foreach ($post_types as $post_type) { + // NEW: IGNY8 Keywords + add_meta_box( + 'igny8-keywords', + __('IGNY8 Keywords', 'igny8-bridge'), + array($this, 'render_keywords_box'), + $post_type, + 'side', + 'high' + ); + + // NEW: IGNY8 SEO + add_meta_box( + 'igny8-seo', + __('IGNY8 SEO', 'igny8-bridge'), + array($this, 'render_seo_box'), + $post_type, + 'normal', + 'high' + ); + + // NEW: IGNY8 Sync Data (read-only) + add_meta_box( + 'igny8-sync-data', + __('IGNY8 Sync Data', 'igny8-bridge'), + array($this, 'render_sync_data_box'), + $post_type, + 'side', + 'low' + ); + + // KEEP: IGNY8 Optimizer (existing) + add_meta_box( + 'igny8-optimizer', + __('IGNY8 Optimizer', 'igny8-bridge'), + array($this, 'render_optimizer_box'), + $post_type, + 'side', + 'default' + ); + } +} +``` + +**Add render methods**: + +```php +/** + * Render Keywords meta box + */ +public function render_keywords_box($post) { + $primary_keyword = get_post_meta($post->ID, '_igny8_primary_keyword', true); + $secondary_keywords = get_post_meta($post->ID, '_igny8_secondary_keywords', true); + + // Decode JSON if needed + if (is_string($secondary_keywords)) { + $secondary_keywords = json_decode($secondary_keywords, true); + } + if (!is_array($secondary_keywords)) { + $secondary_keywords = array(); + } + + wp_nonce_field('igny8_keywords_nonce', 'igny8_keywords_nonce'); + ?> +
+

+ + +

+ +

+ + + +

+ + +

+ +

+ +
+ ID, '_igny8_meta_title', true); + $meta_description = get_post_meta($post->ID, '_igny8_meta_description', true); + + wp_nonce_field('igny8_seo_nonce', 'igny8_seo_nonce'); + ?> +
+

+ + + +

+ +

+ + + +

+ + +

+ +

+ +
+ ID, '_igny8_content_id', true); + $content_type = get_post_meta($post->ID, '_igny8_content_type', true); + $content_structure = get_post_meta($post->ID, '_igny8_content_structure', true); + $cluster_id = get_post_meta($post->ID, '_igny8_cluster_id', true); + $sector_id = get_post_meta($post->ID, '_igny8_sector_id', true); + + ?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

+ +

+ +

+ +

+ +
+