Files
igny8/docs/WP-PLUGIN-REFACTOR-PLAN.md
IGNY8 VPS (Salman) 04f04af813 some test
2025-12-01 07:03:46 +00:00

24 KiB

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:

// 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

File: sync/igny8-to-wp.php Current: Line 290 calls igny8_set_gallery_images() but function is named igny8_set_image_gallery()

Fix:

// 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:

// 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):

// 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:

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:

// 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');
    ?>
    <fieldset>
        <label>
            <input type="radio" name="igny8_default_post_status" value="draft" <?php checked($status, 'draft'); ?>>
            <?php _e('Draft - Save as draft for review', 'igny8-bridge'); ?>
        </label><br>
        <label>
            <input type="radio" name="igny8_default_post_status" value="publish" <?php checked($status, 'publish'); ?>>
            <?php _e('Publish - Publish immediately', 'igny8-bridge'); ?>
        </label>
        <p class="description">
            <?php _e('Choose whether content from IGNY8 should be published immediately or saved as draft.', 'igny8-bridge'); ?>
        </p>
    </fieldset>
    <?php
}

File: sync/igny8-to-wp.php Use settings in post creation (line ~122):

// OLD:
'post_status' => 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):

// 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:

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:

/**
 * 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');
    ?>
    <div class="igny8-keywords-box">
        <p>
            <label for="igny8_primary_keyword">
                <strong><?php _e('Primary Keyword', 'igny8-bridge'); ?></strong>
            </label>
            <input type="text" 
                   id="igny8_primary_keyword" 
                   name="igny8_primary_keyword" 
                   value="<?php echo esc_attr($primary_keyword); ?>" 
                   class="widefat"
                   placeholder="<?php _e('Enter primary keyword', 'igny8-bridge'); ?>">
        </p>
        
        <p>
            <label for="igny8_secondary_keywords">
                <strong><?php _e('Secondary Keywords', 'igny8-bridge'); ?></strong>
            </label>
            <input type="text" 
                   id="igny8_secondary_keywords" 
                   name="igny8_secondary_keywords" 
                   value="<?php echo esc_attr(implode(', ', $secondary_keywords)); ?>" 
                   class="widefat"
                   placeholder="<?php _e('keyword1, keyword2, keyword3', 'igny8-bridge'); ?>">
            <span class="description"><?php _e('Separate keywords with commas', 'igny8-bridge'); ?></span>
        </p>
        
        <?php if (!empty($primary_keyword) || !empty($secondary_keywords)) : ?>
            <p class="description">
                <em><?php _e('✅ These keywords were set by IGNY8', 'igny8-bridge'); ?></em>
            </p>
        <?php endif; ?>
    </div>
    <?php
}

/**
 * Render SEO meta box
 */
public function render_seo_box($post) {
    $meta_title = get_post_meta($post->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');
    ?>
    <div class="igny8-seo-box">
        <p>
            <label for="igny8_meta_title">
                <strong><?php _e('SEO Title', 'igny8-bridge'); ?></strong>
            </label>
            <input type="text" 
                   id="igny8_meta_title" 
                   name="igny8_meta_title" 
                   value="<?php echo esc_attr($meta_title); ?>" 
                   class="widefat"
                   placeholder="<?php _e('Enter SEO title', 'igny8-bridge'); ?>">
            <span class="description"><?php _e('Recommended: 50-60 characters', 'igny8-bridge'); ?></span>
        </p>
        
        <p>
            <label for="igny8_meta_description">
                <strong><?php _e('SEO Meta Description', 'igny8-bridge'); ?></strong>
            </label>
            <textarea id="igny8_meta_description" 
                      name="igny8_meta_description" 
                      rows="3" 
                      class="widefat"
                      placeholder="<?php _e('Enter meta description', 'igny8-bridge'); ?>"><?php echo esc_textarea($meta_description); ?></textarea>
            <span class="description"><?php _e('Recommended: 150-160 characters', 'igny8-bridge'); ?></span>
        </p>
        
        <?php if (!empty($meta_title) || !empty($meta_description)) : ?>
            <p class="description">
                <em><?php _e('✅ These SEO fields were set by IGNY8', 'igny8-bridge'); ?></em>
            </p>
        <?php endif; ?>
    </div>
    <?php
}

/**
 * Render Sync Data meta box (read-only)
 */
public function render_sync_data_box($post) {
    $content_id = get_post_meta($post->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);
    
    ?>
    <div class="igny8-sync-data-box">
        <table class="widefat striped">
            <tbody>
                <?php if ($content_id) : ?>
                    <tr>
                        <td><strong><?php _e('Content ID', 'igny8-bridge'); ?></strong></td>
                        <td><code><?php echo esc_html($content_id); ?></code></td>
                    </tr>
                <?php endif; ?>
                
                <?php if ($content_type) : ?>
                    <tr>
                        <td><strong><?php _e('Content Type', 'igny8-bridge'); ?></strong></td>
                        <td><?php echo esc_html($content_type); ?></td>
                    </tr>
                <?php endif; ?>
                
                <?php if ($content_structure) : ?>
                    <tr>
                        <td><strong><?php _e('Structure', 'igny8-bridge'); ?></strong></td>
                        <td><?php echo esc_html($content_structure); ?></td>
                    </tr>
                <?php endif; ?>
                
                <?php if ($cluster_id) : ?>
                    <tr>
                        <td><strong><?php _e('Cluster ID', 'igny8-bridge'); ?></strong></td>
                        <td><code><?php echo esc_html($cluster_id); ?></code></td>
                    </tr>
                <?php endif; ?>
                
                <?php if ($sector_id) : ?>
                    <tr>
                        <td><strong><?php _e('Sector ID', 'igny8-bridge'); ?></strong></td>
                        <td><code><?php echo esc_html($sector_id); ?></code></td>
                    </tr>
                <?php endif; ?>
            </tbody>
        </table>
        
        <?php if ($content_id) : ?>
            <p class="description">
                <em><?php _e('This post was published from IGNY8', 'igny8-bridge'); ?></em>
            </p>
        <?php else : ?>
            <p class="description">
                <em><?php _e('This post was not created by IGNY8', 'igny8-bridge'); ?></em>
            </p>
        <?php endif; ?>
    </div>
    <?php
}

Add save handlers:

/**
 * Save keywords when post is saved
 */
public function save_keywords($post_id) {
    // Check nonce
    if (!isset($_POST['igny8_keywords_nonce']) || !wp_verify_nonce($_POST['igny8_keywords_nonce'], 'igny8_keywords_nonce')) {
        return;
    }
    
    // Check autosave
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    
    // Check permissions
    if (!current_user_can('edit_post', $post_id)) {
        return;
    }
    
    // Save primary keyword
    if (isset($_POST['igny8_primary_keyword'])) {
        update_post_meta($post_id, '_igny8_primary_keyword', sanitize_text_field($_POST['igny8_primary_keyword']));
    }
    
    // Save secondary keywords
    if (isset($_POST['igny8_secondary_keywords'])) {
        $keywords = sanitize_text_field($_POST['igny8_secondary_keywords']);
        $keywords_array = array_map('trim', explode(',', $keywords));
        $keywords_array = array_filter($keywords_array); // Remove empty
        update_post_meta($post_id, '_igny8_secondary_keywords', json_encode($keywords_array));
    }
}

/**
 * Save SEO fields when post is saved
 */
public function save_seo($post_id) {
    // Check nonce
    if (!isset($_POST['igny8_seo_nonce']) || !wp_verify_nonce($_POST['igny8_seo_nonce'], 'igny8_seo_nonce')) {
        return;
    }
    
    // Check autosave
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    
    // Check permissions
    if (!current_user_can('edit_post', $post_id)) {
        return;
    }
    
    // Save meta title
    if (isset($_POST['igny8_meta_title'])) {
        $meta_title = sanitize_text_field($_POST['igny8_meta_title']);
        update_post_meta($post_id, '_igny8_meta_title', $meta_title);
        
        // Also update SEO plugin fields
        update_post_meta($post_id, '_yoast_wpseo_title', $meta_title);
        update_post_meta($post_id, '_seopress_titles_title', $meta_title);
        update_post_meta($post_id, '_aioseo_title', $meta_title);
    }
    
    // Save meta description
    if (isset($_POST['igny8_meta_description'])) {
        $meta_description = sanitize_textarea_field($_POST['igny8_meta_description']);
        update_post_meta($post_id, '_igny8_meta_description', $meta_description);
        
        // Also update SEO plugin fields
        update_post_meta($post_id, '_yoast_wpseo_metadesc', $meta_description);
        update_post_meta($post_id, '_seopress_titles_desc', $meta_description);
        update_post_meta($post_id, '_aioseo_description', $meta_description);
    }
}

// Register save handlers in constructor:
public function __construct() {
    add_action('add_meta_boxes', array($this, 'add_meta_boxes'));
    add_action('save_post', array($this, 'save_keywords'), 10, 1);
    add_action('save_post', array($this, 'save_seo'), 10, 1);
    add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
    
    // Keep optimizer AJAX handler
    add_action('wp_ajax_igny8_create_optimizer_job', array($this, 'create_optimizer_job'));
    add_action('wp_ajax_igny8_get_optimizer_status', array($this, 'get_optimizer_status'));
}

Phase 4: Backend Changes (IGNY8 Django)

Task 4.1: Send cluster_name and sector_name

File: backend/igny8_core/business/publishing/services/adapters/wordpress_adapter.py Add to payload (around line 172):

# Current:
if hasattr(content, 'cluster') and content.cluster:
    content_data['cluster_id'] = content.cluster.id
    optional_fields.append('cluster_id')

# NEW:
if hasattr(content, 'cluster') and content.cluster:
    content_data['cluster_id'] = content.cluster.id
    content_data['cluster_name'] = content.cluster.name  # ADD THIS
    optional_fields.append('cluster_id')

if hasattr(content, 'sector') and content.sector:
    content_data['sector_id'] = content.sector.id
    content_data['sector_name'] = content.sector.name  # ADD THIS
    optional_fields.append('sector_id')

Task 4.2: Capture and Save term_ids from Response

File: backend/igny8_core/business/publishing/services/adapters/wordpress_adapter.py Modify response handling (around line 300):

if response.status_code == 201:
    wp_data = response.json().get('data', {})
    logger.info(f"[WordPressAdapter._publish_via_api_key] ✅ Success! WordPress post created: post_id={wp_data.get('post_id')}, url={wp_data.get('post_url')}")
    
    # NEW: Extract term_ids
    term_ids = wp_data.get('term_ids', {})
    
    return {
        'success': True,
        'external_id': str(wp_data.get('post_id')),
        'url': wp_data.get('post_url'),
        'published_at': datetime.now(),
        'metadata': {
            'post_id': wp_data.get('post_id'),
            'status': destination_config.get('status', 'publish'),
            'term_ids': term_ids  # NEW: Save term mappings
        }
    }

Optional: Store term_ids in content.external_metadata JSON field for future reference


🗂️ File Structure After Refactor

igny8-wp-plugin/
├── igny8-bridge.php (main plugin file)
├── uninstall.php
├── admin/
│   ├── class-admin.php
│   ├── class-admin-columns.php
│   ├── class-post-meta-boxes.php ✅ UPDATED (new meta boxes, remove brief)
│   ├── settings.php ✅ UPDATED (add draft/publish setting)
│   └── assets/
├── includes/
│   ├── class-igny8-api.php
│   ├── class-igny8-rest-api.php ✅ UPDATED (return term_ids)
│   ├── class-igny8-logger.php
│   ├── class-igny8-webhooks.php (KEEP for future)
│   └── functions.php ✅ UPDATED (ensure taxonomies registered)
├── sync/
│   ├── igny8-to-wp.php ✅ UPDATED (fix gallery, remove post_meta for cluster/sector)
│   ├── hooks.php ❌ DELETE
│   ├── post-sync.php ❌ DELETE
│   └── taxonomy-sync.php ❌ DELETE
├── data/ (keep for site collection/link graph)
└── tests/ (keep for testing)

Testing Checklist

After Refactor, Test:

  1. Publish from IGNY8:

    • Post created in WordPress
    • Title, content, excerpt correct
    • Categories assigned correctly
    • Tags assigned correctly
    • Featured image downloaded and set
    • Gallery images downloaded and saved to post_meta
    • Cluster assigned as taxonomy (no post_meta)
    • Sector assigned as taxonomy (no post_meta)
    • Primary keyword saved to _igny8_primary_keyword
    • Secondary keywords saved to _igny8_secondary_keywords (JSON)
    • Meta title saved to SEO plugin fields
    • Meta description saved to SEO plugin fields
  2. WordPress Response:

    • Returns post_id immediately
    • Returns post_url immediately
    • Returns term_ids for all taxonomies
    • IGNY8 backend saves external_id, external_url, status='published'
  3. WordPress Editor UI:

    • "IGNY8 Keywords" meta box shows primary + secondary keywords (editable)
    • "IGNY8 SEO" meta box shows meta_title + meta_description (editable)
    • "IGNY8 Sync Data" meta box shows all tracking fields (read-only)
    • NO "IGNY8 Planner Brief" meta box
    • Cluster and sector show in taxonomy sidebars
    • Editing keywords/SEO saves correctly
  4. WordPress Settings:

    • "Default Post Status" option exists (draft/publish radio)
    • Changing setting affects next publish
  5. No Automatic Sync:

    • Editing post in WordPress does NOT trigger API call to IGNY8
    • Publishing post in WordPress does NOT trigger sync
    • No cron jobs running

📝 Summary of Changes

Removed:

  • All automatic sync hooks (save_post, publish_post, etc.)
  • Bidirectional sync files (post-sync.php, taxonomy-sync.php, hooks.php)
  • Brief meta box (no data in IGNY8)
  • Cron jobs for sync
  • Post_meta storage for cluster_id and sector_id

Fixed:

  • Gallery images function name (igny8_set_gallery_imagesigny8_set_image_gallery)
  • Cluster/sector stored ONLY as taxonomies (with term_meta for IGNY8 ID mapping)

Added:

  • "IGNY8 Keywords" meta box (primary_keyword, secondary_keywords)
  • "IGNY8 SEO" meta box (meta_title, meta_description)
  • "IGNY8 Sync Data" meta box (read-only tracking fields)
  • WP admin setting: "Default Post Status" (draft/publish)
  • WordPress returns term_ids in publish response
  • Backend sends cluster_name and sector_name for taxonomy creation
  • Backend saves term_ids from WordPress response

🚀 Implementation Order

  1. Phase 1 (Clean up): Remove sync files, hooks, brief meta box
  2. Phase 2 (Fix core): Gallery images, cluster/sector, draft setting, response enhancement
  3. Phase 3 (Add UI): New meta boxes for keywords, SEO, sync data
  4. Phase 4 (Backend): Update WordPress adapter to send names and capture term_ids

Estimated time: 4-6 hours Risk level: Low (mostly removing code, fixing bugs, adding UI) Testing requirement: High (verify all fields save correctly)