PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / wp-mcp-server / tools / class-mcp-tool-otto.php

class-mcp-tool-otto.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.0, at wp-mcp-server/tools/class-mcp-tool-otto.php

673 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Tools: OTTO Pipeline Tools
4 *
5 * Provides three MCP tools that close the loop for AI-driven SEO work:
6 * 1. Trigger Otto Optimization — runs the full OTTO pipeline (warm
7 * transient → SEO DB sync → plugin sync → cache purge → cache warm →
8 * CDN purge) for a URL or post.
9 * 2. Get Otto Status — returns the current OTTO state for a URL or post
10 * (enabled flag, exclusion, transient warmth, last-written meta,
11 * persistence settings, plugin sync timestamps).
12 * 3. Verify SEO Output — fetches the live rendered HTML for a URL and
13 * compares the rendered head tags against stored post meta.
14 *
15 * @package MetaSync
16 * @subpackage MCP_Server/Tools
17 */
18
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * Trigger OTTO Optimization Tool
25 */
26 class MCP_Tool_Trigger_Otto_Optimization extends MCP_Tool_Base {
27
28 public function get_name() {
29 return 'wordpress_trigger_otto_optimization';
30 }
31
32 public function get_description() {
33 return 'Trigger the full OTTO optimization pipeline for a URL or post ID: warm transient → SEO DB sync → plugin sync → cache purge → cache warm → CDN purge. Requires OTTO to be enabled and is rate-limited to 10 calls per minute per API key.';
34 }
35
36 public function get_input_schema() {
37 return [
38 'type' => 'object',
39 'properties' => [
40 'url' => [
41 'type' => 'string',
42 'description' => 'Absolute URL to optimize. Either url or post_id must be provided.',
43 ],
44 'post_id' => [
45 'type' => 'integer',
46 'description' => 'Post or page ID to optimize. Either url or post_id must be provided.',
47 ],
48 ],
49 'required' => [],
50 ];
51 }
52
53 public function execute($params) {
54 $this->validate_params($params);
55 $this->require_capability('manage_options');
56
57 $has_url = !empty($params['url']);
58 $has_post_id = isset($params['post_id']) && (int) $params['post_id'] > 0;
59
60 if (!$has_url && !$has_post_id) {
61 throw new InvalidArgumentException('Either url or post_id is required');
62 }
63 if ($has_url && $has_post_id) {
64 throw new InvalidArgumentException('Provide only one of url or post_id, not both');
65 }
66
67 // Resolve url <-> post_id
68 if ($has_post_id) {
69 $post_id = $this->sanitize_integer($params['post_id']);
70 $url = get_permalink($post_id);
71 if (empty($url)) {
72 throw new Exception(sprintf('Unable to resolve permalink for post_id %d', $post_id));
73 }
74 } else {
75 $url = $this->sanitize_url($params['url']);
76 $post_id = (int) url_to_postid($url);
77 }
78
79 // Post-level permission check (when a specific post is targeted)
80 if ($has_post_id && $post_id > 0) {
81 $this->verify_post_exists($post_id);
82 $this->check_post_permission($post_id);
83 }
84
85 // Load OTTO config
86 $otto_base = dirname(dirname(dirname(__FILE__))) . '/otto/';
87 if (!class_exists('Metasync_Otto_Config')) {
88 $config_file = $otto_base . 'class-metasync-otto-config.php';
89 if (file_exists($config_file)) {
90 require_once $config_file;
91 }
92 }
93
94 if (!class_exists('Metasync_Otto_Config') || !Metasync_Otto_Config::is_otto_enabled()) {
95 throw new Exception('OTTO is not enabled for this site.');
96 }
97
98 // Rate-limit: 10 calls per 60s per API key (fall back to IP)
99 if (class_exists('Metasync_Rate_Limiter')) {
100 $options = get_option('metasync_options', []);
101 $api_key = isset($options['general']['apikey']) ? (string) $options['general']['apikey'] : '';
102 $rate_source = $api_key !== '' ? $api_key : (isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : 'anon');
103 $rate_key = hash('sha256', $rate_source);
104 $rl_result = Metasync_Rate_Limiter::get_instance()->check_rate_limit($rate_key, 10, 60, 'otto_trigger_');
105 if (is_wp_error($rl_result)) {
106 throw new Exception($rl_result->get_error_message());
107 }
108 }
109
110 $otto_uuid = Metasync_Otto_Config::get_otto_uuid();
111 $steps = [];
112
113 // Helper: time a callable and return a standardized step result.
114 // The callable returns either a boolean success flag or an array
115 // overriding fields in the result (e.g. ['success'=>false,'reason'=>'x']).
116 $run_step = function (callable $fn) {
117 $start = microtime(true);
118 try {
119 $outcome = $fn();
120 } catch (Exception $e) {
121 return [
122 'success' => false,
123 'error' => $e->getMessage(),
124 'duration_ms' => round((microtime(true) - $start) * 1000, 2),
125 ];
126 }
127 $duration_ms = round((microtime(true) - $start) * 1000, 2);
128 if (is_array($outcome)) {
129 return array_merge(['duration_ms' => $duration_ms], $outcome);
130 }
131 return [
132 'success' => (bool) $outcome,
133 'duration_ms' => $duration_ms,
134 ];
135 };
136
137 // Step 1: warm_transient
138 $steps['warm_transient'] = $run_step(function () use ($otto_base, $otto_uuid, $url) {
139 $transient_file = $otto_base . 'class-metasync-otto-transient-cache.php';
140 if (!class_exists('Metasync_Otto_Transient_Cache') && file_exists($transient_file)) {
141 require_once $transient_file;
142 }
143 if (!class_exists('Metasync_Otto_Transient_Cache') || empty($otto_uuid)) {
144 return ['success' => false, 'reason' => 'not_available'];
145 }
146 $tc = new Metasync_Otto_Transient_Cache($otto_uuid);
147 $result = $tc->warm_cache($url);
148 // Report success only when real suggestions were cached. A URL OTTO
149 // holds no data for returns an empty array rather than false, so a
150 // `!== false` check would report a successful warm for a URL with
151 // nothing to warm — inverting the signal you would use to debug this.
152 if ($result === false) {
153 return ['success' => false, 'reason' => 'fetch_failed'];
154 }
155 if (!$tc->has_payload($result)) {
156 return ['success' => false, 'reason' => 'no_suggestions_for_url'];
157 }
158 return true;
159 });
160
161 // Step 2: seo_db_sync
162 $steps['seo_db_sync'] = $run_step(function () use ($url) {
163 if (!function_exists('metasync_process_otto_seo_data')) {
164 return ['success' => false, 'reason' => 'not_available'];
165 }
166 // The sync returns an outcome string (success / no_change /
167 // retryable / permanent / deferred); a no-change result is a
168 // successful sync, and anything else carries its reason through.
169 $outcome = metasync_process_otto_seo_data($url);
170 if ($outcome === Metasync_Otto_Job_Status::OUTCOME_SUCCESS
171 || $outcome === Metasync_Otto_Job_Status::OUTCOME_NO_CHANGE) {
172 return true;
173 }
174 return ['success' => false, 'reason' => is_string($outcome) ? $outcome : 'unknown'];
175 });
176
177 // Step 3: plugin_sync (may not be available yet)
178 $steps['plugin_sync'] = $run_step(function () use ($post_id) {
179 if (!class_exists('Metasync_Plugin_Sync')) {
180 return ['success' => false, 'reason' => 'not_available'];
181 }
182 $sync_result = null;
183 if (method_exists('Metasync_Plugin_Sync', 'sync_post')) {
184 $sync_result = Metasync_Plugin_Sync::sync_post($post_id);
185 } elseif (method_exists('Metasync_Plugin_Sync', 'sync')) {
186 $sync_result = Metasync_Plugin_Sync::sync($post_id);
187 } else {
188 return ['success' => false, 'reason' => 'not_available'];
189 }
190 return $sync_result !== false;
191 });
192
193 // Step 4: cache_purge
194 $steps['cache_purge'] = $run_step(function () use ($url) {
195 if (!class_exists('Metasync_Cache_Purge')) {
196 return ['success' => false, 'reason' => 'not_available'];
197 }
198 Metasync_Cache_Purge::purge_single_url($url);
199 return true;
200 });
201
202 // Step 5: cache_warm
203 $steps['cache_warm'] = $run_step(function () use ($url) {
204 if (!class_exists('Metasync_Cache_Purge')) {
205 return ['success' => false, 'reason' => 'not_available'];
206 }
207 Metasync_Cache_Purge::warm_urls([$url]);
208 return true;
209 });
210
211 // Step 6: cdn_purge
212 $steps['cdn_purge'] = $run_step(function () use ($url) {
213 if (!class_exists('Metasync_Edge_Cache_Purge')) {
214 return ['success' => false, 'reason' => 'not_available'];
215 }
216 Metasync_Edge_Cache_Purge::purge([$url]);
217 return true;
218 });
219
220 return $this->success(
221 [
222 'url' => $url,
223 'post_id' => $post_id ?: null,
224 'steps' => $steps,
225 ],
226 'OTTO optimization pipeline completed'
227 );
228 }
229 }
230
231 /**
232 * Get OTTO Status Tool
233 */
234 class MCP_Tool_Get_Otto_Status extends MCP_Tool_Base {
235
236 public function get_name() {
237 return 'wordpress_get_otto_status';
238 }
239
240 public function get_description() {
241 return 'Get the current OTTO state for a URL or post: enabled flag, exclusion status, transient warmth, last-written OTTO meta values, persistence settings, and plugin sync timestamps. Works regardless of whether OTTO is enabled.';
242 }
243
244 public function get_input_schema() {
245 return [
246 'type' => 'object',
247 'properties' => [
248 'url' => [
249 'type' => 'string',
250 'description' => 'Absolute URL to inspect. Either url or post_id must be provided.',
251 ],
252 'post_id' => [
253 'type' => 'integer',
254 'description' => 'Post or page ID to inspect. Either url or post_id must be provided.',
255 ],
256 ],
257 'required' => [],
258 ];
259 }
260
261 public function execute($params) {
262 $this->validate_params($params);
263 $this->require_capability('manage_options');
264
265 $has_url = !empty($params['url']);
266 $has_post_id = isset($params['post_id']) && (int) $params['post_id'] > 0;
267
268 if (!$has_url && !$has_post_id) {
269 throw new InvalidArgumentException('Either url or post_id is required');
270 }
271 if ($has_url && $has_post_id) {
272 throw new InvalidArgumentException('Provide only one of url or post_id, not both');
273 }
274
275 if ($has_post_id) {
276 $post_id = $this->sanitize_integer($params['post_id']);
277 $this->verify_post_exists($post_id);
278 $this->check_post_permission($post_id);
279 $url = get_permalink($post_id);
280 } else {
281 $url = $this->sanitize_url($params['url']);
282 $post_id = (int) url_to_postid($url);
283 if ($post_id > 0) {
284 $this->check_post_permission($post_id);
285 }
286 }
287
288 $otto_base = dirname(dirname(dirname(__FILE__))) . '/otto/';
289 if (!class_exists('Metasync_Otto_Config')) {
290 $config_file = $otto_base . 'class-metasync-otto-config.php';
291 if (file_exists($config_file)) {
292 require_once $config_file;
293 }
294 }
295
296 $otto_enabled = class_exists('Metasync_Otto_Config') ? Metasync_Otto_Config::is_otto_enabled() : false;
297 $otto_uuid = class_exists('Metasync_Otto_Config') ? Metasync_Otto_Config::get_otto_uuid() : '';
298
299 // Exclusion status (manual only — reflects user-configured exclusions)
300 $url_excluded = null;
301 if (!empty($url)) {
302 if (!function_exists('metasync_is_otto_url_manually_excluded')) {
303 $pixel_file = $otto_base . 'otto_pixel.php';
304 if (file_exists($pixel_file)) {
305 require_once $pixel_file;
306 }
307 }
308 if (function_exists('metasync_is_otto_url_manually_excluded')) {
309 $url_excluded = (bool) metasync_is_otto_url_manually_excluded($url);
310 }
311 }
312
313 // Transient state
314 $transient = [
315 'warm' => false,
316 'has_suggestions' => false,
317 ];
318 if (!empty($url) && !empty($otto_uuid)) {
319 if (!class_exists('Metasync_Otto_Transient_Cache')) {
320 $transient_file = $otto_base . 'class-metasync-otto-transient-cache.php';
321 if (file_exists($transient_file)) {
322 require_once $transient_file;
323 }
324 }
325 if (class_exists('Metasync_Otto_Transient_Cache')) {
326 $tc = new Metasync_Otto_Transient_Cache($otto_uuid);
327 $stats = $tc->get_stats($url);
328 $transient['warm'] = !empty($stats['has_cache']);
329 $transient['has_suggestions'] = !empty($stats['has_suggestions']);
330 $transient['cache_key'] = isset($stats['cache_key']) ? $stats['cache_key'] : null;
331
332 // Get transient expiry for the OTTO cache
333 global $wpdb;
334 $site_id = is_multisite() ? get_current_blog_id() : 0;
335 $normalized_url = rtrim(strtolower($url), '/');
336 $url_hash = md5($normalized_url);
337 $transient_key = 'otto_suggestions_' . $site_id . '_' . $url_hash;
338 $timeout_key = '_transient_timeout_' . $transient_key;
339 $expiry_raw = $wpdb->get_var(
340 $wpdb->prepare(
341 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
342 $timeout_key
343 )
344 );
345 $cache_expires_at = $expiry_raw ? (int) $expiry_raw : null;
346 $cache_expires_in = $cache_expires_at ? max(0, $cache_expires_at - time()) : null;
347
348 $transient['cache_expires_at'] = $cache_expires_at;
349 $transient['cache_expires_in_seconds'] = $cache_expires_in;
350 }
351 }
352
353 // Last-written OTTO meta values
354 $otto_meta_keys = [
355 '_metasync_otto_title',
356 '_metasync_otto_description',
357 '_metasync_otto_keywords',
358 '_metasync_otto_og_title',
359 '_metasync_otto_og_description',
360 '_metasync_otto_twitter_title',
361 '_metasync_otto_twitter_description',
362 '_metasync_canonical_url',
363 '_metasync_otto_structured_data',
364 '_metasync_otto_image_alt_data',
365 '_metasync_otto_headings_data',
366 '_metasync_otto_last_update',
367 ];
368 $last_written_meta = [];
369 if ($post_id > 0) {
370 foreach ($otto_meta_keys as $key) {
371 $val = get_post_meta($post_id, $key, true);
372 if ($val !== '' && $val !== false && $val !== null) {
373 $last_written_meta[$key] = $val;
374 }
375 }
376 }
377
378 // Persistence settings
379 $persistence_settings = null;
380 if (!class_exists('Metasync_Otto_Persistence_Settings')) {
381 $persist_file = $otto_base . 'class-metasync-otto-persistence-settings.php';
382 if (file_exists($persist_file)) {
383 require_once $persist_file;
384 }
385 }
386 if (class_exists('Metasync_Otto_Persistence_Settings')) {
387 $persistence_settings = Metasync_Otto_Persistence_Settings::get_settings();
388 }
389
390 // Plugin sync timestamp (written by when available)
391 $plugin_sync_timestamp = null;
392 if ($post_id > 0) {
393 $ts = get_post_meta($post_id, '_metasync_plugin_sync_ts', true);
394 $plugin_sync_timestamp = $ts !== '' ? $ts : null;
395 }
396
397 return $this->success(
398 [
399 'otto_enabled' => (bool) $otto_enabled,
400 'url' => $url,
401 'post_id' => $post_id ?: null,
402 'url_excluded' => $url_excluded,
403 'transient' => $transient,
404 'last_written_meta' => $last_written_meta,
405 'persistence_settings' => $persistence_settings,
406 'plugin_sync_timestamp' => $plugin_sync_timestamp,
407 ],
408 'OTTO status retrieved'
409 );
410 }
411 }
412
413 /**
414 * Verify SEO Output Tool
415 */
416 class MCP_Tool_Verify_SEO_Output extends MCP_Tool_Base {
417
418 public function get_name() {
419 return 'wordpress_verify_seo_output';
420 }
421
422 public function get_description() {
423 return 'Fetch the live rendered HTML for a URL and extract what Google actually sees (title, description, canonical, robots, OG/Twitter tags, JSON-LD schema, hreflang). Compares rendered values against stored post meta and reports duplicate head tags.';
424 }
425
426 public function get_input_schema() {
427 return [
428 'type' => 'object',
429 'properties' => [
430 'url' => [
431 'type' => 'string',
432 'description' => 'Absolute URL to fetch and verify.',
433 ],
434 'fields' => [
435 'type' => 'array',
436 'description' => 'Optional list of field categories to include. If omitted, all are returned.',
437 'items' => [
438 'type' => 'string',
439 'enum' => ['title', 'description', 'canonical', 'robots', 'og', 'twitter', 'hreflang', 'schema'],
440 ],
441 ],
442 ],
443 'required' => ['url'],
444 ];
445 }
446
447 public function execute($params) {
448 $this->validate_params($params);
449 $this->require_capability('manage_options');
450
451 $url = $this->sanitize_url($params['url']);
452 if (empty($url)) {
453 throw new InvalidArgumentException('url is required');
454 }
455
456 $fields_filter = [];
457 if (!empty($params['fields']) && is_array($params['fields'])) {
458 foreach ($params['fields'] as $f) {
459 if (is_string($f)) {
460 $fields_filter[] = strtolower($f);
461 }
462 }
463 }
464 $include = function ($category) use ($fields_filter) {
465 return empty($fields_filter) || in_array($category, $fields_filter, true);
466 };
467
468 $post_id = (int) url_to_postid($url);
469
470 // Fetch the live HTML
471 $response = wp_remote_get($url, [
472 'timeout' => 15,
473 'redirection' => 5,
474 'user-agent' => 'MetaSync-Verify/1.0',
475 'sslverify' => false,
476 ]);
477
478 if (is_wp_error($response)) {
479 throw new Exception('HTTP fetch failed: ' . $response->get_error_message());
480 }
481
482 $status = (int) wp_remote_retrieve_response_code($response);
483 $html = (string) wp_remote_retrieve_body($response);
484
485 // Load simplehtmldom via the OTTO HTML class which owns the vendor lib
486 $html_class_file = dirname(dirname(dirname(__FILE__))) . '/otto/Otto_html_class.php';
487 if (file_exists($html_class_file)) {
488 require_once $html_class_file;
489 }
490 if (!class_exists('simplehtmldom\\HtmlDocument')) {
491 throw new Exception('simplehtmldom HtmlDocument is not available');
492 }
493
494 $dom = new simplehtmldom\HtmlDocument($html, true, true, 'UTF-8', false);
495
496 // --- Extract rendered values ---
497 $rendered = [];
498 $duplicates = [];
499
500 // title
501 $title_nodes = $dom->find('title');
502 $title_val = null;
503 if (!empty($title_nodes)) {
504 $title_val = trim($title_nodes[0]->plaintext);
505 if (count($title_nodes) > 1) {
506 $duplicates['title'] = count($title_nodes);
507 }
508 }
509 if ($include('title')) {
510 $rendered['title'] = $title_val;
511 }
512
513 // meta description
514 $desc_nodes = $dom->find('meta[name=description]');
515 $desc_val = null;
516 if (!empty($desc_nodes)) {
517 $desc_val = isset($desc_nodes[0]->content) ? trim($desc_nodes[0]->content) : null;
518 if (count($desc_nodes) > 1) {
519 $duplicates['meta[name=description]'] = count($desc_nodes);
520 }
521 }
522 if ($include('description')) {
523 $rendered['description'] = $desc_val;
524 }
525
526 // canonical
527 $canon_nodes = $dom->find('link[rel=canonical]');
528 $canon_val = null;
529 if (!empty($canon_nodes)) {
530 $canon_val = isset($canon_nodes[0]->href) ? trim($canon_nodes[0]->href) : null;
531 if (count($canon_nodes) > 1) {
532 $duplicates['link[rel=canonical]'] = count($canon_nodes);
533 }
534 }
535 if ($include('canonical')) {
536 $rendered['canonical'] = $canon_val;
537 }
538
539 // robots
540 $robots_nodes = $dom->find('meta[name=robots]');
541 $robots_val = null;
542 if (!empty($robots_nodes)) {
543 $robots_val = isset($robots_nodes[0]->content) ? trim($robots_nodes[0]->content) : null;
544 if (count($robots_nodes) > 1) {
545 $duplicates['meta[name=robots]'] = count($robots_nodes);
546 }
547 }
548 if ($include('robots')) {
549 $rendered['robots'] = $robots_val;
550 }
551
552 // OG tags (property starts with og:)
553 if ($include('og')) {
554 $og_tags = [];
555 foreach ($dom->find('meta') as $m) {
556 $property = isset($m->property) ? (string) $m->property : '';
557 if ($property !== '' && strpos($property, 'og:') === 0) {
558 $og_tags[$property] = isset($m->content) ? (string) $m->content : '';
559 }
560 }
561 $rendered['og'] = $og_tags;
562 }
563
564 // Twitter tags
565 if ($include('twitter')) {
566 $tw_tags = [];
567 foreach ($dom->find('meta') as $m) {
568 $name = isset($m->name) ? (string) $m->name : '';
569 if ($name !== '' && strpos($name, 'twitter:') === 0) {
570 $tw_tags[$name] = isset($m->content) ? (string) $m->content : '';
571 }
572 }
573 $rendered['twitter'] = $tw_tags;
574 }
575
576 // hreflang
577 if ($include('hreflang')) {
578 $hreflang = [];
579 foreach ($dom->find('link[rel=alternate]') as $lnk) {
580 $hl = isset($lnk->hreflang) ? (string) $lnk->hreflang : '';
581 if ($hl !== '') {
582 $hreflang[] = [
583 'hreflang' => $hl,
584 'href' => isset($lnk->href) ? (string) $lnk->href : '',
585 ];
586 }
587 }
588 $rendered['hreflang'] = $hreflang;
589 }
590
591 // JSON-LD schema
592 if ($include('schema')) {
593 $schemas = [];
594 foreach ($dom->find('script[type=application/ld+json]') as $s) {
595 $raw = trim($s->innertext);
596 $decoded = json_decode($raw, true);
597 if (json_last_error() === JSON_ERROR_NONE) {
598 $schemas[] = $decoded;
599 } else {
600 $schemas[] = ['_raw' => $raw, '_parse_error' => json_last_error_msg()];
601 }
602 }
603 $rendered['schema'] = $schemas;
604 }
605
606 // --- Stored meta for comparison ---
607 $stored_meta_keys = [
608 'title' => '_metasync_otto_title',
609 'description' => '_metasync_otto_description',
610 'canonical' => '_metasync_canonical_url',
611 'og_title' => '_metasync_otto_og_title',
612 'og_description' => '_metasync_otto_og_description',
613 'twitter_title' => '_metasync_otto_twitter_title',
614 'twitter_description' => '_metasync_otto_twitter_description',
615 ];
616 $stored_meta = [];
617 if ($post_id > 0) {
618 foreach ($stored_meta_keys as $logical => $meta_key) {
619 $val = get_post_meta($post_id, $meta_key, true);
620 $stored_meta[$logical] = $val !== '' ? $val : null;
621 }
622 } else {
623 foreach ($stored_meta_keys as $logical => $meta_key) {
624 $stored_meta[$logical] = null;
625 }
626 }
627
628 // --- Field-by-field comparison ---
629 $comparison = [];
630
631 $add_comparison = function ($category, $field, $stored_val, $rendered_val) use (&$comparison, $include) {
632 if (!$include($category)) {
633 return;
634 }
635 $comparison[] = [
636 'field' => $field,
637 'match' => ($stored_val === $rendered_val) || ($stored_val === null && $rendered_val === null) || ((string) $stored_val === (string) $rendered_val),
638 'stored' => $stored_val,
639 'rendered' => $rendered_val,
640 ];
641 };
642
643 $add_comparison('title', 'title', $stored_meta['title'], $title_val);
644 $add_comparison('description', 'description', $stored_meta['description'], $desc_val);
645 $add_comparison('canonical', 'canonical', $stored_meta['canonical'], $canon_val);
646
647 if ($include('og')) {
648 $og_rendered = isset($rendered['og']) ? $rendered['og'] : [];
649 $add_comparison('og', 'og:title', $stored_meta['og_title'], isset($og_rendered['og:title']) ? $og_rendered['og:title'] : null);
650 $add_comparison('og', 'og:description', $stored_meta['og_description'], isset($og_rendered['og:description']) ? $og_rendered['og:description'] : null);
651 }
652 if ($include('twitter')) {
653 $tw_rendered = isset($rendered['twitter']) ? $rendered['twitter'] : [];
654 $add_comparison('twitter', 'twitter:title', $stored_meta['twitter_title'], isset($tw_rendered['twitter:title']) ? $tw_rendered['twitter:title'] : null);
655 $add_comparison('twitter', 'twitter:description', $stored_meta['twitter_description'], isset($tw_rendered['twitter:description']) ? $tw_rendered['twitter:description'] : null);
656 }
657
658 return $this->success(
659 [
660 'url' => $url,
661 'post_id' => $post_id ?: null,
662 'http_status' => $status,
663 'fetch_status' => $status >= 200 && $status < 400 ? 'ok' : 'error',
664 'rendered' => $rendered,
665 'stored_meta' => $stored_meta,
666 'comparison' => $comparison,
667 'duplicate_tags' => $duplicates,
668 ],
669 'SEO output verification complete'
670 );
671 }
672 }
673