sync: v3.2.0 — Supabase fixes, site edit, WP plugin rename, black formatting

- Supabase: fix filters, count_rows, upsert, anon_key optional, meta_url field
- Dashboard: site edit page + PATCH update route
- WP Plugin: rename SEO API Bridge → Airano MCP SEO Bridge
- supabase/client.py: black formatting fix
- .gitignore: exclude BUILD_REPORT.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-03-11 03:21:28 +03:30
parent a806671e2d
commit c0d55f9061
16 changed files with 669 additions and 140 deletions

View File

@@ -0,0 +1,309 @@
# SEO API Bridge - WordPress Plugin
**Version:** 1.3.0
**Requires:** WordPress 5.0+, PHP 7.4+
**License:** GPLv2 or later
## Description
SEO API Bridge is a comprehensive WordPress plugin that exposes Rank Math SEO and Yoast SEO meta fields via dedicated REST API endpoints. This enables MCP servers, AI agents, and other applications to read and write SEO metadata programmatically for posts, pages, and WooCommerce products.
## What's New in 1.3.0
- **Full REST API Endpoints** - GET and POST operations for all post types
- **Product SEO Support** - Complete WooCommerce product SEO management
- **Simplified Integration** - Direct API calls instead of meta field manipulation
- **Better Error Handling** - Proper validation and error responses
## Features
- **Rank Math SEO Support** - Full access to all Rank Math meta fields
- **Yoast SEO Support** - Full access to all Yoast SEO meta fields
- **WooCommerce Compatible** - Works with product post types
- **Secure** - Requires proper authentication and edit_posts capability
- **Auto-Detection** - Automatically detects which SEO plugin is active
- **Health Check Endpoint** - Dedicated API endpoint for plugin detection
- **Zero Configuration** - Works out of the box after activation
## Supported SEO Fields
### Rank Math SEO
| Category | Field | Description |
|----------|-------|-------------|
| Core | `rank_math_focus_keyword` | Focus keyword |
| Core | `rank_math_title` | Meta title |
| Core | `rank_math_description` | Meta description |
| Core | `rank_math_additional_keywords` | Additional keywords |
| Advanced | `rank_math_canonical_url` | Canonical URL |
| Advanced | `rank_math_robots` | Robots meta directives |
| Advanced | `rank_math_breadcrumb_title` | Breadcrumb title |
| Open Graph | `rank_math_facebook_title` | OG title |
| Open Graph | `rank_math_facebook_description` | OG description |
| Open Graph | `rank_math_facebook_image` | OG image URL |
| Open Graph | `rank_math_facebook_image_id` | OG image ID |
| Twitter | `rank_math_twitter_title` | Twitter title |
| Twitter | `rank_math_twitter_description` | Twitter description |
| Twitter | `rank_math_twitter_image` | Twitter image URL |
| Twitter | `rank_math_twitter_image_id` | Twitter image ID |
| Twitter | `rank_math_twitter_card_type` | Card type (summary, summary_large_image) |
### Yoast SEO
| Category | Field | Description |
|----------|-------|-------------|
| Core | `_yoast_wpseo_focuskw` | Focus keyword |
| Core | `_yoast_wpseo_title` | Meta title |
| Core | `_yoast_wpseo_metadesc` | Meta description |
| Advanced | `_yoast_wpseo_canonical` | Canonical URL |
| Advanced | `_yoast_wpseo_meta-robots-noindex` | Noindex setting |
| Advanced | `_yoast_wpseo_meta-robots-nofollow` | Nofollow setting |
| Advanced | `_yoast_wpseo_bctitle` | Breadcrumb title |
| Open Graph | `_yoast_wpseo_opengraph-title` | OG title |
| Open Graph | `_yoast_wpseo_opengraph-description` | OG description |
| Open Graph | `_yoast_wpseo_opengraph-image` | OG image URL |
| Open Graph | `_yoast_wpseo_opengraph-image-id` | OG image ID |
| Twitter | `_yoast_wpseo_twitter-title` | Twitter title |
| Twitter | `_yoast_wpseo_twitter-description` | Twitter description |
| Twitter | `_yoast_wpseo_twitter-image` | Twitter image URL |
| Twitter | `_yoast_wpseo_twitter-image-id` | Twitter image ID |
## Installation
### Method 1: Upload via WordPress Admin
1. Download `seo-api-bridge.zip` from [Releases](https://github.com/airano-ir/mcphub)
2. Go to WordPress Admin > Plugins > Add New > Upload Plugin
3. Upload the ZIP file and click "Install Now"
4. Click "Activate Plugin"
### Method 2: FTP/SSH Upload
1. Upload the `seo-api-bridge` folder to `/wp-content/plugins/`
2. Go to WordPress Admin > Plugins
3. Find "SEO API Bridge" and click "Activate"
### Method 3: WP-CLI
```bash
cd /var/www/html/wp-content/plugins/
# Copy the plugin folder here
wp plugin activate seo-api-bridge
```
## REST API Endpoints
All endpoints require **WordPress Application Password** authentication.
### Status Endpoint
**GET** `/wp-json/seo-api-bridge/v1/status`
```bash
curl -X GET "https://yoursite.com/wp-json/seo-api-bridge/v1/status" \
-u "username:application_password"
```
**Response:**
```json
{
"plugin": "SEO API Bridge",
"version": "1.3.0",
"active": true,
"seo_plugins": {
"rank_math": { "active": true, "version": "1.0.257" },
"yoast": { "active": false, "version": null }
},
"supported_post_types": ["post", "page", "product"],
"message": "SEO API Bridge is active and working with Rank Math SEO."
}
```
### Post SEO Endpoints
**GET** `/wp-json/seo-api-bridge/v1/posts/{id}/seo` — Get SEO metadata for a post
**POST** `/wp-json/seo-api-bridge/v1/posts/{id}/seo` — Update SEO metadata for a post
```bash
# Get post SEO
curl -X GET "https://yoursite.com/wp-json/seo-api-bridge/v1/posts/123/seo" \
-u "username:application_password"
# Update post SEO
curl -X POST "https://yoursite.com/wp-json/seo-api-bridge/v1/posts/123/seo" \
-u "username:application_password" \
-H "Content-Type: application/json" \
-d '{"focus_keyword": "wordpress", "seo_title": "My Title"}'
```
### Page SEO Endpoints
**GET** `/wp-json/seo-api-bridge/v1/pages/{id}/seo`
**POST** `/wp-json/seo-api-bridge/v1/pages/{id}/seo`
### Product SEO Endpoints (WooCommerce)
**GET** `/wp-json/seo-api-bridge/v1/products/{id}/seo`
**POST** `/wp-json/seo-api-bridge/v1/products/{id}/seo`
```bash
curl -X POST "https://yoursite.com/wp-json/seo-api-bridge/v1/products/1217/seo" \
-u "username:application_password" \
-H "Content-Type: application/json" \
-d '{"focus_keyword": "product keyword", "seo_title": "Product Title"}'
```
### Alternative: Standard WordPress REST API
The plugin also registers all SEO meta fields on standard WordPress REST API endpoints. You can read/write SEO data directly via:
- `GET/POST /wp-json/wp/v2/posts/{id}` — SEO fields in the `meta` object
- `GET/POST /wp-json/wp/v2/pages/{id}`
- `GET/POST /wp-json/wp/v2/products/{id}` (WooCommerce)
```bash
# Read SEO fields via standard endpoint
curl -X GET "https://yoursite.com/wp-json/wp/v2/posts/123" \
-u "username:application_password"
# Update SEO via standard endpoint
curl -X POST "https://yoursite.com/wp-json/wp/v2/posts/123" \
-u "username:application_password" \
-H "Content-Type: application/json" \
-d '{"meta": {"rank_math_focus_keyword": "wordpress optimization"}}'
```
## Usage with MCP Hub
This plugin is designed to work with [MCP Hub](https://github.com/airano-ir/mcphub).
```javascript
// Get product SEO
const seo = await mcp.wordpress_get_product_seo({
site: "yoursite",
product_id: 1217
});
// Update product SEO
await mcp.wordpress_update_product_seo({
site: "yoursite",
product_id: 1217,
focus_keyword: "keyword",
seo_title: "Title",
meta_description: "Description"
});
```
```python
# Get post SEO data
result = await mcp.call_tool(
"wordpress_get_post_seo",
{"site": "yoursite", "post_id": 123}
)
# Update post SEO
result = await mcp.call_tool(
"wordpress_update_post_seo",
{
"site": "yoursite",
"post_id": 123,
"focus_keyword": "wordpress seo",
"seo_title": "Complete SEO Guide",
"meta_description": "Learn WordPress SEO..."
}
)
```
## Verification
1. **Admin Notice:** After activation, visit the Plugins page to see a status notice indicating which SEO plugin was detected.
2. **Status Endpoint:**
```bash
curl -X GET "https://your-site.com/wp-json/seo-api-bridge/v1/status" \
-u "username:application_password"
```
3. **REST API Test:**
```bash
curl -X GET "https://your-site.com/wp-json/wp/v2/posts/1" \
-u "username:application_password"
```
4. **MCP Health Check:** The MCP server automatically detects the plugin using the status endpoint.
## Security
- All meta fields require authentication via Application Passwords
- Write access requires `edit_posts` capability
- Read access follows WordPress post visibility rules
- No sensitive data exposed without proper permissions
## Troubleshooting
### MCP Server Cannot Detect Plugin
1. Upgrade to v1.1.0+ (has dedicated health check endpoint)
2. Restart MCP server
3. Test endpoint: `curl "https://your-site.com/wp-json/seo-api-bridge/v1/status" -u "user:pass"`
### SEO Plugin Not Detected
1. Verify Rank Math or Yoast SEO is installed and activated
2. Deactivate and reactivate SEO API Bridge
3. Check PHP error logs for warnings
### Meta Fields Not Appearing in REST API
1. Ensure you're authenticated (use Application Password)
2. Check user has `edit_posts` permission
3. Verify the post type is supported (post, page, product)
## Compatibility
- WordPress 5.0+
- PHP 7.4+
- Rank Math SEO 1.0+
- Yoast SEO 14.0+
- WooCommerce 5.0+ (for product support)
- Classic Editor & Gutenberg
- Multisite compatible
## Changelog
### 1.3.0 (2025-02-18)
- Added REST API endpoints for posts, pages, and products (GET/POST operations)
- Added authentication requirement for all endpoints
- Fixed `rank_math_title` meta key registration
- Fixed output escaping for WordPress.org compliance
### 1.2.0 (2025-02-15)
- Enhanced WooCommerce product support
- Improved MariaDB compatibility for meta field queries
### 1.1.0 (2025-01-10)
- Added health check REST API endpoint `/seo-api-bridge/v1/status`
- Direct plugin detection without requiring posts/products
- Better compatibility with sites that have no content
- Returns SEO plugin versions in status endpoint
### 1.0.0 (2025-01-10)
- Initial release
- Rank Math SEO support (15 meta fields)
- Yoast SEO support (15 meta fields)
- WooCommerce product support
- Auto-detection of active SEO plugins
## Support
- **Plugin issues:** [GitHub Issues](https://github.com/airano-ir/mcphub/issues)
- **MCP Server:** [MCP Hub Documentation](https://github.com/airano-ir/mcphub)
- **Rank Math SEO:** [Rank Math Support](https://rankmath.com/support/)
- **Yoast SEO:** [Yoast Support](https://yoast.com/help/)
## License
GPLv2 or later - See [LICENSE](https://www.gnu.org/licenses/gpl-2.0.html) for details

View File

@@ -0,0 +1,82 @@
=== Airano MCP SEO Meta Bridge ===
Contributors: airano
Tags: seo, rest-api, rank-math, yoast, mcp
Requires at least: 5.0
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 1.3.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Exposes Rank Math SEO and Yoast SEO meta fields via WordPress REST API for use with MCP servers and AI agents.
== Description ==
Airano MCP SEO Bridge is a WordPress plugin that exposes Rank Math SEO and Yoast SEO meta fields via dedicated REST API endpoints. This enables MCP servers, AI agents, and other applications to read and write SEO metadata programmatically for posts, pages, and WooCommerce products.
**Features:**
* **Rank Math SEO Support** — Full access to all Rank Math meta fields
* **Yoast SEO Support** — Full access to all Yoast SEO meta fields
* **WooCommerce Compatible** — Works with product post types
* **Secure** — Requires WordPress Application Password and edit_posts capability
* **Auto-Detection** — Automatically detects which SEO plugin is active
* **Status Endpoint** — Dedicated API endpoint for plugin and SEO detection
* **Zero Configuration** — Works out of the box after activation
**REST API Endpoints:**
* `GET/POST /wp-json/airano-mcp-seo-bridge/v1/posts/{id}/seo` — Post SEO data
* `GET/POST /wp-json/airano-mcp-seo-bridge/v1/pages/{id}/seo` — Page SEO data
* `GET/POST /wp-json/airano-mcp-seo-bridge/v1/products/{id}/seo` — Product SEO data (WooCommerce)
* `GET /wp-json/airano-mcp-seo-bridge/v1/status` — Plugin status and SEO detection
**Designed for [MCP Hub](https://github.com/airano-ir/mcphub)** — the AI-native management hub for WordPress and self-hosted services.
== Installation ==
1. Upload the `airano-mcp-seo-bridge` folder to `/wp-content/plugins/`
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Ensure either Rank Math SEO or Yoast SEO is active
4. The REST API endpoints are now available
== Frequently Asked Questions ==
= Which SEO plugins are supported? =
Rank Math SEO and Yoast SEO. The plugin auto-detects which one is active.
= Does it work with WooCommerce? =
Yes. Product SEO endpoints are available when WooCommerce is active.
= How is authentication handled? =
All endpoints require WordPress Application Password authentication and the `edit_posts` capability. POST requests require the same capability.
= Can I use this without MCP Hub? =
Yes. The REST API endpoints work with any application that can make HTTP requests with proper WordPress authentication.
== Changelog ==
= 1.3.0 =
* Added REST API endpoints for posts, pages, and products (GET/POST operations)
* Added authentication requirement for all endpoints
* Fixed rank_math_title meta key registration
= 1.2.0 =
* Enhanced WooCommerce product support
* Improved MariaDB compatibility
= 1.1.0 =
* Added status endpoint for plugin detection
* Improved error handling
= 1.0.0 =
* Initial release with Rank Math and Yoast SEO support
== Upgrade Notice ==
= 1.3.0 =
GET endpoints now require authentication (edit_posts capability). Update your API clients if needed.

View File

@@ -0,0 +1,715 @@
<?php
/**
* Plugin Name: Airano MCP SEO Meta Bridge
* Plugin URI: https://github.com/airano-ir/mcphub
* Description: Exposes Rank Math SEO and Yoast SEO meta fields via WordPress REST API for use with MCP servers and AI agents. Supports posts, pages, and WooCommerce products with full CRUD operations.
* Version: 1.3.0
* Author: MCP Hub
* Author URI: https://github.com/airano-ir
* License: GPL-2.0-or-later
* Requires at least: 5.0
* Requires PHP: 7.4
* Text Domain: airano-mcp-seo-bridge
*
* Changelog:
* 1.3.0 - Added REST API endpoints for posts, pages, and products (GET/POST operations)
* 1.2.0 - Enhanced WooCommerce product support, improved MariaDB compatibility
* 1.1.0 - Added status endpoint and improved plugin detection
* 1.0.0 - Initial release
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Airano MCP SEO Meta Bridge Main Class
*/
class SEO_API_Bridge {
/**
* Plugin version
*/
const VERSION = '1.3.0';
/**
* Supported post types
*/
private $supported_post_types = ['post', 'page', 'product'];
/**
* Constructor
*/
public function __construct() {
add_action('rest_api_init', [$this, 'register_meta_fields']);
add_action('rest_api_init', [$this, 'register_rest_routes']);
add_action('admin_notices', [$this, 'admin_notices']);
// Add product support message if WooCommerce is active
if ($this->is_woocommerce_active()) {
$this->supported_post_types[] = 'product_variation'; // Also support variations
}
}
/**
* Register REST API routes
*/
public function register_rest_routes() {
// Status endpoint
register_rest_route('airano-mcp-seo-bridge/v1', '/status', [
'methods' => 'GET',
'callback' => [$this, 'get_status'],
'permission_callback' => function() {
return is_user_logged_in();
}
]);
// Post SEO endpoints
register_rest_route('airano-mcp-seo-bridge/v1', '/posts/(?P<id>\d+)/seo', [
[
'methods' => 'GET',
'callback' => [$this, 'get_post_seo'],
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
}
]
]
],
[
'methods' => 'POST',
'callback' => [$this, 'update_post_seo'],
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
}
]
]
]
]);
// Page SEO endpoints
register_rest_route('airano-mcp-seo-bridge/v1', '/pages/(?P<id>\d+)/seo', [
[
'methods' => 'GET',
'callback' => [$this, 'get_page_seo'],
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
}
]
]
],
[
'methods' => 'POST',
'callback' => [$this, 'update_page_seo'],
'permission_callback' => function() {
return current_user_can('edit_pages');
},
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
}
]
]
]
]);
// Product SEO endpoints (WooCommerce)
register_rest_route('airano-mcp-seo-bridge/v1', '/products/(?P<id>\d+)/seo', [
[
'methods' => 'GET',
'callback' => [$this, 'get_product_seo'],
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
}
]
]
],
[
'methods' => 'POST',
'callback' => [$this, 'update_product_seo'],
'permission_callback' => function() {
return current_user_can('edit_posts');
},
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
}
]
]
]
]);
}
/**
* Get plugin status endpoint
*/
public function get_status() {
$rank_math_active = $this->is_rank_math_active();
$yoast_active = $this->is_yoast_active();
$response = [
'plugin' => 'Airano MCP SEO Meta Bridge',
'version' => self::VERSION,
'active' => true,
'seo_plugins' => [
'rank_math' => [
'active' => $rank_math_active,
'version' => $rank_math_active && defined('RANK_MATH_VERSION') ? RANK_MATH_VERSION : null
],
'yoast' => [
'active' => $yoast_active,
'version' => $yoast_active && defined('WPSEO_VERSION') ? WPSEO_VERSION : null
]
],
'supported_post_types' => $this->supported_post_types,
'message' => $this->get_status_message($rank_math_active, $yoast_active)
];
return rest_ensure_response($response);
}
/**
* Get status message based on active plugins
*/
private function get_status_message($rank_math_active, $yoast_active) {
if (!$rank_math_active && !$yoast_active) {
return 'No SEO plugin detected. Please install and activate Rank Math SEO or Yoast SEO.';
}
$active_plugins = [];
if ($rank_math_active) $active_plugins[] = 'Rank Math SEO';
if ($yoast_active) $active_plugins[] = 'Yoast SEO';
return 'Airano MCP SEO Meta Bridge is active and working with ' . implode(' and ', $active_plugins) . '.';
}
/**
* Register all meta fields for REST API
*/
public function register_meta_fields() {
// Register Rank Math fields if plugin is active
if ($this->is_rank_math_active()) {
$this->register_rank_math_fields();
}
// Register Yoast SEO fields if plugin is active
if ($this->is_yoast_active()) {
$this->register_yoast_fields();
}
}
/**
* Check if Rank Math is active
*/
private function is_rank_math_active() {
return defined('RANK_MATH_VERSION') || class_exists('RankMath');
}
/**
* Check if Yoast SEO is active
*/
private function is_yoast_active() {
return defined('WPSEO_VERSION') || class_exists('WPSEO_Options');
}
/**
* Check if WooCommerce is active
*/
private function is_woocommerce_active() {
return class_exists('WooCommerce') || defined('WC_VERSION');
}
/**
* Register Rank Math meta fields
*/
private function register_rank_math_fields() {
$rank_math_fields = [
// Core SEO Fields
'rank_math_focus_keyword' => [
'type' => 'string',
'description' => 'Rank Math focus keyword',
'single' => true,
],
'rank_math_title' => [
'type' => 'string',
'description' => 'Rank Math SEO title (meta title)',
'single' => true,
],
'rank_math_description' => [
'type' => 'string',
'description' => 'Rank Math meta description',
'single' => true,
],
// Additional Keywords
'rank_math_additional_keywords' => [
'type' => 'string',
'description' => 'Rank Math additional keywords (comma-separated)',
'single' => true,
],
// Advanced Fields
'rank_math_canonical_url' => [
'type' => 'string',
'description' => 'Rank Math canonical URL',
'single' => true,
],
'rank_math_robots' => [
'type' => 'array',
'description' => 'Rank Math robots meta (noindex, nofollow, etc.)',
'single' => true,
],
'rank_math_breadcrumb_title' => [
'type' => 'string',
'description' => 'Rank Math breadcrumb title',
'single' => true,
],
// Open Graph Fields
'rank_math_facebook_title' => [
'type' => 'string',
'description' => 'Rank Math Facebook Open Graph title',
'single' => true,
],
'rank_math_facebook_description' => [
'type' => 'string',
'description' => 'Rank Math Facebook Open Graph description',
'single' => true,
],
'rank_math_facebook_image' => [
'type' => 'string',
'description' => 'Rank Math Facebook Open Graph image URL',
'single' => true,
],
'rank_math_facebook_image_id' => [
'type' => 'integer',
'description' => 'Rank Math Facebook Open Graph image ID',
'single' => true,
],
// Twitter Card Fields
'rank_math_twitter_title' => [
'type' => 'string',
'description' => 'Rank Math Twitter Card title',
'single' => true,
],
'rank_math_twitter_description' => [
'type' => 'string',
'description' => 'Rank Math Twitter Card description',
'single' => true,
],
'rank_math_twitter_image' => [
'type' => 'string',
'description' => 'Rank Math Twitter Card image URL',
'single' => true,
],
'rank_math_twitter_image_id' => [
'type' => 'integer',
'description' => 'Rank Math Twitter Card image ID',
'single' => true,
],
'rank_math_twitter_card_type' => [
'type' => 'string',
'description' => 'Rank Math Twitter Card type (summary, summary_large_image)',
'single' => true,
],
];
$this->register_fields($rank_math_fields);
}
/**
* Register Yoast SEO meta fields
*/
private function register_yoast_fields() {
$yoast_fields = [
// Core SEO Fields
'_yoast_wpseo_focuskw' => [
'type' => 'string',
'description' => 'Yoast SEO focus keyword',
'single' => true,
],
'_yoast_wpseo_title' => [
'type' => 'string',
'description' => 'Yoast SEO title (meta title)',
'single' => true,
],
'_yoast_wpseo_metadesc' => [
'type' => 'string',
'description' => 'Yoast SEO meta description',
'single' => true,
],
// Advanced Fields
'_yoast_wpseo_canonical' => [
'type' => 'string',
'description' => 'Yoast SEO canonical URL',
'single' => true,
],
'_yoast_wpseo_meta-robots-noindex' => [
'type' => 'string',
'description' => 'Yoast SEO noindex setting (0 = index, 1 = noindex)',
'single' => true,
],
'_yoast_wpseo_meta-robots-nofollow' => [
'type' => 'string',
'description' => 'Yoast SEO nofollow setting (0 = follow, 1 = nofollow)',
'single' => true,
],
'_yoast_wpseo_bctitle' => [
'type' => 'string',
'description' => 'Yoast SEO breadcrumb title',
'single' => true,
],
// Open Graph Fields
'_yoast_wpseo_opengraph-title' => [
'type' => 'string',
'description' => 'Yoast SEO Open Graph title',
'single' => true,
],
'_yoast_wpseo_opengraph-description' => [
'type' => 'string',
'description' => 'Yoast SEO Open Graph description',
'single' => true,
],
'_yoast_wpseo_opengraph-image' => [
'type' => 'string',
'description' => 'Yoast SEO Open Graph image URL',
'single' => true,
],
'_yoast_wpseo_opengraph-image-id' => [
'type' => 'integer',
'description' => 'Yoast SEO Open Graph image ID',
'single' => true,
],
// Twitter Card Fields
'_yoast_wpseo_twitter-title' => [
'type' => 'string',
'description' => 'Yoast SEO Twitter title',
'single' => true,
],
'_yoast_wpseo_twitter-description' => [
'type' => 'string',
'description' => 'Yoast SEO Twitter description',
'single' => true,
],
'_yoast_wpseo_twitter-image' => [
'type' => 'string',
'description' => 'Yoast SEO Twitter image URL',
'single' => true,
],
'_yoast_wpseo_twitter-image-id' => [
'type' => 'integer',
'description' => 'Yoast SEO Twitter image ID',
'single' => true,
],
];
$this->register_fields($yoast_fields);
}
/**
* Register fields for all supported post types
*/
private function register_fields($fields) {
foreach ($this->supported_post_types as $post_type) {
foreach ($fields as $meta_key => $args) {
$show_in_rest = $args['type'] === 'array'
? [ 'schema' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ] ]
: true;
register_post_meta($post_type, $meta_key, [
'show_in_rest' => $show_in_rest,
'single' => $args['single'],
'type' => $args['type'],
'description' => $args['description'],
'auth_callback' => function() {
return current_user_can('edit_posts');
}
]);
}
}
}
/**
* Get SEO data for a post
*/
public function get_post_seo($request) {
$post_id = $request['id'];
return $this->get_seo_data($post_id, 'post');
}
/**
* Update SEO data for a post
*/
public function update_post_seo($request) {
$post_id = $request['id'];
return $this->update_seo_data($post_id, $request->get_json_params(), 'post');
}
/**
* Get SEO data for a page
*/
public function get_page_seo($request) {
$post_id = $request['id'];
return $this->get_seo_data($post_id, 'page');
}
/**
* Update SEO data for a page
*/
public function update_page_seo($request) {
$post_id = $request['id'];
return $this->update_seo_data($post_id, $request->get_json_params(), 'page');
}
/**
* Get SEO data for a product
*/
public function get_product_seo($request) {
$post_id = $request['id'];
return $this->get_seo_data($post_id, 'product');
}
/**
* Update SEO data for a product
*/
public function update_product_seo($request) {
$post_id = $request['id'];
return $this->update_seo_data($post_id, $request->get_json_params(), 'product');
}
/**
* Generic method to get SEO data
*/
private function get_seo_data($post_id, $post_type) {
// Verify post exists and is correct type
$post = get_post($post_id);
if (!$post || $post->post_type !== $post_type) {
return new WP_Error(
'invalid_post',
sprintf('%s not found', ucfirst($post_type)),
['status' => 404]
);
}
$seo_data = [
'post_id' => $post_id,
'post_type' => $post_type,
'post_title' => $post->post_title,
];
// Detect which SEO plugin is active and get data
if ($this->is_rank_math_active()) {
$seo_data['plugin'] = 'rank_math';
$seo_data = array_merge($seo_data, $this->get_rank_math_data($post_id));
} elseif ($this->is_yoast_active()) {
$seo_data['plugin'] = 'yoast';
$seo_data = array_merge($seo_data, $this->get_yoast_data($post_id));
} else {
return new WP_Error(
'no_seo_plugin',
'No supported SEO plugin found (Rank Math or Yoast required)',
['status' => 500]
);
}
return rest_ensure_response($seo_data);
}
/**
* Get Rank Math SEO data
*/
private function get_rank_math_data($post_id) {
return [
'focus_keyword' => get_post_meta($post_id, 'rank_math_focus_keyword', true),
'seo_title' => get_post_meta($post_id, 'rank_math_title', true),
'meta_description' => get_post_meta($post_id, 'rank_math_description', true),
'canonical_url' => get_post_meta($post_id, 'rank_math_canonical_url', true),
'robots' => get_post_meta($post_id, 'rank_math_robots', true),
'og_title' => get_post_meta($post_id, 'rank_math_facebook_title', true),
'og_description' => get_post_meta($post_id, 'rank_math_facebook_description', true),
'og_image' => get_post_meta($post_id, 'rank_math_facebook_image', true),
'twitter_title' => get_post_meta($post_id, 'rank_math_twitter_title', true),
'twitter_description' => get_post_meta($post_id, 'rank_math_twitter_description', true),
'twitter_image' => get_post_meta($post_id, 'rank_math_twitter_image', true),
];
}
/**
* Get Yoast SEO data
*/
private function get_yoast_data($post_id) {
return [
'focus_keyword' => get_post_meta($post_id, '_yoast_wpseo_focuskw', true),
'seo_title' => get_post_meta($post_id, '_yoast_wpseo_title', true),
'meta_description' => get_post_meta($post_id, '_yoast_wpseo_metadesc', true),
'canonical_url' => get_post_meta($post_id, '_yoast_wpseo_canonical', true),
'noindex' => get_post_meta($post_id, '_yoast_wpseo_meta-robots-noindex', true),
'nofollow' => get_post_meta($post_id, '_yoast_wpseo_meta-robots-nofollow', true),
'og_title' => get_post_meta($post_id, '_yoast_wpseo_opengraph-title', true),
'og_description' => get_post_meta($post_id, '_yoast_wpseo_opengraph-description', true),
'og_image' => get_post_meta($post_id, '_yoast_wpseo_opengraph-image', true),
'twitter_title' => get_post_meta($post_id, '_yoast_wpseo_twitter-title', true),
'twitter_description' => get_post_meta($post_id, '_yoast_wpseo_twitter-description', true),
'twitter_image' => get_post_meta($post_id, '_yoast_wpseo_twitter-image', true),
];
}
/**
* Generic method to update SEO data
*/
private function update_seo_data($post_id, $params, $post_type) {
// Verify post exists and is correct type
$post = get_post($post_id);
if (!$post || $post->post_type !== $post_type) {
return new WP_Error(
'invalid_post',
sprintf('%s not found', ucfirst($post_type)),
['status' => 404]
);
}
$updated_fields = [];
// Update based on active SEO plugin
if ($this->is_rank_math_active()) {
$updated_fields = $this->update_rank_math_data($post_id, $params);
} elseif ($this->is_yoast_active()) {
$updated_fields = $this->update_yoast_data($post_id, $params);
} else {
return new WP_Error(
'no_seo_plugin',
'No supported SEO plugin found',
['status' => 500]
);
}
return rest_ensure_response([
'post_id' => $post_id,
'post_type' => $post_type,
'updated_fields' => $updated_fields,
'message' => 'SEO metadata updated successfully'
]);
}
/**
* Update Rank Math data
*/
private function update_rank_math_data($post_id, $params) {
$updated = [];
$field_map = [
'focus_keyword' => 'rank_math_focus_keyword',
'seo_title' => 'rank_math_title',
'meta_description' => 'rank_math_description',
'canonical_url' => 'rank_math_canonical_url',
'robots' => 'rank_math_robots',
'og_title' => 'rank_math_facebook_title',
'og_description' => 'rank_math_facebook_description',
'og_image' => 'rank_math_facebook_image',
'twitter_title' => 'rank_math_twitter_title',
'twitter_description' => 'rank_math_twitter_description',
'twitter_image' => 'rank_math_twitter_image',
];
foreach ($field_map as $param_key => $meta_key) {
if (isset($params[$param_key])) {
update_post_meta($post_id, $meta_key, $params[$param_key]);
$updated[] = $meta_key;
}
}
return $updated;
}
/**
* Update Yoast data
*/
private function update_yoast_data($post_id, $params) {
$updated = [];
$field_map = [
'focus_keyword' => '_yoast_wpseo_focuskw',
'seo_title' => '_yoast_wpseo_title',
'meta_description' => '_yoast_wpseo_metadesc',
'canonical_url' => '_yoast_wpseo_canonical',
'og_title' => '_yoast_wpseo_opengraph-title',
'og_description' => '_yoast_wpseo_opengraph-description',
'og_image' => '_yoast_wpseo_opengraph-image',
'twitter_title' => '_yoast_wpseo_twitter-title',
'twitter_description' => '_yoast_wpseo_twitter-description',
'twitter_image' => '_yoast_wpseo_twitter-image',
];
foreach ($field_map as $param_key => $meta_key) {
if (isset($params[$param_key])) {
update_post_meta($post_id, $meta_key, $params[$param_key]);
$updated[] = $meta_key;
}
}
return $updated;
}
/**
* Display admin notices
*/
public function admin_notices() {
// Only show notices on the Plugins page to avoid clutter on every admin page
$screen = get_current_screen();
if ( ! $screen || $screen->id !== 'plugins' ) {
return;
}
$rank_math_active = $this->is_rank_math_active();
$yoast_active = $this->is_yoast_active();
$woocommerce_active = $this->is_woocommerce_active();
if (!$rank_math_active && !$yoast_active) {
echo '<div class="notice notice-warning is-dismissible">';
echo '<p><strong>Airano MCP SEO Meta Bridge:</strong> ' . esc_html__( 'Neither Rank Math SEO nor Yoast SEO is detected. Please install and activate one of these plugins to enable SEO meta field access via REST API.', 'airano-mcp-seo-bridge' ) . '</p>';
echo '</div>';
} else {
$active_plugins = [];
if ($rank_math_active) $active_plugins[] = 'Rank Math SEO';
if ($yoast_active) $active_plugins[] = 'Yoast SEO';
$supported_types = implode(', ', $this->supported_post_types);
echo '<div class="notice notice-success is-dismissible">';
echo '<p><strong>Airano MCP SEO Meta Bridge v' . esc_html( self::VERSION ) . ':</strong> ' . esc_html( sprintf( 'Successfully registered meta fields for %s.', implode( ' and ', $active_plugins ) ) ) . '</p>';
echo '<p><strong>' . esc_html__( 'Supported post types:', 'airano-mcp-seo-bridge' ) . '</strong> ' . esc_html( $supported_types ) . '</p>';
if ($woocommerce_active) {
echo '<p><strong>WooCommerce:</strong> ' . esc_html__( 'Detected and supported. Product SEO fields are available via REST API.', 'airano-mcp-seo-bridge' ) . '</p>';
}
echo '</div>';
}
}
}
// Initialize the plugin
new SEO_API_Bridge();