| @@ -362,11 +362,16 @@ | ||
| 362 | 362 | * mirrors written for third-party plugins and the MCP tools; no singular render |
| 363 | 363 | * path reads them (confirmed by two independent traces). Ranking them above OTTO |
| 364 | 364 | * — as Metasync_SEO_Health still does — makes the column disagree with the page. |
| 365 | 365 | * |
| 366 | - * Reads post meta only. No API calls and no transient lookups: WP_Query has | |
| 367 | - * already primed the post meta cache for this list table, so this costs nothing, | |
| 368 | - * whereas a per-row transient read would not. | |
| 366 | + * Reads post meta only, with one exception: the OTTO disable test also consults | |
| 367 | + * the manual URL exclusion list, which costs a URL resolution per row plus a | |
| 368 | + * single cached read of the exclusion set for the whole request. On the Pages | |
| 369 | + * table that resolution is not free — get_permalink() walks ancestors through | |
| 370 | + * get_page_uri() for a nested page, which WP_Query has not primed, and it fires | |
| 371 | + * the post_link/page_link filters that translation and permalink plugins hook. | |
| 372 | + * Everything else here is free: WP_Query has already primed the post meta cache | |
| 373 | + * for this list table. | |
| 369 | 374 | * |
| 370 | 375 | * @param int $post_id Post ID. |
| 371 | 376 | * @param string $field 'title' or 'description'. |
| 372 | 377 | * @return array{value:string,source:string} Source is '' for a user-owned value. |
| @@ -374,17 +379,30 @@ | ||
| 374 | 379 | private function resolve_rendered_value($post_id, $field) |
| 375 | 380 | { |
| 376 | 381 | $is_title = ($field === 'title'); |
| 377 | 382 | |
| 378 | - $chain = array($is_title ? '_metasync_seo_title' : '_metasync_seo_desc' => ''); | |
| 379 | - | |
| 383 | + // MetaSync's own tiers come from Metasync_Seo_Precedence, the one place | |
| 384 | + // the order is defined. | |
| 385 | + // | |
| 380 | 386 | // OTTO stands down entirely for a post with the per-post disable flag set |
| 381 | 387 | // (otto/metasync-otto-seo-functions.php:2582 -> the title filter returns the |
| 382 | 388 | // incoming title untouched). Its persisted suggestion is then dead data, so |
| 383 | 389 | // reporting it would name a value the page does not use. |
| 384 | - if (!$this->otto_disabled_for_post($post_id)) { | |
| 385 | - $chain[$is_title ? '_metasync_otto_title' : '_metasync_otto_description'] = 'OTTO'; | |
| 386 | - } | |
| 390 | + // | |
| 391 | + // The persisted-OTTO tier is left out here, which is what this column has | |
| 392 | + // always done. SEO Health does include it, so the two surfaces can disagree | |
| 393 | + // about a post whose only value is `_metasync_metatitle`; carried forward | |
| 394 | + // unchanged rather than resolved as a side effect of consolidating. | |
| 395 | + $chain = Metasync_Seo_Precedence::chain( | |
| 396 | + $is_title | |
| 397 | + ? Metasync_Seo_Precedence::FIELD_TITLE | |
| 398 | + : Metasync_Seo_Precedence::FIELD_DESCRIPTION, | |
| 399 | + Metasync_Seo_Precedence::TYPE_POST, | |
| 400 | + array( | |
| 401 | + 'include_otto' => !$this->otto_disabled_for_post($post_id), | |
| 402 | + 'include_persisted_otto' => false, | |
| 403 | + ) | |
| 404 | + ); | |
| 387 | 405 | |
| 388 | 406 | $third_party = $this->get_active_plugin_meta_keys($is_title); |
| 389 | 407 | |
| 390 | 408 | // The legacy `meta_description` tag is only emitted when no active SEO plugin |
| @@ -416,18 +434,49 @@ | ||
| 416 | 434 | |
| 417 | 435 | /** |
| 418 | 436 | * Whether OTTO output is switched off for this specific post. |
| 419 | 437 | * |
| 420 | - * Matches Metasync_Otto_Frontend_Toolbar::is_otto_disabled() exactly — it treats | |
| 421 | - * only '1' and 'true' as disabled. A looser truthiness test would read the string | |
| 422 | - * 'false' as disabled and wrongly hide OTTO's value from the column while the | |
| 423 | - * front end still serves it. | |
| 438 | + * Delegates to Metasync_Otto_Frontend_Toolbar::is_otto_disabled(), which reports | |
| 439 | + * the effective status: the `_metasync_otto_disabled` meta flag OR a manual entry | |
| 440 | + * on the Compatibility page's "Excluded URLs" list. Both stop OTTO rendering, | |
| 441 | + * so both have to stop the column naming OTTO's value as the rendered one. | |
| 424 | 442 | * |
| 443 | + * The fallback repeats the meta test verbatim for the case where the toolbar class | |
| 444 | + * is not loaded. It treats only '1' and 'true' as disabled: a looser truthiness | |
| 445 | + * test would read the string 'false' as disabled and wrongly hide OTTO's value | |
| 446 | + * from the column while the front end still serves it. | |
| 447 | + * | |
| 425 | 448 | * @param int $post_id Post ID. |
| 426 | 449 | * @return bool |
| 427 | 450 | */ |
| 428 | 451 | private function otto_disabled_for_post($post_id) |
| 429 | 452 | { |
| 453 | + // Quick Edit saves over admin-ajax as `inline-save`, and wp_ajax_inline_save() | |
| 454 | + // redraws this row. metasync.php skips otto/otto_pixel.php for any admin-ajax | |
| 455 | + // action that is not ours, so the exclusion checker is absent in exactly the | |
| 456 | + // request that re-renders the column — and the row would show OTTO's value for | |
| 457 | + // an excluded post until the next full page load. Load it here, where the | |
| 458 | + // column is actually being rendered, rather than widening the load for every | |
| 459 | + // third-party ajax request: this runs only inside a column render callback, | |
| 460 | + // so a Quick Edit save pays for it and the frequent ajax actions the guard in | |
| 461 | + // metasync.php exists for (heartbeat, autosave) never reach this line. | |
| 462 | + // | |
| 463 | + // Gated on wp_doing_ajax() rather than the DOING_AJAX constant that | |
| 464 | + // metasync_is_non_metasync_admin_ajax() reads. The two can in principle | |
| 465 | + // disagree — wp_doing_ajax() is filterable — but it is the sanctioned API | |
| 466 | + // and the one PHPStan's WordPress ruleset requires, and a plugin filtering | |
| 467 | + // it to false mid-Quick-Edit would only cost this column its freshness. | |
| 468 | + if (!function_exists('metasync_is_otto_url_manually_excluded') && wp_doing_ajax()) { | |
| 469 | + $pixel_file = plugin_dir_path(dirname(__FILE__)) . 'otto/otto_pixel.php'; | |
| 470 | + if (file_exists($pixel_file)) { | |
| 471 | + require_once $pixel_file; | |
| 472 | + } | |
| 473 | + } | |
| 474 | + | |
| 475 | + if (class_exists('Metasync_Otto_Frontend_Toolbar')) { | |
| 476 | + return (bool) Metasync_Otto_Frontend_Toolbar::is_otto_disabled($post_id); | |
| 477 | + } | |
| 478 | + | |
| 430 | 479 | $disabled = get_post_meta($post_id, '_metasync_otto_disabled', true); |
| 431 | 480 | return $disabled === '1' || $disabled === 'true'; |
| 432 | 481 | } |
| 433 | 482 | |