Files
igny8/igny8-wp-integration-plugin/docs/WORDPRESS-PLUGIN-INTEGRATION.md
IGNY8 VPS (Salman) c35b3c3641 Add Site Metadata Endpoint and API Key Management
- Introduced a new Site Metadata endpoint (`GET /wp-json/igny8/v1/site-metadata/`) for retrieving available post types and taxonomies, including counts.
- Added API key input in the admin settings for authentication, with secure storage and revocation functionality.
- Implemented a toggle for enabling/disabling two-way sync operations.
- Updated documentation to reflect new features and usage examples.
- Enhanced permission checks for REST API calls to ensure secure access.
2025-11-21 15:18:48 +00:00

60 KiB
Raw Blame History

WordPress Plugin Integration Guide

Version: 1.0.0
Last Updated: 2025-11-17

Complete guide for integrating WordPress plugins with IGNY8 API v1.0.


Overview

This guide helps WordPress plugin developers integrate with the IGNY8 API using the unified response format.


Implementation Roadmap (2025-11 refresh)

The bridge now follows a hands-off model: once a site connects and saves the recommended settings, all data collection and sync flows run automatically via cron/webhooks. The detailed engineering schedule lives in docs/wp-bridge-implementation-plan.md; use it for sprint planning and status tracking.

Module Integration Matrix

Area WordPress Bridge Responsibilities SaaS API / Endpoint Status
Admin & Auth Store creds, expose post-type/Woo toggles, control mode, diagnostics panel /auth/login/, /auth/refresh/, /system/sites/{id}/settings/ (planned) Auth live; remote-settings endpoint pending
Taxonomies & Keywords Mirror sectors/clusters as custom taxonomies, attach keywords to post meta, render read-only badges /planner/sectors/, /planner/clusters/, /planner/keywords/ In progress
Writer Tasks Pull new tasks, create drafts, push status/URL updates, cache briefs /writer/tasks/, /writer/tasks/{id}/, /writer/tasks/{id}/brief/ (pending) Push path live; brief endpoint pending
Site Data & Semantic Map Scheduled full/incremental scans, submit site payloads, store semantic metadata /system/sites/{id}/import/, /planner/sites/{id}/semantic-map/ Collection live; semantic-map read pending
Planner / Linker / Optimizer Hooks Attach briefs, export link graph, accept link recommendations & optimizer jobs /planner/tasks/{id}/refresh/, /linker/link-map/, /optimizer/jobs/ Requires new SaaS endpoints
Webhooks & Automation Provide secured WP REST endpoints for SaaS events (task ready, link suggestion, optimizer action) SaaS → /wp-json/igny8/v1/event WP side planned; SaaS needs outbound hooks
Monitoring & Tooling WP-CLI commands, logging, admin notices, health widget /system/ping/, /system/sites/{id}/status/ Ping/status endpoints pending

Reference docs/missing-saas-api-endpoints.md for any API work the SaaS team must complete before the bridge can fully automate these areas.


Authentication

Getting Access Token

function igny8_login($email, $password) {
    $response = wp_remote_post('https://api.igny8.com/api/v1/auth/login/', [
        'headers' => [
            'Content-Type' => 'application/json'
        ],
        'body' => json_encode([
            'email' => $email,
            'password' => $password
        ])
    ]);
    
    $body = json_decode(wp_remote_retrieve_body($response), true);
    
    if ($body['success']) {
        // Store tokens
        update_option('igny8_access_token', $body['data']['access']);
        update_option('igny8_refresh_token', $body['data']['refresh']);
        return $body['data']['access'];
    } else {
        return new WP_Error('login_failed', $body['error']);
    }
}

Using Access Token

function igny8_get_headers() {
    $token = get_option('igny8_access_token');
    
    if (!$token) {
        return false;
    }
    
    return [
        'Authorization' => 'Bearer ' . $token,
        'Content-Type' => 'application/json'
    ];
}

API Client Class

Complete PHP Implementation

class Igny8API {
    private $base_url = 'https://api.igny8.com/api/v1';
    private $access_token = null;
    private $refresh_token = null;
    
    public function __construct() {
        $this->access_token = get_option('igny8_access_token');
        $this->refresh_token = get_option('igny8_refresh_token');
    }
    
    /**
     * Login and store tokens
     */
    public function login($email, $password) {
        $response = wp_remote_post($this->base_url . '/auth/login/', [
            'headers' => [
                'Content-Type' => 'application/json'
            ],
            'body' => json_encode([
                'email' => $email,
                'password' => $password
            ])
        ]);
        
        $body = $this->parse_response($response);
        
        if ($body['success']) {
            $this->access_token = $body['data']['access'];
            $this->refresh_token = $body['data']['refresh'];
            
            update_option('igny8_access_token', $this->access_token);
            update_option('igny8_refresh_token', $this->refresh_token);
            
            return true;
        }
        
        return false;
    }
    
    /**
     * Refresh access token
     */
    public function refresh_token() {
        if (!$this->refresh_token) {
            return false;
        }
        
        $response = wp_remote_post($this->base_url . '/auth/refresh/', [
            'headers' => [
                'Content-Type' => 'application/json'
            ],
            'body' => json_encode([
                'refresh' => $this->refresh_token
            ])
        ]);
        
        $body = $this->parse_response($response);
        
        if ($body['success']) {
            $this->access_token = $body['data']['access'];
            $this->refresh_token = $body['data']['refresh'];
            
            update_option('igny8_access_token', $this->access_token);
            update_option('igny8_refresh_token', $this->refresh_token);
            
            return true;
        }
        
        return false;
    }
    
    /**
     * Parse unified API response
     */
    private function parse_response($response) {
        if (is_wp_error($response)) {
            return [
                'success' => false,
                'error' => $response->get_error_message()
            ];
        }
        
        $body = json_decode(wp_remote_retrieve_body($response), true);
        $status_code = wp_remote_retrieve_response_code($response);
        
        // Handle non-JSON responses
        if (!$body) {
            return [
                'success' => false,
                'error' => 'Invalid response format'
            ];
        }
        
        // Check if response follows unified format
        if (isset($body['success'])) {
            return $body;
        }
        
        // Legacy format - wrap in unified format
        if ($status_code >= 200 && $status_code < 300) {
            return [
                'success' => true,
                'data' => $body
            ];
        } else {
            return [
                'success' => false,
                'error' => $body['detail'] ?? 'Unknown error'
            ];
        }
    }
    
    /**
     * Get headers with authentication
     */
    private function get_headers() {
        if (!$this->access_token) {
            throw new Exception('Not authenticated');
        }
        
        return [
            'Authorization' => 'Bearer ' . $this->access_token,
            'Content-Type' => 'application/json'
        ];
    }
    
    /**
     * Make GET request
     */
    public function get($endpoint) {
        $response = wp_remote_get($this->base_url . $endpoint, [
            'headers' => $this->get_headers()
        ]);
        
        $body = $this->parse_response($response);
        
        // Handle 401 - token expired
        if (!$body['success'] && wp_remote_retrieve_response_code($response) == 401) {
            // Try to refresh token
            if ($this->refresh_token()) {
                // Retry request
                $response = wp_remote_get($this->base_url . $endpoint, [
                    'headers' => $this->get_headers()
                ]);
                $body = $this->parse_response($response);
            }
        }
        
        return $body;
    }
    
    /**
     * Make POST request
     */
    public function post($endpoint, $data) {
        $response = wp_remote_post($this->base_url . $endpoint, [
            'headers' => $this->get_headers(),
            'body' => json_encode($data)
        ]);
        
        $body = $this->parse_response($response);
        
        // Handle 401 - token expired
        if (!$body['success'] && wp_remote_retrieve_response_code($response) == 401) {
            // Try to refresh token
            if ($this->refresh_token()) {
                // Retry request
                $response = wp_remote_post($this->base_url . $endpoint, [
                    'headers' => $this->get_headers(),
                    'body' => json_encode($data)
                ]);
                $body = $this->parse_response($response);
            }
        }
        
        return $body;
    }
    
    /**
     * Make PUT request
     */
    public function put($endpoint, $data) {
        $response = wp_remote_request($this->base_url . $endpoint, [
            'method' => 'PUT',
            'headers' => $this->get_headers(),
            'body' => json_encode($data)
        ]);
        
        return $this->parse_response($response);
    }
    
    /**
     * Make DELETE request
     */
    public function delete($endpoint) {
        $response = wp_remote_request($this->base_url . $endpoint, [
            'method' => 'DELETE',
            'headers' => $this->get_headers()
        ]);
        
        return $this->parse_response($response);
    }
}

Usage Examples

Get Keywords

$api = new Igny8API();

// Get keywords
$response = $api->get('/planner/keywords/');

if ($response['success']) {
    $keywords = $response['results'];
    $count = $response['count'];
    
    foreach ($keywords as $keyword) {
        echo $keyword['name'] . '<br>';
    }
} else {
    echo 'Error: ' . $response['error'];
}

Create Keyword

$api = new Igny8API();

$data = [
    'seed_keyword_id' => 1,
    'site_id' => 1,
    'sector_id' => 1,
    'status' => 'active'
];

$response = $api->post('/planner/keywords/', $data);

if ($response['success']) {
    $keyword = $response['data'];
    echo 'Created keyword: ' . $keyword['id'];
} else {
    echo 'Error: ' . $response['error'];
    if (isset($response['errors'])) {
        foreach ($response['errors'] as $field => $errors) {
            echo $field . ': ' . implode(', ', $errors) . '<br>';
        }
    }
}

Handle Pagination

$api = new Igny8API();

function get_all_keywords($api) {
    $all_keywords = [];
    $page = 1;
    
    do {
        $response = $api->get("/planner/keywords/?page={$page}&page_size=100");
        
        if ($response['success']) {
            $all_keywords = array_merge($all_keywords, $response['results']);
            $page++;
        } else {
            break;
        }
    } while ($response['next']);
    
    return $all_keywords;
}

$keywords = get_all_keywords($api);

Handle Rate Limiting

function make_rate_limited_request($api, $endpoint, $max_retries = 3) {
    for ($attempt = 0; $attempt < $max_retries; $attempt++) {
        $response = $api->get($endpoint);
        
        // Check if rate limited
        if (!$response['success'] && isset($response['error'])) {
            if (strpos($response['error'], 'Rate limit') !== false) {
                // Wait before retry
                sleep(pow(2, $attempt)); // Exponential backoff
                continue;
            }
        }
        
        return $response;
    }
    
    return ['success' => false, 'error' => 'Max retries exceeded'];
}

Error Handling

Unified Error Handling

function handle_api_response($response) {
    if ($response['success']) {
        return $response['data'] ?? $response['results'];
    } else {
        $error_message = $response['error'];
        
        // Log error with request ID
        error_log(sprintf(
            'IGNY8 API Error: %s (Request ID: %s)',
            $error_message,
            $response['request_id'] ?? 'unknown'
        ));
        
        // Handle field-specific errors
        if (isset($response['errors'])) {
            foreach ($response['errors'] as $field => $errors) {
                error_log("  {$field}: " . implode(', ', $errors));
            }
        }
        
        return new WP_Error('igny8_api_error', $error_message, $response);
    }
}

Best Practices

1. Store Tokens Securely

// Use WordPress options API with encryption
function save_token($token) {
    // Encrypt token before storing
    $encrypted = base64_encode($token);
    update_option('igny8_access_token', $encrypted, false);
}

function get_token() {
    $encrypted = get_option('igny8_access_token');
    return base64_decode($encrypted);
}

2. Implement Token Refresh

function ensure_valid_token($api) {
    // Check if token is about to expire (refresh 1 minute before)
    // Token expires in 15 minutes, refresh at 14 minutes
    $last_refresh = get_option('igny8_token_refreshed_at', 0);
    
    if (time() - $last_refresh > 14 * 60) {
        if ($api->refresh_token()) {
            update_option('igny8_token_refreshed_at', time());
        }
    }
}

3. Cache Responses

function get_cached_keywords($api, $cache_key = 'igny8_keywords', $ttl = 300) {
    $cached = get_transient($cache_key);
    
    if ($cached !== false) {
        return $cached;
    }
    
    $response = $api->get('/planner/keywords/');
    
    if ($response['success']) {
        $keywords = $response['results'];
        set_transient($cache_key, $keywords, $ttl);
        return $keywords;
    }
    
    return false;
}

4. Handle Rate Limits

function check_rate_limit($response) {
    // Note: WordPress wp_remote_* doesn't expose all headers easily
    // Consider using cURL or checking response for 429 status
    
    if (isset($response['error']) && strpos($response['error'], 'Rate limit') !== false) {
        // Wait and retry
        sleep(60);
        return true; // Should retry
    }
    
    return false;
}

WordPress Admin Integration

Settings Page

function igny8_settings_page() {
    ?>
    <div class="wrap">
        <h1>IGNY8 API Settings</h1>
        <form method="post" action="options.php">
            <?php settings_fields('igny8_settings'); ?>
            <table class="form-table">
                <tr>
                    <th>API Email</th>
                    <td><input type="email" name="igny8_email" value="<?php echo get_option('igny8_email'); ?>" /></td>
                </tr>
                <tr>
                    <th>API Password</th>
                    <td><input type="password" name="igny8_password" value="" /></td>
                </tr>
            </table>
            <?php submit_button('Save & Connect'); ?>
        </form>
    </div>
    <?php
}

function igny8_save_settings() {
    if (isset($_POST['igny8_email']) && isset($_POST['igny8_password'])) {
        $api = new Igny8API();
        
        if ($api->login($_POST['igny8_email'], $_POST['igny8_password'])) {
            update_option('igny8_email', $_POST['igny8_email']);
            add_settings_error('igny8_settings', 'igny8_connected', 'Successfully connected to IGNY8 API', 'updated');
        } else {
            add_settings_error('igny8_settings', 'igny8_error', 'Failed to connect to IGNY8 API', 'error');
        }
    }
}
add_action('admin_init', 'igny8_save_settings');
  • Post Type Toggles store JSON option (e.g., igny8_enabled_post_types) so the bridge only syncs what the SaaS site expects (post, page, product, custom CPTs).
  • WooCommerce Switches enable/disable product sync, inventory fields, and taxonomy export.
  • Control Mode Selector choose between mirror (IGNY8 authoritative; WP read-only) and hybrid (WP edits allowed and synced back).
  • Automation Status Panel show last cron run, last webhook received, token age, and any queued failures.

Once these settings are saved, cron jobs (igny8_schedule_cron_jobs) and webhook endpoints take over automatically—no manual “sync” buttons are needed outside of troubleshooting. The exact flow chart lives in docs/wp-bridge-implementation-plan.md.


Testing

Unit Tests

class TestIgny8API extends WP_UnitTestCase {
    public function test_login() {
        $api = new Igny8API();
        $result = $api->login('test@example.com', 'password');
        
        $this->assertTrue($result);
        $this->assertNotEmpty(get_option('igny8_access_token'));
    }
    
    public function test_get_keywords() {
        $api = new Igny8API();
        $response = $api->get('/planner/keywords/');
        
        $this->assertTrue($response['success']);
        $this->assertArrayHasKey('results', $response);
        $this->assertArrayHasKey('count', $response);
    }
}

Troubleshooting

Issue: Authentication Fails

Check:

  1. Email and password are correct
  2. Account is active
  3. API endpoint is accessible

Issue: Token Expires Frequently

Solution: Implement automatic token refresh before expiration.

Issue: Rate Limited

Solution: Implement request throttling and caching.


WordPress Hooks and Two-Way Sync

Overview

The integration supports two-way synchronization:

  • IGNY8 → WordPress: Publishing content from IGNY8 to WordPress
  • WordPress → IGNY8: Syncing WordPress post status changes back to IGNY8

WordPress Post Hooks

1. Post Save Hook (save_post)

Hook into WordPress post saves to sync status back to IGNY8:

/**
 * Sync WordPress post status to IGNY8 when post is saved
 */
add_action('save_post', 'igny8_sync_post_status_to_igny8', 10, 3);

function igny8_sync_post_status_to_igny8($post_id, $post, $update) {
    // Skip autosaves and revisions
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    
    if (wp_is_post_revision($post_id)) {
        return;
    }
    
    // Only sync IGNY8-managed posts
    $task_id = get_post_meta($post_id, '_igny8_task_id', true);
    if (!$task_id) {
        return;
    }
    
    // Get post status
    $post_status = $post->post_status;
    
    // Map WordPress status to IGNY8 task status
    $task_status_map = [
        'publish' => 'completed',
        'draft' => 'draft',
        'pending' => 'pending',
        'private' => 'completed',
        'trash' => 'archived'
    ];
    
    $task_status = $task_status_map[$post_status] ?? 'draft';
    
    // Sync to IGNY8 API
    $api = new Igny8API();
    $response = $api->put("/writer/tasks/{$task_id}/", [
        'status' => $task_status,
        'assigned_post_id' => $post_id,
        'post_url' => get_permalink($post_id)
    ]);
    
    if ($response['success']) {
        error_log("IGNY8: Synced post {$post_id} status to task {$task_id}");
    } else {
        error_log("IGNY8: Failed to sync post status: " . $response['error']);
    }
}

2. Post Publish Hook (publish_post)

Update keyword status when content is published:

/**
 * Update keyword status when WordPress post is published
 */
add_action('publish_post', 'igny8_update_keywords_on_post_publish', 10, 1);
add_action('publish_page', 'igny8_update_keywords_on_post_publish', 10, 1);
add_action('draft_to_publish', 'igny8_update_keywords_on_post_publish', 10, 1);
add_action('future_to_publish', 'igny8_update_keywords_on_post_publish', 10, 1);

function igny8_update_keywords_on_post_publish($post_id) {
    // Get task ID from post meta
    $task_id = get_post_meta($post_id, '_igny8_task_id', true);
    if (!$task_id) {
        return;
    }
    
    $api = new Igny8API();
    
    // Get task details to find associated cluster/keywords
    $task_response = $api->get("/writer/tasks/{$task_id}/");
    
    if (!$task_response['success']) {
        return;
    }
    
    $task = $task_response['data'];
    $cluster_id = $task['cluster_id'] ?? null;
    
    if ($cluster_id) {
        // Get keywords in this cluster
        $keywords_response = $api->get("/planner/keywords/?cluster_id={$cluster_id}");
        
        if ($keywords_response['success']) {
            $keywords = $keywords_response['results'];
            
            // Update each keyword status to 'mapped'
            foreach ($keywords as $keyword) {
                $api->put("/planner/keywords/{$keyword['id']}/", [
                    'status' => 'mapped'
                ]);
            }
        }
    }
    
    // Update task status to completed
    $api->put("/writer/tasks/{$task_id}/", [
        'status' => 'completed',
        'assigned_post_id' => $post_id,
        'post_url' => get_permalink($post_id)
    ]);
}

3. Post Status Change Hook (transition_post_status)

Handle all post status transitions:

/**
 * Sync post status changes to IGNY8
 */
add_action('transition_post_status', 'igny8_sync_post_status_transition', 10, 3);

function igny8_sync_post_status_transition($new_status, $old_status, $post) {
    // Skip if status hasn't changed
    if ($new_status === $old_status) {
        return;
    }
    
    // Only sync IGNY8-managed posts
    $task_id = get_post_meta($post->ID, '_igny8_task_id', true);
    if (!$task_id) {
        return;
    }
    
    $api = new Igny8API();
    
    // Map WordPress status to IGNY8 task status
    $status_map = [
        'publish' => 'completed',
        'draft' => 'draft',
        'pending' => 'pending',
        'private' => 'completed',
        'trash' => 'archived',
        'future' => 'scheduled'
    ];
    
    $task_status = $status_map[$new_status] ?? 'draft';
    
    // Sync to IGNY8
    $response = $api->put("/writer/tasks/{$task_id}/", [
        'status' => $task_status,
        'assigned_post_id' => $post->ID,
        'post_url' => get_permalink($post->ID)
    ]);
    
    if ($response['success']) {
        do_action('igny8_post_status_synced', $post->ID, $task_id, $new_status);
    }
}

Fetching WordPress Post Status

Get Post Status from WordPress

/**
 * Get WordPress post status and sync to IGNY8
 */
function igny8_fetch_and_sync_post_status($post_id) {
    $post = get_post($post_id);
    
    if (!$post) {
        return false;
    }
    
    // Get post status
    $wp_status = $post->post_status;
    
    // Get additional post data
    $post_data = [
        'id' => $post_id,
        'status' => $wp_status,
        'title' => $post->post_title,
        'url' => get_permalink($post_id),
        'modified' => $post->post_modified,
        'published' => $post->post_date
    ];
    
    // Get task ID
    $task_id = get_post_meta($post_id, '_igny8_task_id', true);
    
    if (!$task_id) {
        return false;
    }
    
    // Sync to IGNY8
    $api = new Igny8API();
    
    // Map WordPress status to IGNY8 status
    $status_map = [
        'publish' => 'completed',
        'draft' => 'draft',
        'pending' => 'pending',
        'private' => 'completed',
        'trash' => 'archived'
    ];
    
    $task_status = $status_map[$wp_status] ?? 'draft';
    
    $response = $api->put("/writer/tasks/{$task_id}/", [
        'status' => $task_status,
        'assigned_post_id' => $post_id,
        'post_url' => $post_data['url']
    ]);
    
    return $response['success'];
}

Batch Sync Post Statuses

/**
 * Sync all IGNY8-managed posts status to IGNY8 API
 */
function igny8_batch_sync_post_statuses() {
    global $wpdb;
    
    // Get all posts with IGNY8 task ID
    $posts = $wpdb->get_results("
        SELECT p.ID, p.post_status, p.post_title, pm.meta_value as task_id
        FROM {$wpdb->posts} p
        INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
        WHERE pm.meta_key = '_igny8_task_id'
        AND p.post_type IN ('post', 'page')
        AND p.post_status != 'trash'
    ");
    
    $api = new Igny8API();
    $synced = 0;
    $failed = 0;
    
    foreach ($posts as $post_data) {
        $post_id = $post_data->ID;
        $task_id = intval($post_data->task_id);
        $wp_status = $post_data->post_status;
        
        // Map status
        $status_map = [
            'publish' => 'completed',
            'draft' => 'draft',
            'pending' => 'pending',
            'private' => 'completed'
        ];
        
        $task_status = $status_map[$wp_status] ?? 'draft';
        
        // Sync to IGNY8
        $response = $api->put("/writer/tasks/{$task_id}/", [
            'status' => $task_status,
            'assigned_post_id' => $post_id,
            'post_url' => get_permalink($post_id)
        ]);
        
        if ($response['success']) {
            $synced++;
        } else {
            $failed++;
            error_log("IGNY8: Failed to sync post {$post_id}: " . $response['error']);
        }
    }
    
    return [
        'synced' => $synced,
        'failed' => $failed,
        'total' => count($posts)
    ];
}

Complete Two-Way Sync Example

/**
 * Complete two-way sync implementation
 */
class Igny8WordPressSync {
    private $api;
    
    public function __construct() {
        $this->api = new Igny8API();
        
        // WordPress → IGNY8 hooks
        add_action('save_post', [$this, 'sync_post_to_igny8'], 10, 3);
        add_action('publish_post', [$this, 'update_keywords_on_publish'], 10, 1);
        add_action('transition_post_status', [$this, 'sync_status_transition'], 10, 3);
        
        // IGNY8 → WordPress (when content is published from IGNY8)
        add_action('igny8_content_published', [$this, 'create_wordpress_post'], 10, 1);
    }
    
    /**
     * WordPress → IGNY8: Sync post changes to IGNY8
     */
    public function sync_post_to_igny8($post_id, $post, $update) {
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }
        
        if (wp_is_post_revision($post_id)) {
            return;
        }
        
        $task_id = get_post_meta($post_id, '_igny8_task_id', true);
        if (!$task_id) {
            return;
        }
        
        $status_map = [
            'publish' => 'completed',
            'draft' => 'draft',
            'pending' => 'pending',
            'private' => 'completed',
            'trash' => 'archived'
        ];
        
        $task_status = $status_map[$post->post_status] ?? 'draft';
        
        $response = $this->api->put("/writer/tasks/{$task_id}/", [
            'status' => $task_status,
            'assigned_post_id' => $post_id,
            'post_url' => get_permalink($post_id)
        ]);
        
        if ($response['success']) {
            error_log("IGNY8: Synced post {$post_id} to task {$task_id}");
        }
    }
    
    /**
     * WordPress → IGNY8: Update keywords when post is published
     */
    public function update_keywords_on_publish($post_id) {
        $task_id = get_post_meta($post_id, '_igny8_task_id', true);
        if (!$task_id) {
            return;
        }
        
        // Get task to find cluster
        $task_response = $this->api->get("/writer/tasks/{$task_id}/");
        if (!$task_response['success']) {
            return;
        }
        
        $task = $task_response['data'];
        $cluster_id = $task['cluster_id'] ?? null;
        
        if ($cluster_id) {
            // Update keywords in cluster to 'mapped'
            $keywords_response = $this->api->get("/planner/keywords/?cluster_id={$cluster_id}");
            if ($keywords_response['success']) {
                foreach ($keywords_response['results'] as $keyword) {
                    $this->api->put("/planner/keywords/{$keyword['id']}/", [
                        'status' => 'mapped'
                    ]);
                }
            }
        }
        
        // Update task status
        $this->api->put("/writer/tasks/{$task_id}/", [
            'status' => 'completed',
            'assigned_post_id' => $post_id,
            'post_url' => get_permalink($post_id)
        ]);
    }
    
    /**
     * WordPress → IGNY8: Handle status transitions
     */
    public function sync_status_transition($new_status, $old_status, $post) {
        if ($new_status === $old_status) {
            return;
        }
        
        $task_id = get_post_meta($post->ID, '_igny8_task_id', true);
        if (!$task_id) {
            return;
        }
        
        $status_map = [
            'publish' => 'completed',
            'draft' => 'draft',
            'pending' => 'pending',
            'private' => 'completed',
            'trash' => 'archived'
        ];
        
        $task_status = $status_map[$new_status] ?? 'draft';
        
        $this->api->put("/writer/tasks/{$task_id}/", [
            'status' => $task_status,
            'assigned_post_id' => $post->ID,
            'post_url' => get_permalink($post->ID)
        ]);
    }
    
    /**
     * IGNY8 → WordPress: Create WordPress post from IGNY8 content
     */
    public function create_wordpress_post($content_data) {
        $post_data = [
            'post_title' => $content_data['title'],
            'post_content' => $content_data['content'],
            'post_status' => $content_data['status'] ?? 'draft',
            'post_type' => 'post',
            'meta_input' => [
                '_igny8_task_id' => $content_data['task_id'],
                '_igny8_content_id' => $content_data['content_id']
            ]
        ];
        
        $post_id = wp_insert_post($post_data);
        
        if (!is_wp_error($post_id)) {
            // Update IGNY8 task with WordPress post ID
            $this->api->put("/writer/tasks/{$content_data['task_id']}/", [
                'assigned_post_id' => $post_id,
                'post_url' => get_permalink($post_id)
            ]);
        }
        
        return $post_id;
    }
}

// Initialize sync
new Igny8WordPressSync();

WordPress Post Status Mapping

WordPress Status IGNY8 Task Status Description
publish completed Post is published
draft draft Post is draft
pending pending Post is pending review
private completed Post is private (published)
trash archived Post is deleted/trashed
future scheduled Post is scheduled

Fetching WordPress Post Data

/**
 * Get WordPress post data for IGNY8 sync
 */
function igny8_get_post_data_for_sync($post_id) {
    $post = get_post($post_id);
    
    if (!$post) {
        return false;
    }
    
    return [
        'id' => $post_id,
        'title' => $post->post_title,
        'status' => $post->post_status,
        'url' => get_permalink($post_id),
        'modified' => $post->post_modified,
        'published' => $post->post_date,
        'author' => get_the_author_meta('display_name', $post->post_author),
        'word_count' => str_word_count(strip_tags($post->post_content)),
        'meta' => [
            'task_id' => get_post_meta($post_id, '_igny8_task_id', true),
            'content_id' => get_post_meta($post_id, '_igny8_content_id', true),
            'primary_keywords' => get_post_meta($post_id, '_igny8_primary_keywords', true)
        ]
    ];
}

Scheduled Sync (Cron Job)

/**
 * Scheduled sync of WordPress post statuses to IGNY8
 */
add_action('igny8_sync_post_statuses', 'igny8_cron_sync_post_statuses');

function igny8_cron_sync_post_statuses() {
    $result = igny8_batch_sync_post_statuses();
    
    error_log(sprintf(
        'IGNY8: Synced %d posts, %d failed',
        $result['synced'],
        $result['failed']
    ));
}

// Schedule daily sync
if (!wp_next_scheduled('igny8_sync_post_statuses')) {
    wp_schedule_event(time(), 'daily', 'igny8_sync_post_statuses');
}

Complete Integration Flow

IGNY8 → WordPress Flow

  1. Content generated in IGNY8
  2. Task created/updated in IGNY8
  3. WordPress post created via wp_insert_post()
  4. Post meta saved with _igny8_task_id
  5. IGNY8 task updated with WordPress post ID

WordPress → IGNY8 Flow

  1. User saves/publishes WordPress post
  2. save_post or publish_post hook fires
  3. Plugin gets _igny8_task_id from post meta
  4. Plugin calls IGNY8 API to update task status
  5. If published, keywords updated to 'mapped' status
  6. IGNY8 task status synced

WordPress Site Data Fetching and Semantic Mapping

Overview

After WordPress site integration and API verification, you can fetch comprehensive site data (posts, taxonomies, products, attributes) and send it to IGNY8 for semantic strategy mapping. This enables content restructuring and site-wide optimization.


Fetching WordPress Posts

Get All Post Types

/**
 * Fetch all posts of a specific type from WordPress
 */
function igny8_fetch_wordpress_posts($post_type = 'post', $per_page = 100) {
    $api = new Igny8API();
    
    // Use WordPress REST API to fetch posts
    $wp_response = wp_remote_get(sprintf(
        '%s/wp-json/wp/v2/%s?per_page=%d&status=publish',
        get_site_url(),
        $post_type,
        $per_page
    ));
    
    if (is_wp_error($wp_response)) {
        return false;
    }
    
    $posts = json_decode(wp_remote_retrieve_body($wp_response), true);
    
    // Format posts for IGNY8
    $formatted_posts = [];
    foreach ($posts as $post) {
        $formatted_posts[] = [
            'id' => $post['id'],
            'title' => $post['title']['rendered'],
            'content' => $post['content']['rendered'],
            'excerpt' => $post['excerpt']['rendered'],
            'status' => $post['status'],
            'url' => $post['link'],
            'published' => $post['date'],
            'modified' => $post['modified'],
            'author' => $post['author'],
            'featured_image' => $post['featured_media'] ? wp_get_attachment_url($post['featured_media']) : null,
            'categories' => $post['categories'] ?? [],
            'tags' => $post['tags'] ?? [],
            'post_type' => $post_type,
            'meta' => [
                'word_count' => str_word_count(strip_tags($post['content']['rendered'])),
                'reading_time' => ceil(str_word_count(strip_tags($post['content']['rendered'])) / 200)
            ]
        ];
    }
    
    return $formatted_posts;
}

Get All Post Types

/**
 * Fetch all available post types from WordPress
 */
function igny8_fetch_all_post_types() {
    $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/types');
    
    if (is_wp_error($wp_response)) {
        return false;
    }
    
    $types = json_decode(wp_remote_retrieve_body($wp_response), true);
    
    $post_types = [];
    foreach ($types as $type_name => $type_data) {
        if ($type_data['public']) {
            $post_types[] = [
                'name' => $type_name,
                'label' => $type_data['name'],
                'description' => $type_data['description'] ?? '',
                'rest_base' => $type_data['rest_base'] ?? $type_name
            ];
        }
    }
    
    return $post_types;
}

Batch Fetch All Posts

/**
 * Fetch all posts from all post types
 */
function igny8_fetch_all_wordpress_posts() {
    $post_types = igny8_fetch_all_post_types();
    $all_posts = [];
    
    foreach ($post_types as $type) {
        $posts = igny8_fetch_wordpress_posts($type['name'], 100);
        if ($posts) {
            $all_posts = array_merge($all_posts, $posts);
        }
    }
    
    return $all_posts;
}

Fetching WordPress Taxonomies

Get All Taxonomies

/**
 * Fetch all taxonomies from WordPress
 */
function igny8_fetch_wordpress_taxonomies() {
    $wp_response = wp_remote_get(get_site_url() . '/wp-json/wp/v2/taxonomies');
    
    if (is_wp_error($wp_response)) {
        return false;
    }
    
    $taxonomies = json_decode(wp_remote_retrieve_body($wp_response), true);
    
    $formatted_taxonomies = [];
    foreach ($taxonomies as $tax_name => $tax_data) {
        if ($tax_data['public']) {
            $formatted_taxonomies[] = [
                'name' => $tax_name,
                'label' => $tax_data['name'],
                'description' => $tax_data['description'] ?? '',
                'hierarchical' => $tax_data['hierarchical'],
                'rest_base' => $tax_data['rest_base'] ?? $tax_name,
                'object_types' => $tax_data['types'] ?? []
            ];
        }
    }
    
    return $formatted_taxonomies;
}

Get Taxonomy Terms

/**
 * Fetch all terms for a specific taxonomy
 */
function igny8_fetch_taxonomy_terms($taxonomy, $per_page = 100) {
    $api = new Igny8API();
    
    $wp_response = wp_remote_get(sprintf(
        '%s/wp-json/wp/v2/%s?per_page=%d',
        get_site_url(),
        $taxonomy,
        $per_page
    ));
    
    if (is_wp_error($wp_response)) {
        return false;
    }
    
    $terms = json_decode(wp_remote_retrieve_body($wp_response), true);
    
    $formatted_terms = [];
    foreach ($terms as $term) {
        $formatted_terms[] = [
            'id' => $term['id'],
            'name' => $term['name'],
            'slug' => $term['slug'],
            'description' => $term['description'] ?? '',
            'count' => $term['count'],
            'parent' => $term['parent'] ?? 0,
            'taxonomy' => $taxonomy,
            'url' => $term['link']
        ];
    }
    
    return $formatted_terms;
}

Get All Taxonomy Terms

/**
 * Fetch all terms from all taxonomies
 */
function igny8_fetch_all_taxonomy_terms() {
    $taxonomies = igny8_fetch_wordpress_taxonomies();
    $all_terms = [];
    
    foreach ($taxonomies as $taxonomy) {
        $terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], 100);
        if ($terms) {
            $all_terms[$taxonomy['name']] = $terms;
        }
    }
    
    return $all_terms;
}

Fetching WooCommerce Products

Get All Products

/**
 * Fetch all WooCommerce products
 */
function igny8_fetch_woocommerce_products($per_page = 100) {
    // Check if WooCommerce is active
    if (!class_exists('WooCommerce')) {
        return false;
    }
    
    $wp_response = wp_remote_get(sprintf(
        '%s/wp-json/wc/v3/products?per_page=%d&status=publish',
        get_site_url(),
        $per_page
    ), [
        'headers' => [
            'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret'))
        ]
    ]);
    
    if (is_wp_error($wp_response)) {
        return false;
    }
    
    $products = json_decode(wp_remote_retrieve_body($wp_response), true);
    
    $formatted_products = [];
    foreach ($products as $product) {
        $formatted_products[] = [
            'id' => $product['id'],
            'name' => $product['name'],
            'slug' => $product['slug'],
            'sku' => $product['sku'],
            'type' => $product['type'],
            'status' => $product['status'],
            'description' => $product['description'],
            'short_description' => $product['short_description'],
            'price' => $product['price'],
            'regular_price' => $product['regular_price'],
            'sale_price' => $product['sale_price'],
            'on_sale' => $product['on_sale'],
            'stock_status' => $product['stock_status'],
            'stock_quantity' => $product['stock_quantity'],
            'categories' => $product['categories'] ?? [],
            'tags' => $product['tags'] ?? [],
            'images' => $product['images'] ?? [],
            'attributes' => $product['attributes'] ?? [],
            'variations' => $product['variations'] ?? [],
            'url' => $product['permalink']
        ];
    }
    
    return $formatted_products;
}

Get Product Categories

/**
 * Fetch WooCommerce product categories
 */
function igny8_fetch_product_categories($per_page = 100) {
    if (!class_exists('WooCommerce')) {
        return false;
    }
    
    $wp_response = wp_remote_get(sprintf(
        '%s/wp-json/wc/v3/products/categories?per_page=%d',
        get_site_url(),
        $per_page
    ), [
        'headers' => [
            'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret'))
        ]
    ]);
    
    if (is_wp_error($wp_response)) {
        return false;
    }
    
    $categories = json_decode(wp_remote_retrieve_body($wp_response), true);
    
    $formatted_categories = [];
    foreach ($categories as $category) {
        $formatted_categories[] = [
            'id' => $category['id'],
            'name' => $category['name'],
            'slug' => $category['slug'],
            'description' => $category['description'] ?? '',
            'count' => $category['count'],
            'parent' => $category['parent'] ?? 0,
            'image' => $category['image']['src'] ?? null
        ];
    }
    
    return $formatted_categories;
}

Get Product Attributes

/**
 * Fetch WooCommerce product attributes
 */
function igny8_fetch_product_attributes() {
    if (!class_exists('WooCommerce')) {
        return false;
    }
    
    $wp_response = wp_remote_get(
        get_site_url() . '/wp-json/wc/v3/products/attributes',
        [
            'headers' => [
                'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret'))
            ]
        ]
    );
    
    if (is_wp_error($wp_response)) {
        return false;
    }
    
    $attributes = json_decode(wp_remote_retrieve_body($wp_response), true);
    
    $formatted_attributes = [];
    foreach ($attributes as $attribute) {
        // Get attribute terms
        $terms_response = wp_remote_get(sprintf(
            '%s/wp-json/wc/v3/products/attributes/%d/terms',
            get_site_url(),
            $attribute['id']
        ), [
            'headers' => [
                'Authorization' => 'Basic ' . base64_encode(get_option('woocommerce_api_consumer_key') . ':' . get_option('woocommerce_api_consumer_secret'))
            ]
        ]);
        
        $terms = [];
        if (!is_wp_error($terms_response)) {
            $terms_data = json_decode(wp_remote_retrieve_body($terms_response), true);
            foreach ($terms_data as $term) {
                $terms[] = [
                    'id' => $term['id'],
                    'name' => $term['name'],
                    'slug' => $term['slug']
                ];
            }
        }
        
        $formatted_attributes[] = [
            'id' => $attribute['id'],
            'name' => $attribute['name'],
            'slug' => $attribute['slug'],
            'type' => $attribute['type'],
            'order_by' => $attribute['order_by'],
            'has_archives' => $attribute['has_archives'],
            'terms' => $terms
        ];
    }
    
    return $formatted_attributes;
}

Sending Site Data to IGNY8 for Semantic Mapping

Complete Site Data Collection

/**
 * Collect all WordPress site data for IGNY8 semantic mapping
 */
function igny8_collect_site_data() {
    $site_data = [
        'site_url' => get_site_url(),
        'site_name' => get_bloginfo('name'),
        'site_description' => get_bloginfo('description'),
        'collected_at' => current_time('mysql'),
        'posts' => [],
        'taxonomies' => [],
        'products' => [],
        'product_attributes' => []
    ];
    
    // Fetch all posts
    $post_types = igny8_fetch_all_post_types();
    foreach ($post_types as $type) {
        $posts = igny8_fetch_wordpress_posts($type['name'], 100);
        if ($posts) {
            $site_data['posts'] = array_merge($site_data['posts'], $posts);
        }
    }
    
    // Fetch all taxonomies and terms
    $taxonomies = igny8_fetch_wordpress_taxonomies();
    foreach ($taxonomies as $taxonomy) {
        $terms = igny8_fetch_taxonomy_terms($taxonomy['rest_base'], 100);
        if ($terms) {
            $site_data['taxonomies'][$taxonomy['name']] = [
                'taxonomy' => $taxonomy,
                'terms' => $terms
            ];
        }
    }
    
    // Fetch WooCommerce products if available
    if (class_exists('WooCommerce')) {
        $products = igny8_fetch_woocommerce_products(100);
        if ($products) {
            $site_data['products'] = $products;
        }
        
        $product_categories = igny8_fetch_product_categories(100);
        if ($product_categories) {
            $site_data['product_categories'] = $product_categories;
        }
        
        $product_attributes = igny8_fetch_product_attributes();
        if ($product_attributes) {
            $site_data['product_attributes'] = $product_attributes;
        }
    }
    
    return $site_data;
}

Send Site Data to IGNY8 API

/**
 * Send WordPress site data to IGNY8 for semantic strategy mapping
 */
function igny8_send_site_data_to_igny8($site_id) {
    $api = new Igny8API();
    
    // Collect all site data
    $site_data = igny8_collect_site_data();
    
    // Send to IGNY8 API
    // Note: This endpoint may need to be created in IGNY8 API
    $response = $api->post("/system/sites/{$site_id}/import/", [
        'site_data' => $site_data,
        'import_type' => 'full_site_scan'
    ]);
    
    if ($response['success']) {
        // Store import ID for tracking
        update_option('igny8_last_site_import_id', $response['data']['import_id'] ?? null);
        return $response['data'];
    } else {
        error_log("IGNY8: Failed to send site data: " . $response['error']);
        return false;
    }
}

Incremental Site Data Sync

/**
 * Sync only changed posts/taxonomies since last sync
 */
function igny8_sync_incremental_site_data($site_id) {
    $api = new Igny8API();
    
    $last_sync = get_option('igny8_last_site_sync', 0);
    
    // Fetch only posts modified since last sync
    $wp_response = wp_remote_get(sprintf(
        '%s/wp-json/wp/v2/posts?after=%s&per_page=100',
        get_site_url(),
        date('c', $last_sync)
    ));
    
    if (is_wp_error($wp_response)) {
        return false;
    }
    
    $posts = json_decode(wp_remote_retrieve_body($wp_response), true);
    
    if (empty($posts)) {
        return ['synced' => 0, 'message' => 'No changes since last sync'];
    }
    
    // Format posts
    $formatted_posts = [];
    foreach ($posts as $post) {
        $formatted_posts[] = [
            'id' => $post['id'],
            'title' => $post['title']['rendered'],
            'content' => $post['content']['rendered'],
            'status' => $post['status'],
            'modified' => $post['modified'],
            'categories' => $post['categories'] ?? [],
            'tags' => $post['tags'] ?? []
        ];
    }
    
    // Send incremental update to IGNY8
    $response = $api->post("/system/sites/{$site_id}/sync/", [
        'posts' => $formatted_posts,
        'sync_type' => 'incremental',
        'last_sync' => $last_sync
    ]);
    
    if ($response['success']) {
        update_option('igny8_last_site_sync', time());
        return [
            'synced' => count($formatted_posts),
            'message' => 'Incremental sync completed'
        ];
    }
    
    return false;
}

Semantic Strategy Mapping

Map Site Data to IGNY8 Semantic Structure

/**
 * Map WordPress site data to IGNY8 semantic strategy
 * This creates sectors, clusters, and keywords based on site structure
 */
function igny8_map_site_to_semantic_strategy($site_id, $site_data) {
    $api = new Igny8API();
    
    // Extract semantic structure from site data
    $semantic_map = [
        'sectors' => [],
        'clusters' => [],
        'keywords' => []
    ];
    
    // Map taxonomies to sectors
    foreach ($site_data['taxonomies'] as $tax_name => $tax_data) {
        if ($tax_data['taxonomy']['hierarchical']) {
            // Hierarchical taxonomies (categories) become sectors
            $sector = [
                'name' => $tax_data['taxonomy']['label'],
                'slug' => $tax_data['taxonomy']['name'],
                'description' => $tax_data['taxonomy']['description'],
                'source' => 'wordpress_taxonomy',
                'source_id' => $tax_name
            ];
            
            // Map terms to clusters
            $clusters = [];
            foreach ($tax_data['terms'] as $term) {
                $clusters[] = [
                    'name' => $term['name'],
                    'slug' => $term['slug'],
                    'description' => $term['description'],
                    'source' => 'wordpress_term',
                    'source_id' => $term['id']
                ];
                
                // Extract keywords from posts in this term
                $keywords = igny8_extract_keywords_from_term_posts($term['id'], $tax_name);
                $semantic_map['keywords'] = array_merge($semantic_map['keywords'], $keywords);
            }
            
            $sector['clusters'] = $clusters;
            $semantic_map['sectors'][] = $sector;
        }
    }
    
    // Map WooCommerce product categories to sectors
    if (!empty($site_data['product_categories'])) {
        $product_sector = [
            'name' => 'Products',
            'slug' => 'products',
            'description' => 'WooCommerce product categories',
            'source' => 'woocommerce',
            'clusters' => []
        ];
        
        foreach ($site_data['product_categories'] as $category) {
            $product_sector['clusters'][] = [
                'name' => $category['name'],
                'slug' => $category['slug'],
                'description' => $category['description'],
                'source' => 'woocommerce_category',
                'source_id' => $category['id']
            ];
        }
        
        $semantic_map['sectors'][] = $product_sector;
    }
    
    // Send semantic map to IGNY8
    $response = $api->post("/planner/sites/{$site_id}/semantic-map/", [
        'semantic_map' => $semantic_map,
        'site_data' => $site_data
    ]);
    
    return $response;
}

Extract Keywords from Posts

/**
 * Extract keywords from posts associated with a taxonomy term
 */
function igny8_extract_keywords_from_term_posts($term_id, $taxonomy) {
    $args = [
        'post_type' => 'any',
        'posts_per_page' => -1,
        'tax_query' => [
            [
                'taxonomy' => $taxonomy,
                'field' => 'term_id',
                'terms' => $term_id
            ]
        ]
    ];
    
    $query = new WP_Query($args);
    $keywords = [];
    
    if ($query->have_posts()) {
        while ($query->have_posts()) {
            $query->the_post();
            
            // Extract keywords from post title and content
            $title_words = str_word_count(get_the_title(), 1);
            $content_words = str_word_count(strip_tags(get_the_content()), 1);
            
            // Combine and get unique keywords
            $all_words = array_merge($title_words, $content_words);
            $unique_words = array_unique(array_map('strtolower', $all_words));
            
            // Filter out common words (stop words)
            $stop_words = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'];
            $keywords = array_merge($keywords, array_diff($unique_words, $stop_words));
        }
        wp_reset_postdata();
    }
    
    // Format keywords
    $formatted_keywords = [];
    foreach (array_unique($keywords) as $keyword) {
        if (strlen($keyword) > 3) { // Only keywords longer than 3 characters
            $formatted_keywords[] = [
                'keyword' => $keyword,
                'source' => 'wordpress_post',
                'source_term_id' => $term_id
            ];
        }
    }
    
    return $formatted_keywords;
}

Content Restructuring Workflow

Complete Site Analysis and Restructuring

/**
 * Complete workflow: Fetch site data → Map to semantic strategy → Restructure content
 */
function igny8_analyze_and_restructure_site($site_id) {
    $api = new Igny8API();
    
    // Step 1: Collect all site data
    $site_data = igny8_collect_site_data();
    
    // Step 2: Send to IGNY8 for analysis
    $analysis_response = $api->post("/system/sites/{$site_id}/analyze/", [
        'site_data' => $site_data,
        'analysis_type' => 'full_site_restructure'
    ]);
    
    if (!$analysis_response['success']) {
        return false;
    }
    
    $analysis_id = $analysis_response['data']['analysis_id'];
    
    // Step 3: Map to semantic strategy
    $mapping_response = igny8_map_site_to_semantic_strategy($site_id, $site_data);
    
    if (!$mapping_response['success']) {
        return false;
    }
    
    // Step 4: Get restructuring recommendations
    $recommendations_response = $api->get("/system/sites/{$site_id}/recommendations/");
    
    if (!$recommendations_response['success']) {
        return false;
    }
    
    return [
        'analysis_id' => $analysis_id,
        'semantic_map' => $mapping_response['data'],
        'recommendations' => $recommendations_response['data'],
        'site_data_summary' => [
            'total_posts' => count($site_data['posts']),
            'total_taxonomies' => count($site_data['taxonomies']),
            'total_products' => count($site_data['products'] ?? []),
            'total_keywords' => count($site_data['keywords'] ?? [])
        ]
    ];
}

Scheduled Site Data Sync

/**
 * Scheduled sync of WordPress site data to IGNY8
 */
add_action('igny8_sync_site_data', 'igny8_cron_sync_site_data');

function igny8_cron_sync_site_data() {
    $site_id = get_option('igny8_site_id');
    
    if (!$site_id) {
        return;
    }
    
    // Incremental sync
    $result = igny8_sync_incremental_site_data($site_id);
    
    if ($result) {
        error_log(sprintf(
            'IGNY8: Synced %d posts to site %d',
            $result['synced'],
            $site_id
        ));
    }
}

// Schedule daily sync
if (!wp_next_scheduled('igny8_sync_site_data')) {
    wp_schedule_event(time(), 'daily', 'igny8_sync_site_data');
}

Complete Site Integration Class

/**
 * Complete WordPress site integration class
 */
class Igny8SiteIntegration {
    private $api;
    private $site_id;
    
    public function __construct($site_id) {
        $this->api = new Igny8API();
        $this->site_id = $site_id;
    }
    
    /**
     * Full site scan and semantic mapping
     */
    public function full_site_scan() {
        // Collect all data
        $site_data = igny8_collect_site_data();
        
        // Send to IGNY8
        $response = $this->api->post("/system/sites/{$this->site_id}/import/", [
            'site_data' => $site_data,
            'import_type' => 'full_scan'
        ]);
        
        if ($response['success']) {
            // Map to semantic strategy
            $mapping = igny8_map_site_to_semantic_strategy($this->site_id, $site_data);
            
            return [
                'success' => true,
                'import_id' => $response['data']['import_id'] ?? null,
                'semantic_map' => $mapping['data'] ?? null,
                'summary' => [
                    'posts' => count($site_data['posts']),
                    'taxonomies' => count($site_data['taxonomies']),
                    'products' => count($site_data['products'] ?? []),
                    'product_attributes' => count($site_data['product_attributes'] ?? [])
                ]
            ];
        }
        
        return ['success' => false, 'error' => $response['error']];
    }
    
    /**
     * Get semantic strategy recommendations
     */
    public function get_recommendations() {
        $response = $this->api->get("/planner/sites/{$this->site_id}/recommendations/");
        
        if ($response['success']) {
            return $response['data'];
        }
        
        return false;
    }
    
    /**
     * Apply restructuring recommendations
     */
    public function apply_restructuring($recommendations) {
        $response = $this->api->post("/planner/sites/{$this->site_id}/restructure/", [
            'recommendations' => $recommendations
        ]);
        
        return $response['success'];
    }
}

// Usage
$integration = new Igny8SiteIntegration($site_id);
$result = $integration->full_site_scan();

if ($result['success']) {
    echo "Scanned {$result['summary']['posts']} posts, {$result['summary']['taxonomies']} taxonomies";
    
    // Get recommendations
    $recommendations = $integration->get_recommendations();
    
    // Apply restructuring
    if ($recommendations) {
        $integration->apply_restructuring($recommendations);
    }
}

Data Structure Examples

Post Data Structure

[
    'id' => 123,
    'title' => 'Post Title',
    'content' => 'Post content...',
    'excerpt' => 'Post excerpt...',
    'status' => 'publish',
    'url' => 'https://example.com/post/',
    'published' => '2025-01-01T00:00:00',
    'modified' => '2025-01-02T00:00:00',
    'author' => 1,
    'featured_image' => 'https://example.com/image.jpg',
    'categories' => [1, 2, 3],
    'tags' => [4, 5],
    'post_type' => 'post',
    'meta' => [
        'word_count' => 500,
        'reading_time' => 3
    ]
]

Taxonomy Structure

[
    'taxonomy' => [
        'name' => 'category',
        'label' => 'Categories',
        'hierarchical' => true,
        'object_types' => ['post']
    ],
    'terms' => [
        [
            'id' => 1,
            'name' => 'Technology',
            'slug' => 'technology',
            'description' => 'Tech posts',
            'count' => 25,
            'parent' => 0
        ]
    ]
]

Product Structure

[
    'id' => 456,
    'name' => 'Product Name',
    'sku' => 'PROD-123',
    'type' => 'simple',
    'price' => '29.99',
    'categories' => [10, 11],
    'tags' => [20],
    'attributes' => [
        [
            'id' => 1,
            'name' => 'Color',
            'options' => ['Red', 'Blue']
        ]
    ],
    'variations' => [789, 790]
]

Last Updated: 2025-11-16
API Version: 1.0.0


Site Metadata Endpoint (Bridge)

The WordPress Bridge exposes a discovery endpoint that the IGNY8 SaaS app can call to retrieve available post types, taxonomies and counts. The endpoint follows the IGNY8 unified response format and includes plugin-side flags so the SaaS app can decide whether to perform automatic syncs.

  • URL: GET /wp-json/igny8/v1/site-metadata/
  • Authentication: Plugin-side connection must be authenticated. The bridge accepts:
    • Authorization: Bearer <token_or_api_key> (preferred)
    • X-IGNY8-API-KEY: <api_key> (supported) The plugin stores an optional API key in igny8_api_key (secure storage via igny8_store_secure_option() when available) and will use it as the active access token when present.
  • Response shape: IGNY8 unified format — top-level success, data, optional message, and request_id.
  • Caching: Results are cached on the WP side using a transient (igny8_site_metadata_v1) with a default TTL of 300 seconds.

Behavior notes:

  • Toggling "Enable Sync Operations" (option igny8_connection_enabled) or "Enable Two-Way Sync" (igny8_enable_two_way_sync) in the plugin admin only affects background/inbound/outbound sync actions. REST discovery endpoints remain accessible so the SaaS app can still query metadata even when sync is disabled on either side.
  • The endpoint includes two flags in the payload: plugin_connection_enabled and two_way_sync_enabled so the SaaS app can make informed decisions.

Example response:

{
  "success": true,
  "data": {
    "post_types": {
      "post": { "label": "Posts", "count": 123 },
      "page": { "label": "Pages", "count": 12 }
    },
    "taxonomies": {
      "category": { "label": "Categories", "count": 25 },
      "post_tag": { "label": "Tags", "count": 102 }
    },
    "generated_at": 1700553600,
    "plugin_connection_enabled": true,
    "two_way_sync_enabled": true
  },
  "message": "Site metadata retrieved",
  "request_id": "550e8400-e29b-41d4-a716-446655440000"
}

Developer notes:

  • Admin settings:
    • API key input (option igny8_api_key) added to Settings → IGNY8 API. When provided the key is stored securely and used as the access token by the plugin.
    • Two-way sync toggle added (option igny8_enable_two_way_sync) — default ON.
    • Connection toggle igny8_connection_enabled remains the master on/off switch for sync operations.
  • Tests: Basic unit test added at igny8-wp-integration-plugin/tests/test-site-metadata.php.
  • Security: Webhook secret and REST permission checks remain enforced; unauthenticated REST calls still return unified 401 errors.