Initial commit: MCP Hub Community Edition v3.0.0

Community edition generated from private repo via sync pipeline.
Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n,
Supabase, OpenPanel, Appwrite, Directus) with ~587 tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-17 08:34:44 +03:30
commit cf62e65c55
237 changed files with 87596 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,7 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
(Full text available at https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt)

View File

@@ -0,0 +1,2 @@
<?php
// Silence is golden.

View File

@@ -0,0 +1,560 @@
<?php
/**
* Plugin Name: OpenPanel
* Description: Activate OpenPanel to start tracking your website. Supports both Cloud and Self-Hosted instances.
* Version: 1.1.1
* Author: OpenPanel / Airano
* License: GPLv2 or later
* Requires at least: 5.8
* Requires PHP: 7.4
* Tested up to: 6.8
* Text Domain: openpanel
*/
if (!defined('ABSPATH')) { exit; }
final class OP_WP_Proxy {
const VERSION = '1.1.1';
const OPTION_KEY = 'op_wp_proxy_settings';
const TRANSIENT_JS = 'op_wp_op1_js';
const REST_NS = 'openpanel';
const REST_ROUTE = '/(?P<path>.*)'; // wildcard path passthrough
const CACHE_TIMEOUT = WEEK_IN_SECONDS;
// Cloud defaults
const CLOUD_JS_URL = 'https://openpanel.dev/op1.js';
const CLOUD_API_BASE = 'https://api.openpanel.dev/';
public function __construct() {
add_action('admin_init', [$this, 'register_settings']);
add_action('admin_menu', [$this, 'add_settings_page']);
add_action('wp_enqueue_scripts', [$this, 'inject_inline_sdk'], 0);
add_action('rest_api_init', [$this, 'register_proxy_route']);
add_action('admin_init', [$this, 'handle_cache_clear']);
}
/** ---------------- Settings ---------------- */
public function register_settings() {
register_setting(self::OPTION_KEY, self::OPTION_KEY, [
'type' => 'array',
'show_in_rest' => false,
'sanitize_callback' => function($input) {
$out = [];
$out['hosting_mode'] = isset($input['hosting_mode']) && $input['hosting_mode'] === 'selfhosted' ? 'selfhosted' : 'cloud';
$out['client_id'] = isset($input['client_id']) ? sanitize_text_field($input['client_id']) : '';
$out['api_url'] = isset($input['api_url']) ? esc_url_raw(untrailingslashit($input['api_url'])) : '';
$out['dashboard_url'] = isset($input['dashboard_url']) ? esc_url_raw(untrailingslashit($input['dashboard_url'])) : '';
$out['track_screen'] = !empty($input['track_screen']) ? 1 : 0;
$out['track_outgoing'] = !empty($input['track_outgoing']) ? 1 : 0;
$out['track_attributes'] = !empty($input['track_attributes']) ? 1 : 0;
return $out;
}
]);
// Section: Hosting Mode
add_settings_section('op_hosting', __('Hosting Mode', 'openpanel'), function() {
echo '<p>' . esc_html__('Choose between OpenPanel Cloud or your own Self-Hosted instance.', 'openpanel') . '</p>';
}, self::OPTION_KEY);
add_settings_field('hosting_mode', __('Mode', 'openpanel'), function() {
$opts = get_option(self::OPTION_KEY);
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
?>
<label>
<input type="radio" name="<?php echo esc_attr(self::OPTION_KEY); ?>[hosting_mode]" value="cloud" <?php checked($mode, 'cloud'); ?> class="op-hosting-mode">
<?php esc_html_e('Cloud (openpanel.dev)', 'openpanel'); ?>
</label><br>
<label>
<input type="radio" name="<?php echo esc_attr(self::OPTION_KEY); ?>[hosting_mode]" value="selfhosted" <?php checked($mode, 'selfhosted'); ?> class="op-hosting-mode">
<?php esc_html_e('Self-Hosted', 'openpanel'); ?>
</label>
<p class="description"><?php esc_html_e('Select Self-Hosted if you run your own OpenPanel instance.', 'openpanel'); ?></p>
<?php
}, self::OPTION_KEY, 'op_hosting');
// Section: Self-Hosted URLs
add_settings_section('op_selfhosted', __('Self-Hosted Settings', 'openpanel'), function() {
echo '<p>' . esc_html__('Configure your Self-Hosted OpenPanel URLs. Only required if using Self-Hosted mode.', 'openpanel') . '</p>';
}, self::OPTION_KEY);
add_settings_field('api_url', __('API URL', 'openpanel'), function() {
$opts = get_option(self::OPTION_KEY);
printf('<input type="url" name="%s[api_url]" value="%s" class="regular-text op-selfhosted-field" placeholder="https://api.openpanel.yourdomain.com"/>',
esc_attr(self::OPTION_KEY),
isset($opts['api_url']) ? esc_attr($opts['api_url']) : ''
);
echo '<p class="description">' . esc_html__('Your OpenPanel API endpoint (e.g., https://api.openpanel.yourdomain.com)', 'openpanel') . '</p>';
}, self::OPTION_KEY, 'op_selfhosted');
add_settings_field('dashboard_url', __('Dashboard URL', 'openpanel'), function() {
$opts = get_option(self::OPTION_KEY);
printf('<input type="url" name="%s[dashboard_url]" value="%s" class="regular-text op-selfhosted-field" placeholder="https://openpanel.yourdomain.com"/>',
esc_attr(self::OPTION_KEY),
isset($opts['dashboard_url']) ? esc_attr($opts['dashboard_url']) : ''
);
echo '<p class="description">' . esc_html__('Your OpenPanel Dashboard URL — used to load op1.js from your server instead of the CDN (e.g., https://openpanel.yourdomain.com)', 'openpanel') . '</p>';
}, self::OPTION_KEY, 'op_selfhosted');
// Section: Main Settings
add_settings_section('op_main', __('OpenPanel Settings', 'openpanel'), function() {
echo '<p>' . esc_html__('Set your OpenPanel Client ID. The SDK and requests are served from your domain to avoid ad blockers.', 'openpanel') . '</p>';
}, self::OPTION_KEY);
add_settings_field('client_id', __('Client ID', 'openpanel'), function() {
$opts = get_option(self::OPTION_KEY);
printf('<input type="text" name="%s[client_id]" value="%s" class="regular-text" placeholder="op_client_..."/>',
esc_attr(self::OPTION_KEY),
isset($opts['client_id']) ? esc_attr($opts['client_id']) : ''
);
}, self::OPTION_KEY, 'op_main');
add_settings_field('toggles', __('Auto-tracking (optional)', 'openpanel'), function() {
$o = get_option(self::OPTION_KEY);
// Default track_screen to true if not set
$track_screen = isset($o['track_screen']) ? !empty($o['track_screen']) : true;
?>
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_screen]" <?php checked($track_screen); ?>> <?php esc_html_e('Track page views automatically', 'openpanel'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_outgoing]" <?php checked(!empty($o['track_outgoing'])); ?>> <?php esc_html_e('Track clicks on outgoing links', 'openpanel'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr(self::OPTION_KEY); ?>[track_attributes]" <?php checked(!empty($o['track_attributes'])); ?>> <?php esc_html_e('Track additional page attributes', 'openpanel'); ?></label>
<?php
}, self::OPTION_KEY, 'op_main');
}
public function add_settings_page() {
add_options_page(
'OpenPanel',
'OpenPanel',
'manage_options',
'op-wp-proxy',
[$this, 'render_settings_page']
);
}
public function handle_cache_clear() {
if (isset($_POST['op_clear_cache']) && current_user_can('manage_options')) {
if (isset($_POST['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'op_clear_cache_nonce')) {
delete_transient(self::TRANSIENT_JS);
add_action('admin_notices', function() {
echo '<div class="notice notice-success is-dismissible"><p>' .
esc_html__('OpenPanel cache cleared successfully. The latest op1.js will be fetched on the next page load.', 'openpanel') .
'</p></div>';
});
}
}
}
public function render_settings_page() {
$opts = get_option(self::OPTION_KEY);
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
?>
<div class="wrap">
<h1>OpenPanel</h1>
<form method="post" action="options.php">
<?php
settings_fields(self::OPTION_KEY);
do_settings_sections(self::OPTION_KEY);
submit_button(__('Save Settings', 'openpanel'));
?>
</form>
<hr style="margin: 2rem 0;">
<h2><?php esc_html_e('Cache Management', 'openpanel'); ?></h2>
<p><?php esc_html_e('Clear the cached op1.js file to force fetch the latest version from OpenPanel.', 'openpanel'); ?></p>
<form method="post" action="">
<?php wp_nonce_field('op_clear_cache_nonce'); ?>
<input type="submit" name="op_clear_cache" class="button button-secondary" value="<?php esc_attr_e('Clear Cache & Force Refresh', 'openpanel'); ?>">
</form>
<?php
// Check if transient exists and get expiration info
$cached_js = get_transient(self::TRANSIENT_JS);
$timeout_option = '_transient_timeout_' . self::TRANSIENT_JS;
$cached_time = get_option($timeout_option);
if ($cached_js !== false && $cached_time) {
$time_remaining = $cached_time - time();
if ($time_remaining > 0) {
echo '<p style="margin-top:1rem;color:#666;">' .
/* translators: %s: human readable time difference */
sprintf(esc_html__('Cache expires in %s', 'openpanel'), esc_html(human_time_diff(time(), $cached_time))) .
'</p>';
} else {
echo '<p style="margin-top:1rem;color:#666;">' .
esc_html__('Cache has expired and will refresh on next page load.', 'openpanel') .
'</p>';
}
} else {
echo '<p style="margin-top:1rem;color:#666;">' .
esc_html__('No cached version found. op1.js will be fetched on next page load.', 'openpanel') .
'</p>';
}
?>
<p style="margin-top:1rem;color:#666;">
<?php esc_html_e('The plugin fetches and inlines op1.js (cached for 1 week). If fetching fails, it falls back to the CDN script.', 'openpanel'); ?>
</p>
<hr style="margin: 2rem 0;">
<h2><?php esc_html_e('Current Configuration', 'openpanel'); ?></h2>
<table class="widefat" style="max-width: 600px;">
<tr>
<td><strong><?php esc_html_e('Mode', 'openpanel'); ?></strong></td>
<td><?php echo esc_html($mode === 'selfhosted' ? 'Self-Hosted' : 'Cloud'); ?></td>
</tr>
<tr>
<td><strong><?php esc_html_e('API URL', 'openpanel'); ?></strong></td>
<td><code><?php echo esc_html($this->get_api_base()); ?></code></td>
</tr>
<tr>
<td><strong><?php esc_html_e('JS URL', 'openpanel'); ?></strong></td>
<td><code><?php echo esc_html($this->get_js_url()); ?></code></td>
</tr>
<tr>
<td><strong><?php esc_html_e('Proxy Endpoint', 'openpanel'); ?></strong></td>
<td><code><?php echo esc_html(rest_url(self::REST_NS . '/')); ?></code></td>
</tr>
</table>
</div>
<style>
.op-selfhosted-section { transition: opacity 0.3s; }
.op-selfhosted-section.hidden { opacity: 0.5; pointer-events: none; }
</style>
<script>
jQuery(document).ready(function($) {
function toggleSelfHostedFields() {
var mode = $('input[name="<?php echo esc_js(self::OPTION_KEY); ?>[hosting_mode]"]:checked').val();
var $selfHostedFields = $('.op-selfhosted-field').closest('tr');
var $selfHostedSection = $selfHostedFields.closest('table');
if (mode === 'selfhosted') {
$selfHostedFields.show();
} else {
$selfHostedFields.hide();
}
}
toggleSelfHostedFields();
$('input[name="<?php echo esc_js(self::OPTION_KEY); ?>[hosting_mode]"]').on('change', toggleSelfHostedFields);
});
</script>
<?php
}
/** ---------------- Helper Methods ---------------- */
/**
* Get the API base URL based on hosting mode
*/
private function get_api_base() {
$opts = get_option(self::OPTION_KEY);
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
if ($mode === 'selfhosted' && !empty($opts['api_url'])) {
return trailingslashit($opts['api_url']);
}
return self::CLOUD_API_BASE;
}
/**
* Get the JS URL based on hosting mode
* Cloud: loads from official CDN (openpanel.dev)
* Self-hosted: loads from dashboard URL if configured, otherwise falls back to CDN
*/
private function get_js_url() {
$opts = get_option(self::OPTION_KEY);
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
if ($mode === 'selfhosted' && !empty($opts['dashboard_url'])) {
return trailingslashit($opts['dashboard_url']) . 'op1.js';
}
return self::CLOUD_JS_URL;
}
/**
* Get allowed hosts for proxy validation
*/
private function get_allowed_hosts() {
$hosts = [
'api.openpanel.dev',
'openpanel.dev'
];
$opts = get_option(self::OPTION_KEY);
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
if ($mode === 'selfhosted') {
if (!empty($opts['api_url'])) {
$parsed = wp_parse_url($opts['api_url']);
if (isset($parsed['host'])) {
$hosts[] = $parsed['host'];
}
}
if (!empty($opts['dashboard_url'])) {
$parsed = wp_parse_url($opts['dashboard_url']);
if (isset($parsed['host'])) {
$hosts[] = $parsed['host'];
}
}
}
return array_unique($hosts);
}
/** ---------------- Inline SDK ---------------- */
public function inject_inline_sdk() {
if (is_admin()) return;
$opts = get_option(self::OPTION_KEY);
$clientId = isset($opts['client_id']) ? trim($opts['client_id']) : '';
if ($clientId === '') return; // don't inject if not configured
// For self-hosted, also require API URL to be configured
$mode = isset($opts['hosting_mode']) ? $opts['hosting_mode'] : 'cloud';
if ($mode === 'selfhosted' && empty($opts['api_url'])) {
return; // don't inject if self-hosted but not configured
}
$apiUrl = untrailingslashit( rest_url(self::REST_NS . '/') );
$jsUrl = $this->get_js_url();
$init = [
'clientId' => $clientId,
'apiUrl' => $apiUrl,
'trackScreenViews' => isset($opts['track_screen']) ? !empty($opts['track_screen']) : true,
'trackOutgoingLinks' => !empty($opts['track_outgoing']),
'trackAttributes' => !empty($opts['track_attributes']),
];
$bootstrap = "(function(){window.op=window.op||function(){(window.op.q=window.op.q||[]).push(arguments)};window.op('init'," . wp_json_encode($init) . ");})();";
$op_js = get_transient(self::TRANSIENT_JS);
if ($op_js === false) {
$res = wp_remote_get($jsUrl, ['timeout' => 8]);
if (!is_wp_error($res) && 200 === wp_remote_retrieve_response_code($res)) {
$op_js = wp_remote_retrieve_body($res);
set_transient(self::TRANSIENT_JS, $op_js, self::CACHE_TIMEOUT);
}
}
wp_register_script('op-inline-stub', false, [], self::VERSION, true);
wp_enqueue_script('op-inline-stub');
wp_add_inline_script('op-inline-stub', $bootstrap, 'before');
if (!empty($op_js)) {
// Validate cached JavaScript content before output
if ($this->is_valid_javascript_content($op_js)) {
wp_add_inline_script('op-inline-stub', $op_js, 'after');
} else {
// Fall back to external script if cached content appears invalid or unsafe
wp_enqueue_script('openpanel-op1', $jsUrl, [], self::VERSION, true);
}
} else {
wp_enqueue_script('openpanel-op1', $jsUrl, [], self::VERSION, true);
}
}
/** ---------------- Proxy Route ---------------- */
public function register_proxy_route() {
register_rest_route(self::REST_NS, self::REST_ROUTE, [
'methods' => \WP_REST_Server::ALLMETHODS,
// INTENTIONALLY PUBLIC ENDPOINT: This endpoint must be publicly accessible to receive
// analytics data from frontend JavaScript running in users' browsers. No authentication
// is required as this acts as a proxy for OpenPanel analytics collection.
//
// Security measures in place:
// 1. Only proxies to whitelisted OpenPanel API endpoints (is_valid_proxy_target)
// 2. All input data is sanitized and validated before forwarding
// 3. Proper CORS headers are set for same-origin requests only
// 4. No sensitive WordPress data is exposed through this endpoint
'permission_callback' => '__return_true',
'callback' => [$this, 'proxy_request'],
'args' => [
'path' => [
'description' => 'Remaining path to forward',
'required' => false,
],
],
]);
}
public function proxy_request(\WP_REST_Request $request) {
try {
// Handle CORS preflight quickly
if (strtoupper($request->get_method()) === 'OPTIONS') {
$resp = new \WP_REST_Response(null, 204);
$this->add_cors_headers($resp);
return $resp;
}
$path = ltrim($request->get_param('path') ?? '', '/');
$api_base = $this->get_api_base();
$target = rtrim($api_base, '/') . '/' . $path;
// Security: Ensure we only proxy to OpenPanel API endpoints
if (!$this->is_valid_proxy_target($target)) {
$resp = new \WP_REST_Response(['error' => 'invalid_target', 'message' => 'Invalid proxy target'], 403);
$this->add_cors_headers($resp);
return $resp;
}
$method = strtoupper($request->get_method());
$body = $request->get_body();
$incoming = $this->collect_request_headers();
$query = $request->get_query_params();
if (!empty($query)) {
$target = add_query_arg($query, $target);
}
$args = [
'method' => $method,
'timeout' => 10,
'headers' => $incoming,
'body' => in_array($method, ['POST','PUT','PATCH','DELETE'], true) ? $body : null,
];
$res = wp_remote_request($target, $args);
if (is_wp_error($res)) {
$resp = new \WP_REST_Response(['error' => 'proxy_failed', 'message' => $res->get_error_message()], 502);
$this->add_cors_headers($resp);
$resp->header('Cache-Control', 'no-store');
return $resp;
}
$code = wp_remote_retrieve_response_code($res);
$bodyOut = wp_remote_retrieve_body($res);
// Handle empty response (common for tracking endpoints returning 202)
if (empty($bodyOut)) {
$resp = new \WP_REST_Response(['success' => true], $code ?: 202);
$resp->header('Content-Type', 'application/json; charset=utf-8');
$resp->header('Cache-Control', 'no-store');
$this->add_cors_headers($resp);
return $resp;
}
// Try to decode JSON response, otherwise use raw body
$decoded = json_decode($bodyOut, true);
$responseData = (json_last_error() === JSON_ERROR_NONE && $decoded !== null) ? $decoded : ['raw' => $bodyOut];
$resp = new \WP_REST_Response($responseData, $code);
// Set Content-Type header (skip copying other headers to avoid compatibility issues)
$resp->header('Content-Type', 'application/json; charset=utf-8');
$resp->header('Cache-Control', 'no-store');
$this->add_cors_headers($resp);
return $resp;
} catch (\Exception $e) {
$resp = new \WP_REST_Response(['error' => 'exception', 'message' => $e->getMessage()], 500);
$this->add_cors_headers($resp);
return $resp;
} catch (\Error $e) {
$resp = new \WP_REST_Response(['error' => 'error', 'message' => $e->getMessage()], 500);
$this->add_cors_headers($resp);
return $resp;
}
}
private function add_cors_headers(\WP_REST_Response $resp) {
$origin = get_site_url();
$resp->header('Access-Control-Allow-Origin', $origin);
$resp->header('Access-Control-Allow-Credentials', 'true');
$resp->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
$resp->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
$resp->header('Vary', 'Origin');
}
private function collect_request_headers() {
$headers = [];
// Content-Type is a special header NOT prefixed with HTTP_ in PHP
// This is critical for OpenPanel API which requires application/json
if (!empty($_SERVER['CONTENT_TYPE'])) {
$headers['Content-Type'] = sanitize_text_field($_SERVER['CONTENT_TYPE']);
}
foreach ($_SERVER as $name => $value) {
// Sanitize the header name and ensure it starts with HTTP_
$name = sanitize_text_field($name);
if (strpos($name, 'HTTP_') === 0) {
// Extract and sanitize the header suffix
$header_suffix = substr($name, 5);
$header_suffix = sanitize_text_field($header_suffix);
$header = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $header_suffix))));
// remove hop-by-hop headers and Origin (we'll set it ourselves)
$lk = strtolower($header);
if (in_array($lk, ['host','content-length','accept-encoding','connection','keep-alive','transfer-encoding','upgrade','via','origin'], true)) {
continue;
}
// Sanitize the header value
$headers[$header] = sanitize_text_field($value);
}
}
if (!isset($headers['User-Agent'])) {
$headers['User-Agent'] = 'OpenPanel-WP-Proxy';
}
// Set Origin header to WordPress site URL (required for OpenPanel CORS)
$headers['Origin'] = get_site_url();
return $headers;
}
private function is_valid_proxy_target($target) {
$allowed_hosts = $this->get_allowed_hosts();
$parsed = wp_parse_url($target);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
return in_array($parsed['host'], $allowed_hosts, true) &&
(empty($parsed['scheme']) || in_array($parsed['scheme'], ['https', 'http'], true));
}
private function is_valid_javascript_content($js_content) {
// Comprehensive validation to ensure the content is safe JavaScript
if (empty($js_content) || !is_string($js_content)) {
return false;
}
// Note: We don't modify $js_content with wp_kses here because we need to preserve
// the original JavaScript content for validation. wp_add_inline_script() handles
// proper escaping when outputting to the page.
// Ensure it doesn't contain HTML tags or script injection attempts
if (preg_match('/<(?:script|iframe|object|embed|form|input|meta|link)/i', $js_content)) {
return false;
}
// Check for potential XSS patterns
if (preg_match('/(?:javascript:|data:|vbscript:|on\w+\s*=)/i', $js_content)) {
return false;
}
// Check that it's a reasonable size for a JavaScript file (typical op1.js is ~5KB)
$trimmed = trim($js_content);
if (strlen($trimmed) < 10 || strlen($trimmed) > 20480) { // Max 20KB
return false;
}
// Should contain typical JavaScript patterns (works for both minified and unminified)
return (strpos($trimmed, 'function') !== false ||
strpos($trimmed, 'var ') !== false ||
strpos($trimmed, 'let ') !== false ||
strpos($trimmed, 'const ') !== false ||
strpos($trimmed, '=>') !== false ||
// Handle minified code patterns like the actual op1.js
preg_match('/[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*function/', $trimmed) ||
preg_match('/\(\s*function\s*\(/', $trimmed));
}
}
new OP_WP_Proxy();

View File

@@ -0,0 +1,214 @@
=== OpenPanel ===
Contributors: openpanel, airano
Tags: analytics, web analytics, privacy-friendly, tracking, proxy, self-hosted
Requires at least: 5.8
Tested up to: 6.8
Requires PHP: 7.4
Stable tag: 1.1.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
OpenPanel WordPress plugin - Privacy-friendly analytics with ad-blocker resistance. Supports both OpenPanel Cloud and Self-Hosted instances. Inline tracking scripts and proxy API calls through your domain.
== Description ==
**OpenPanel** is an open-source web and product analytics platform that serves as a privacy-friendly alternative to traditional analytics solutions. This WordPress plugin seamlessly integrates [OpenPanel](https://openpanel.dev) with your WordPress site while maximizing reliability and avoiding ad-blocker interference.
= Key Features =
* **🏠 Self-Hosted Support**: Works with both OpenPanel Cloud and your own Self-Hosted instance
* **🚀 Ad-Blocker Resistant**: Serves analytics scripts and API calls from your own domain
* **📊 Real-Time Analytics**: Get instant insights without processing delays
* **🔒 Privacy-Friendly**: Cookie-less tracking that respects user privacy (no cookie banners needed!)
* **⚡ Performance Optimized**: Caches scripts locally and uses efficient proxying
* **🎯 Product Analytics**: Funnel analysis, retention tracking, and conversion metrics
* **📈 Web Analytics**: Visitors, referrals, top pages, devices, sessions, and bounce rates
= How It Works =
This plugin integrates OpenPanel with WordPress in a blocker-resistant way:
* **Inlines** `op1.js` directly into your pages (cached locally for 1 week; falls back to CDN if needed)
* **Bootstraps** the OpenPanel SDK with your Client ID automatically
* **Proxies** all SDK requests through WordPress REST API (`/wp-json/openpanel/`)
* **Preserves** all request methods, headers, query parameters, and body data
* **Handles** CORS properly for cross-origin requests
**Why use a proxy?** Serving scripts and data from your own domain origin avoids third-party blocking and improves tracking reliability significantly.
= Privacy Benefits =
* **🍪 No Cookie Banners Required**: OpenPanel uses cookie-less tracking, so you don't need annoying cookie consent banners
* **🛡️ GDPR Friendly**: Compliant with privacy regulations without requiring user consent for basic analytics
* **🔐 Data Ownership**: You maintain full control over your analytics data
* **🚫 No Personal Data Collection**: Tracks behavior patterns without collecting personally identifiable information
**Learn more at [OpenPanel.dev](https://openpanel.dev)**
== Installation ==
= Getting Started (Cloud) =
1. **Get your OpenPanel Client ID**:
* Sign up for an account at [OpenPanel.dev](https://openpanel.dev)
* Create a new project for your website
* Copy your Client ID (starts with `op_client_`)
2. **Install the Plugin**:
* Upload the plugin ZIP file via **Plugins → Add New → Upload Plugin**
* Or place the `openpanel` folder in `/wp-content/plugins/`
* Activate the plugin via **Plugins → Installed Plugins**
3. **Configure Settings**:
* Go to **Settings → OpenPanel** in your WordPress admin
* Select **Cloud (openpanel.dev)** as hosting mode
* Paste your **Client ID** in the settings
* Optionally enable auto-tracking features:
- ✅ **Track page views automatically**
- ✅ **Track clicks on outgoing links**
- ✅ **Track additional page attributes**
4. **Verify Installation**:
* Visit your website frontend
* Check browser developer tools - you should see OpenPanel tracking requests to your own domain
* Check your OpenPanel dashboard for incoming data
**That's it!** No theme edits or manual code insertion required.
= Self-Hosted Setup =
If you run your own OpenPanel instance (e.g., on Coolify, Docker, or any self-hosted environment):
1. **Install the Plugin** (same as above)
2. **Configure Self-Hosted Settings**:
* Go to **Settings → OpenPanel** in your WordPress admin
* Select **Self-Hosted** as hosting mode
* Enter your **API URL** (e.g., `https://api.openpanel.yourdomain.com`)
* Enter your **Dashboard URL** (e.g., `https://openpanel.yourdomain.com`)
* Paste your **Client ID** from your self-hosted OpenPanel instance
* Enable desired auto-tracking features
3. **Verify Configuration**:
* Check the "Current Configuration" table at the bottom of settings page
* Ensure API URL and JS URL point to your self-hosted instance
* Visit your website frontend and verify tracking works
= Self-Hosted URL Examples =
| Setting | Example Value |
|---------|---------------|
| API URL | `https://api.openpanel.yourdomain.com` |
| Dashboard URL | `https://openpanel.yourdomain.com` |
| Client ID | Your project's client ID from OpenPanel |
**Note**: In Self-Hosted mode, op1.js is loaded from your Dashboard URL. In Cloud mode, it is loaded from the official OpenPanel CDN.
== Frequently Asked Questions ==
= What is OpenPanel? =
OpenPanel is an open-source web and product analytics platform designed as a privacy-friendly alternative to traditional analytics solutions. It provides real-time insights, funnel analysis, retention tracking, and more while respecting user privacy.
= Does this plugin support Self-Hosted OpenPanel? =
Yes! Version 1.1.0 adds full support for self-hosted OpenPanel instances. Simply select "Self-Hosted" mode in settings and enter your API URL and Dashboard URL.
= Where is the proxy endpoint? =
The proxy endpoint is at `/wp-json/openpanel/` (followed by the OpenPanel API path). The SDK automatically points to this endpoint, so all analytics requests go through your WordPress site instead of directly to OpenPanel servers (Cloud or Self-Hosted).
= Why do I need this proxy approach? =
Serving analytics scripts and API requests from your own domain significantly reduces blocking by ad-blockers and privacy tools. This improves data collection reliability and ensures more accurate analytics.
= Does it respect CORS and security? =
Yes. The proxy responds with proper CORS headers allowing your site origin and credentials. It also sanitizes all inputs and only forwards legitimate OpenPanel API requests.
= What if inlining `op1.js` fails? =
The plugin automatically falls back to loading the script externally. In Self-Hosted mode, it loads from your Dashboard URL; in Cloud mode, from the OpenPanel CDN (`https://openpanel.dev/op1.js`).
= How is the script cached? =
The `op1.js` script is cached locally for 1 week using WordPress transients. You can manually clear the cache from the plugin settings page if needed.
= Can I limit tracking to certain users or pages? =
Yes! The plugin includes hooks and checks. For example, tracking is automatically disabled for admin pages. You can extend this by modifying the `inject_inline_sdk()` method or using WordPress filters.
= Will this affect my site performance? =
No, the plugin is designed for minimal performance impact. Scripts are cached locally, loaded asynchronously, and the proxy only handles analytics requests efficiently.
= Do I need to modify my theme? =
No theme modifications are required. The plugin automatically injects the necessary tracking code into all frontend pages.
= Is my data secure? =
Yes. OpenPanel respects privacy by design with cookie-less tracking. Your data ownership is maintained, and you can export or delete data as needed. Since no cookies are used, you don't need cookie consent banners on your site.
= Do I need cookie consent banners with OpenPanel? =
No! OpenPanel uses cookie-less tracking technology, which means no cookies are stored on your visitors' devices. This eliminates the need for cookie consent banners and makes your site GDPR compliant for basic analytics without requiring user consent.
= Where can I get support? =
For plugin-specific issues, use the WordPress plugin support forum. For OpenPanel platform questions, visit [OpenPanel.dev](https://openpanel.dev) or their community channels.
== External Services ==
This plugin connects to external OpenPanel.dev services to provide web analytics functionality. Here's what you need to know:
**OpenPanel.dev Analytics Service**
* **What it is**: OpenPanel.dev is an open-source web and product analytics platform that combines the power of Mixpanel with the ease of Plausible and one of the best Google Analytics replacements.
* **What data is sent**:
- **User Agent**: Browser and device information for compatibility and analytics
- **IP Address**: Collected from X-Forwarded-For header for geolocation (no personal identification)
- **Page Information**: Current page URL/path and referrer information
- **Event Data**: Other user-defined properties when custom events are triggered via `window.op('track', 'event_name')`
* **When data is sent**:
- **Page Views**: Only when "Track page views automatically" is enabled in settings
- **Outgoing Link Clicks**: Only when "Track clicks on outgoing links" is enabled in settings
- **Custom Events**: Only when website owner implements `window.op('track', 'custom_event')` method
- **No background tracking** - data is sent only for the specific interactions you've configured
* **External endpoints used**:
- `https://openpanel.dev/op1.js` - Analytics tracking script (cached locally)
- `https://api.openpanel.dev/` - Analytics data collection API (proxied through your WordPress site)
* **Legal Information**:
- Service Terms: https://openpanel.dev/terms
- Privacy Policy: https://openpanel.dev/privacy
This integration is essential for the plugin's core functionality of providing website analytics. The plugin uses a proxy approach to serve requests through your own domain to improve reliability and avoid ad-blocker interference.
== Changelog ==
= 1.1.1 =
* **Self-Hosted JS Loading Fix** - op1.js now loads from Dashboard URL in Self-Hosted mode
* ✅ Fixes ERR_SSL_PROTOCOL_ERROR when openpanel.dev CDN is blocked/filtered
* ✅ Both inline cache and external fallback now use the correct self-hosted URL
= 1.1.0 =
* **Self-Hosted Support** - Full support for self-hosted OpenPanel instances
* ✅ New hosting mode selector: Cloud vs Self-Hosted
* ✅ Configurable API URL for self-hosted instances
* ✅ Configurable Dashboard URL for op1.js loading
* ✅ Dynamic proxy validation for custom domains
* ✅ Current configuration display in settings
* ✅ Toggle visibility for self-hosted fields
= 1.0.0 =
* **Initial Release** - Complete OpenPanel WordPress integration
* ✅ Automatic script inlining with local caching (1 week cache duration)
* ✅ REST API proxy for ad-blocker resistant tracking
* ✅ Auto-tracking options: page views, outgoing links, page attributes
* ✅ CORS-compliant request handling
* ✅ Cache management with manual clear functionality
* ✅ Fallback to CDN if local caching fails
* ✅ Admin interface for easy configuration
* ✅ No theme modifications required
== Upgrade Notice ==
= 1.1.1 =
Fixes op1.js loading for self-hosted instances. Previously op1.js always loaded from openpanel.dev CDN even in Self-Hosted mode, causing SSL errors in regions where the CDN is blocked.
= 1.1.0 =
Adds full support for self-hosted OpenPanel instances. You can now use the plugin with your own OpenPanel deployment on Coolify, Docker, or any self-hosted environment.
= 1.0.0 =
Initial release of the OpenPanel WordPress plugin. Provides ad-blocker resistant analytics with local script caching and API proxying.

Binary file not shown.

View File

@@ -0,0 +1,428 @@
# SEO API Bridge - WordPress Plugin
**Version:** 1.3.0
**Requires:** WordPress 5.0+, PHP 7.4+
**License:** MIT
## 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** - NEW! Dedicated API endpoint for plugin detection
-**Zero Configuration** - Works out of the box after activation
## Supported SEO Fields
### Rank Math SEO
#### Core Fields
- `rank_math_focus_keyword` - Focus keyword
- `rank_math_seo_title` - Meta title
- `rank_math_description` - Meta description
- `rank_math_additional_keywords` - Additional keywords
#### Advanced Fields
- `rank_math_canonical_url` - Canonical URL
- `rank_math_robots` - Robots meta directives
- `rank_math_breadcrumb_title` - Breadcrumb title
#### Open Graph (Facebook)
- `rank_math_facebook_title` - OG title
- `rank_math_facebook_description` - OG description
- `rank_math_facebook_image` - OG image URL
- `rank_math_facebook_image_id` - OG image ID
#### Twitter Card
- `rank_math_twitter_title` - Twitter title
- `rank_math_twitter_description` - Twitter description
- `rank_math_twitter_image` - Twitter image URL
- `rank_math_twitter_image_id` - Twitter image ID
- `rank_math_twitter_card_type` - Card type (summary, summary_large_image)
### Yoast SEO
#### Core Fields
- `_yoast_wpseo_focuskw` - Focus keyword
- `_yoast_wpseo_title` - Meta title
- `_yoast_wpseo_metadesc` - Meta description
#### Advanced Fields
- `_yoast_wpseo_canonical` - Canonical URL
- `_yoast_wpseo_meta-robots-noindex` - Noindex setting
- `_yoast_wpseo_meta-robots-nofollow` - Nofollow setting
- `_yoast_wpseo_bctitle` - Breadcrumb title
#### Open Graph
- `_yoast_wpseo_opengraph-title` - OG title
- `_yoast_wpseo_opengraph-description` - OG description
- `_yoast_wpseo_opengraph-image` - OG image URL
- `_yoast_wpseo_opengraph-image-id` - OG image ID
#### Twitter Card
- `_yoast_wpseo_twitter-title` - Twitter title
- `_yoast_wpseo_twitter-description` - Twitter description
- `_yoast_wpseo_twitter-image` - Twitter image URL
- `_yoast_wpseo_twitter-image-id` - Twitter image ID
## Installation
### Method 1: Manual Upload via WordPress Admin
1. Download the plugin folder
2. Create a ZIP file: `seo-api-bridge.zip`
3. Go to WordPress Admin → Plugins → Add New → Upload Plugin
4. Upload the ZIP file and click "Install Now"
5. 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 (Recommended for Coolify)
```bash
# Via SSH to your WordPress container
cd /var/www/html/wp-content/plugins/
# Copy the plugin folder here
wp plugin activate seo-api-bridge
```
## REST API Endpoints
### Status Endpoint
**GET** `/wp-json/seo-bridge/v1/status`
Check plugin status and detected SEO plugins.
```bash
curl https://yoursite.com/wp-json/seo-bridge/v1/status
```
### Post SEO Endpoints
**GET** `/wp-json/seo-bridge/v1/posts/{id}/seo`
Get SEO metadata for a post.
**POST** `/wp-json/seo-bridge/v1/posts/{id}/seo`
Update SEO metadata for a post.
```bash
curl -X POST https://yoursite.com/wp-json/seo-bridge/v1/posts/123/seo \
-H "Content-Type: application/json" \
-d '{"focus_keyword": "wordpress", "seo_title": "My Title"}'
```
### Page SEO Endpoints
**GET** `/wp-json/seo-bridge/v1/pages/{id}/seo`
**POST** `/wp-json/seo-bridge/v1/pages/{id}/seo`
### Product SEO Endpoints (WooCommerce)
**GET** `/wp-json/seo-bridge/v1/products/{id}/seo`
Get SEO metadata for a WooCommerce product.
**POST** `/wp-json/seo-bridge/v1/products/{id}/seo`
Update SEO metadata for a WooCommerce product.
```bash
curl -X POST https://yoursite.com/wp-json/seo-bridge/v1/products/1217/seo \
-H "Content-Type: application/json" \
-d '{"focus_keyword": "product keyword", "seo_title": "Product Title"}'
```
## Usage with MCP Servers
```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"
});
```
**Benefits of this approach:**
1.**WordPress Best Practice** - Uses recommended core REST API functionality
2.**Zero Maintenance** - Leverages WordPress built-in features
3.**Universal Compatibility** - Works with all WordPress REST API clients
4.**Inherits Security** - Uses WordPress authentication and permissions
5.**Simpler Code** - No custom endpoints to maintain
**Standard Endpoints:**
- Posts/Pages: `/wp-json/wp/v2/posts/{id}` or `/wp-json/wp/v2/pages/{id}`
- Products: `/wp-json/wp/v2/products/{id}` (WooCommerce custom post type)
**SEO data is in the `meta` object:**
```json
{
"id": 123,
"title": {"rendered": "My Post"},
"meta": {
"rank_math_focus_keyword": "wordpress seo",
"rank_math_seo_title": "Complete SEO Guide",
"rank_math_description": "Learn WordPress SEO best practices..."
}
}
```
### Reading SEO Data
**Get Post with SEO Fields:**
```bash
GET /wp-json/wp/v2/posts/{id}
```
**Response includes:**
```json
{
"id": 123,
"title": {"rendered": "My Post"},
"meta": {
"rank_math_focus_keyword": "wordpress seo",
"rank_math_seo_title": "Complete WordPress SEO Guide",
"rank_math_description": "Learn how to optimize your WordPress site...",
"rank_math_facebook_title": "SEO Guide for Facebook",
"rank_math_twitter_card_type": "summary_large_image"
}
}
```
### Writing SEO Data
**Update Post SEO Fields:**
```bash
POST /wp-json/wp/v2/posts/{id}
Authorization: Basic [base64(username:application_password)]
Content-Type: application/json
{
"meta": {
"rank_math_focus_keyword": "wordpress optimization",
"rank_math_seo_title": "WordPress Optimization Tips",
"rank_math_description": "Discover the best practices for WordPress optimization"
}
}
```
### WooCommerce Products
**Get Product with SEO:**
```bash
GET /wp-json/wp/v2/products/{id}
```
**Update Product SEO:**
```bash
POST /wp-json/wp/v2/products/{id}
{
"meta": {
"rank_math_focus_keyword": "premium widget",
"rank_math_description": "Buy the best premium widget online"
}
}
```
## Usage with MCP Server
This plugin is designed to work with the [Coolify Projects MCP Server](https://github.com/your-repo/mcphub).
### Example MCP Tool Usage
```python
# Get post SEO data
result = await mcp.call_tool(
"wordpress_site1_get_post_seo",
{"post_id": 123}
)
# Update post SEO
result = await mcp.call_tool(
"wordpress_site1_update_post_seo",
{
"post_id": 123,
"focus_keyword": "wordpress seo",
"seo_title": "Complete SEO Guide",
"meta_description": "Learn WordPress SEO..."
}
)
# Update WooCommerce product SEO
result = await mcp.call_tool(
"wordpress_site1_update_product_seo",
{
"product_id": 456,
"focus_keyword": "premium widget",
"meta_description": "Buy the best widget"
}
)
```
## Verification
### Check if Plugin is Working
1. **Admin Notice:** After activation, you should see a success notice indicating which SEO plugin was detected
2. **Health Check Endpoint (v1.1.0+):**
```bash
# Check plugin status directly
curl -X GET "https://your-site.com/wp-json/seo-api-bridge/v1/status" \
-u "username:application_password"
```
**Response:**
```json
{
"plugin": "SEO API Bridge",
"version": "1.1.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."
}
```
3. **REST API Test:**
```bash
# Get any post and check if meta fields are present
curl -X GET "https://your-site.com/wp-json/wp/v2/posts/1" \
-u "username:application_password"
```
4. **MCP Health Check:** The MCP server will automatically detect the plugin using the new 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
**Issue:** MCP reports "SEO API Bridge plugin not detected" even though plugin is active
**Solution for v1.1.0+:**
1. **Upgrade to v1.1.0** - This version adds a dedicated health check endpoint
2. **Restart MCP server** - New detection logic will be used
3. **Test endpoint directly:**
```bash
curl "https://your-site.com/wp-json/seo-api-bridge/v1/status" -u "user:pass"
```
**Solution for v1.0.0 (legacy):**
1. Create at least one post or product with SEO metadata set
2. The old detection requires checking actual content meta fields
3. Upgrade to v1.1.0 to avoid this requirement
### SEO Plugin Not Detected
**Issue:** Admin notice says "Neither Rank Math SEO nor Yoast SEO is detected"
**Solution:**
1. Verify Rank Math or Yoast SEO is installed and activated
2. Try deactivating and reactivating SEO API Bridge
3. Check PHP error logs for any warnings
### Meta Fields Not Appearing in REST API
**Issue:** `/wp-json/wp/v2/posts/{id}` doesn't show `meta` object
**Solution:**
1. Ensure you're authenticated (use Application Password)
2. Check that you have `edit_posts` permission
3. Verify the post type is supported (post, page, product)
### Fields Are Read-Only
**Issue:** Can read SEO fields but cannot update them
**Solution:**
1. Verify authentication credentials
2. Check user has `edit_posts` capability
3. Ensure you're using POST/PUT request, not GET
## 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
## Support
For issues related to:
- **This plugin:** [GitHub Issues](https://github.com/your-repo/mcphub/issues)
- **MCP Server:** [MCP Server Documentation](https://github.com/your-repo/mcphub)
- **Rank Math SEO:** [Rank Math Support](https://rankmath.com/support/)
- **Yoast SEO:** [Yoast Support](https://yoast.com/help/)
## Changelog
### 1.1.0 (2025-01-10)
- 🎉 **NEW:** Health check REST API endpoint `/seo-api-bridge/v1/status`
- 🎉 **NEW:** Direct plugin detection without requiring posts/products
- 🎉 **IMPROVEMENT:** Better compatibility with sites that have no content
- 🎉 **IMPROVEMENT:** Returns SEO plugin versions in status endpoint
- 🎉 **FIX:** MCP server can now detect plugin even with zero posts
### 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
- Admin notices for status
## License
MIT License - See LICENSE file for details
## Credits
Developed for use with Coolify Projects MCP Server to enable AI-powered SEO content management.

View File

@@ -0,0 +1,699 @@
<?php
/**
* Plugin Name: SEO API Bridge
* Plugin URI: https://github.com/your-repo/seo-api-bridge
* 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 Coolify Projects
* Author URI: https://github.com/your-repo
* License: MIT
* Requires at least: 5.0
* Requires PHP: 7.4
* Text Domain: seo-api-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;
}
/**
* SEO API 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('seo-api-bridge/v1', '/status', [
'methods' => 'GET',
'callback' => [$this, 'get_status'],
'permission_callback' => function() {
return is_user_logged_in();
}
]);
// Post SEO endpoints
register_rest_route('seo-api-bridge/v1', '/posts/(?P<id>\d+)/seo', [
[
'methods' => 'GET',
'callback' => [$this, 'get_post_seo'],
'permission_callback' => '__return_true',
'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('seo-api-bridge/v1', '/pages/(?P<id>\d+)/seo', [
[
'methods' => 'GET',
'callback' => [$this, 'get_page_seo'],
'permission_callback' => '__return_true',
'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('seo-api-bridge/v1', '/products/(?P<id>\d+)/seo', [
[
'methods' => 'GET',
'callback' => [$this, 'get_product_seo'],
'permission_callback' => '__return_true',
'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' => 'SEO API 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 'SEO API 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_seo_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) {
register_post_meta($post_type, $meta_key, [
'show_in_rest' => true,
'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() {
$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>SEO API Bridge:</strong> 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.</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>SEO API Bridge v' . self::VERSION . ':</strong> Successfully registered meta fields for ' . implode(' and ', $active_plugins) . '.</p>';
echo '<p><strong>Supported post types:</strong> ' . $supported_types . '</p>';
if ($woocommerce_active) {
echo '<p><strong>WooCommerce:</strong> Detected and supported. Product SEO fields are available via REST API.</p>';
}
echo '</div>';
}
}
}
// Initialize the plugin
new SEO_API_Bridge();