Files
igny8/igny8-wp-plugin/docs/WORDPRESS-PLUGIN-INTEGRATION.md
2025-11-30 00:54:44 +05:00

2136 lines
60 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```php
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
```php
function igny8_get_headers() {
$token = get_option('igny8_access_token');
if (!$token) {
return false;
}
return [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json'
];
}
```
Note (required): The bridge now requires all three credentials to be provided in Settings → IGNY8 API: **Email**, **Password**, and **API Key**. These map to WordPress options `igny8_email`, `igny8_access_token`/`igny8_refresh_token`, and `igny8_api_key`. The API key will be stored with `igny8_store_secure_option()` when available; if any required credential is missing the plugin will not establish the connection.
---
## API Client Class
### Complete PHP Implementation
```php
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
```php
$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
```php
$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
```php
$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
```php
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
```php
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
```php
// 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
```php
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
```php
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
```php
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
```php
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');
```
#### Recommended Controls
- **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
```php
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:
```php
/**
* 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:
```php
/**
* 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:
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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)
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
/**
* 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
```php
[
'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
```php
[
'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
```php
[
'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:
```json
{
"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.