| @@ -463,8 +463,54 @@ | ||
| 463 | 463 | return ''; |
| 464 | 464 | } |
| 465 | 465 | |
| 466 | 466 | /** |
| 467 | + * The WebSite SearchAction for the sitelinks search box, or null when the | |
| 468 | + * site switched it off. | |
| 469 | + * | |
| 470 | + * Reads the Website tab's two settings. website_enable_search absent (or | |
| 471 | + * null) means not configured, which stays on; any other value is read as | |
| 472 | + * a boolean, so the '' a switched-off toggle is stored as turns it off. | |
| 473 | + * | |
| 474 | + * website_search_url is used as the urlTemplate when it is an http(s) URL, | |
| 475 | + * or a site path, and carries the {search_term_string} placeholder that | |
| 476 | + * query-input names. Without the placeholder the template cannot take a | |
| 477 | + * query, so the default WordPress search URL is used instead, as it is | |
| 478 | + * when the field is empty. | |
| 479 | + * | |
| 480 | + * @since 2.15.0 | |
| 481 | + * | |
| 482 | + * @param array $settings Schema settings or site_data. | |
| 483 | + * @return array|null SearchAction node, or null when switched off. | |
| 484 | + */ | |
| 485 | + public static function website_search_action(array $settings): ?array { | |
| 486 | + $enabled = $settings['website_enable_search'] ?? null; | |
| 487 | + if (null !== $enabled && empty($enabled)) { | |
| 488 | + return null; | |
| 489 | + } | |
| 490 | + | |
| 491 | + $url_template = home_url('/?s={search_term_string}'); | |
| 492 | + | |
| 493 | + $custom = trim((string) ($settings['website_search_url'] ?? '')); | |
| 494 | + if ('' !== $custom && false !== strpos($custom, '{search_term_string}')) { | |
| 495 | + if (\ThinkRank\Core\Url_Validator::is_http_url($custom)) { | |
| 496 | + $url_template = $custom; | |
| 497 | + } elseif (\ThinkRank\Core\Url_Validator::is_http_url_or_path($custom)) { | |
| 498 | + $url_template = home_url($custom); | |
| 499 | + } | |
| 500 | + } | |
| 501 | + | |
| 502 | + return [ | |
| 503 | + '@type' => 'SearchAction', | |
| 504 | + 'target' => [ | |
| 505 | + '@type' => 'EntryPoint', | |
| 506 | + 'urlTemplate' => $url_template | |
| 507 | + ], | |
| 508 | + 'query-input' => 'required name=search_term_string' | |
| 509 | + ]; | |
| 510 | + } | |
| 511 | + | |
| 512 | + /** | |
| 467 | 513 | * Populate Organization schema |
| 468 | 514 | * PRESERVED: Exact same method logic from original Schema_Generator |
| 469 | 515 | * |
| 470 | 516 | * @since 1.0.0 |
| @@ -493,11 +539,18 @@ | ||
| 493 | 539 | $data['url'] ?? null, |
| 494 | 540 | home_url() |
| 495 | 541 | ); |
| 496 | 542 | |
| 497 | - // Logo from user configuration or theme customizer | |
| 498 | - if (!empty($data['site_data']['organization_logo'])) { | |
| 499 | - $schema['logo'] = $this->format_image_schema($data['site_data']['organization_logo']); | |
| 543 | + // Logo: the Schema Manager's Organization logo, then the Site Identity | |
| 544 | + // logo (the one WebSite.publisher and LocalBusiness already use, so the | |
| 545 | + // same entity carries the same logo everywhere), then the theme's | |
| 546 | + // custom logo. With none of them, logo is left out (#1010). | |
| 547 | + $logo_url = $this->first_non_empty( | |
| 548 | + $data['site_data']['organization_logo'] ?? null, | |
| 549 | + $data['site_data']['logo_url'] ?? null | |
| 550 | + ); | |
| 551 | + if ('' !== $logo_url) { | |
| 552 | + $schema['logo'] = $this->format_image_schema($logo_url); | |
| 500 | 553 | } else { |
| 501 | 554 | // Fallback to WordPress custom logo |
| 502 | 555 | $custom_logo_id = get_theme_mod('custom_logo'); |
| 503 | 556 | if ($custom_logo_id) { |
| @@ -924,9 +977,9 @@ | ||
| 924 | 977 | 'organization_social_telegram' |
| 925 | 978 | ]; |
| 926 | 979 | |
| 927 | 980 | foreach ($social_fields as $field) { |
| 928 | - if (!empty($settings[$field]) && filter_var($settings[$field], FILTER_VALIDATE_URL)) { | |
| 981 | + if (!empty($settings[$field]) && \ThinkRank\Core\Url_Validator::is_valid($settings[$field])) { | |
| 929 | 982 | $social_profiles[] = $settings[$field]; |
| 930 | 983 | } |
| 931 | 984 | } |
| 932 | 985 | |
| @@ -1130,22 +1183,14 @@ | ||
| 1130 | 1183 | } |
| 1131 | 1184 | |
| 1132 | 1185 | $schema['publisher'] = $publisher; |
| 1133 | 1186 | |
| 1134 | - // Search action for sitelinks search box (optional but recommended) | |
| 1135 | - if ($data['site_data']['website_enable_search'] ?? true) { | |
| 1136 | - $search_url = $this->first_non_empty( | |
| 1137 | - $data['site_data']['website_search_url'] ?? '', | |
| 1138 | - home_url('/?s={search_term_string}') | |
| 1139 | - ); | |
| 1140 | - $schema['potentialAction'] = [ | |
| 1141 | - '@type' => 'SearchAction', | |
| 1142 | - 'target' => [ | |
| 1143 | - '@type' => 'EntryPoint', | |
| 1144 | - 'urlTemplate' => $search_url | |
| 1145 | - ], | |
| 1146 | - 'query-input' => 'required name=search_term_string' | |
| 1147 | - ]; | |
| 1187 | + // Search action for sitelinks search box (optional but recommended). | |
| 1188 | + // Shared with the live fallback in SEO_Manager so the deployed and | |
| 1189 | + // live WebSite nodes read the switch and the URL the same way (#1009). | |
| 1190 | + $search_action = self::website_search_action($data['site_data'] ?? []); | |
| 1191 | + if (null !== $search_action) { | |
| 1192 | + $schema['potentialAction'] = $search_action; | |
| 1148 | 1193 | } |
| 1149 | 1194 | |
| 1150 | 1195 | // Social media profiles (sameAs) |
| 1151 | 1196 | $social_profiles = $this->get_social_profiles(); |
| @@ -1598,14 +1643,9 @@ | ||
| 1598 | 1643 | $social_profiles, |
| 1599 | 1644 | static function ($url) { |
| 1600 | 1645 | return is_string($url) |
| 1601 | 1646 | && '' !== trim($url) |
| 1602 | - && filter_var($url, FILTER_VALIDATE_URL) | |
| 1603 | - && in_array( | |
| 1604 | - strtolower((string) wp_parse_url($url, PHP_URL_SCHEME)), | |
| 1605 | - ['http', 'https'], | |
| 1606 | - true | |
| 1607 | - ); | |
| 1647 | + && \ThinkRank\Core\Url_Validator::is_http_url($url); | |
| 1608 | 1648 | } |
| 1609 | 1649 | ))); |
| 1610 | 1650 | |
| 1611 | 1651 | if (!empty($social_profiles)) { |