Settings fixes: - MAX_SITES_PER_USER, USER_RATE_LIMIT_PER_MIN/HR now read from DB settings table (DB > ENV > default), so dashboard/settings changes apply without restart. Sync cache refreshed on every save or delete. - /api/me reports the live DB value for max_sites_per_user. Admin improvements: - Admin users bypass per-user rate limiting entirely (role=admin or ADMIN_EMAILS). - Admin Overview now shows platform stats: registered users, new users (7d), total user sites, available tools. Plugin cleanup: - Appwrite and Directus plugins removed from the active registry (8 plugins now: WordPress, WooCommerce, WordPress Specialist, Gitea, n8n, Supabase, OpenPanel, Coolify). Plugin code is retained for future re-enabling. - Settings page plugin visibility list updated to match. Mobile onboarding: - Stepper steps on narrow viewports stack vertically with correct full border and rounded corners on each step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
795 lines
29 KiB
PHP
795 lines
29 KiB
PHP
<?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.1
|
|
* 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.1 - Migration notice — this plugin has been superseded by "Airano MCP Bridge" (slug `airano-mcp-bridge`), which carries the same SEO routes plus media uploads, bulk meta, cache purge, transient flush, site health, audit hook, and thumbnail regeneration. All SEO functionality in this plugin is unchanged; existing installs keep working. A dismissible admin notice points users to the replacement.
|
|
* 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.1';
|
|
|
|
/**
|
|
* User-meta key tracking whether the current user dismissed the
|
|
* migration notice that points to the replacement plugin.
|
|
*/
|
|
const MIGRATION_DISMISS_META = 'airano_seo_bridge_migration_dismissed';
|
|
|
|
/**
|
|
* 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']);
|
|
|
|
// 1.3.1 — surface the migration notice across the admin and
|
|
// honour the dismissal click before notices render.
|
|
add_action('admin_init', [$this, 'handle_migration_notice_dismiss']);
|
|
add_action('admin_notices', [$this, 'render_migration_notice']);
|
|
|
|
// 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>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 1.3.1 — Handle the per-user dismissal click for the migration
|
|
* notice. Triggered before notices render so the dismissed state
|
|
* is reflected on the same page load (no double-render after
|
|
* redirect).
|
|
*/
|
|
public function handle_migration_notice_dismiss() {
|
|
if ( ! isset( $_GET['airano_seo_bridge_dismiss_migration'] ) ) {
|
|
return;
|
|
}
|
|
if ( ! is_user_logged_in() ) {
|
|
return;
|
|
}
|
|
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
|
|
if ( ! wp_verify_nonce( $nonce, 'airano_seo_bridge_dismiss_migration' ) ) {
|
|
return;
|
|
}
|
|
update_user_meta( get_current_user_id(), self::MIGRATION_DISMISS_META, 1 );
|
|
// Redirect back to a clean URL so the query parameter doesn't
|
|
// linger in the address bar or history.
|
|
$redirect = remove_query_arg( [ 'airano_seo_bridge_dismiss_migration', '_wpnonce' ] );
|
|
if ( $redirect ) {
|
|
wp_safe_redirect( $redirect );
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 1.3.1 — Persistent migration notice that points users to the
|
|
* replacement plugin "Airano MCP Bridge" (slug `airano-mcp-bridge`).
|
|
* Shown on every admin page so users don't miss it; permanently
|
|
* dismissible per-user via a nonce-protected query param.
|
|
*/
|
|
public function render_migration_notice() {
|
|
if ( ! current_user_can( 'install_plugins' ) ) {
|
|
return;
|
|
}
|
|
if ( get_user_meta( get_current_user_id(), self::MIGRATION_DISMISS_META, true ) ) {
|
|
return;
|
|
}
|
|
$install_url = wp_nonce_url(
|
|
self_admin_url( 'plugin-install.php?s=airano-mcp-bridge&tab=search&type=term' ),
|
|
'airano_seo_bridge_open_install'
|
|
);
|
|
$dismiss_url = wp_nonce_url(
|
|
add_query_arg( 'airano_seo_bridge_dismiss_migration', '1' ),
|
|
'airano_seo_bridge_dismiss_migration'
|
|
);
|
|
$msg = sprintf(
|
|
/* translators: 1: replacement plugin name, 2: installed plugin name. */
|
|
esc_html__( '%1$s supersedes %2$s — same SEO routes, plus media uploads, bulk meta, cache purge, transient flush, site health, audit hook, and thumbnail regeneration. Existing SEO functionality keeps working until you migrate.', 'airano-mcp-seo-bridge' ),
|
|
'<strong>Airano MCP Bridge</strong>',
|
|
'<strong>Airano MCP SEO Meta Bridge</strong>'
|
|
);
|
|
?>
|
|
<div class="notice notice-info" style="border-left-color:#51b9f4;">
|
|
<p style="margin-bottom:8px;"><?php echo wp_kses_post( $msg ); ?></p>
|
|
<p style="margin-top:0;">
|
|
<a href="<?php echo esc_url( $install_url ); ?>" class="button button-primary"><?php esc_html_e( 'Install Airano MCP Bridge', 'airano-mcp-seo-bridge' ); ?></a>
|
|
|
|
<a href="<?php echo esc_url( $dismiss_url ); ?>"><?php esc_html_e( 'Dismiss', 'airano-mcp-seo-bridge' ); ?></a>
|
|
</p>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
}
|
|
|
|
// Initialize the plugin
|
|
new SEO_API_Bridge();
|