Changeset 3708357
- Timestamp:
- 09/23/2026 02:48:32 AM (4 weeks ago)
- Location:
- sitehale/trunk
- Files:
-
- 6 edited
-
changelog.txt (modified) (1 diff)
-
includes/class-sitehale-seo-import-options.php (modified) (4 diffs)
-
includes/class-sitehale-seo-migration.php (modified) (1 diff)
-
includes/class-sitehale-seo-sitemap.php (modified) (1 diff)
-
readme.txt (modified) (2 diffs)
-
sitehale.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
sitehale/trunk/changelog.txt
r3707986 r3708357 3 3 readme.txt carries only recent releases: wp.org truncates a Changelog 4 4 section past 5,000 words, silently, at import. This file is the full 5 record, newest first (0.7.67 down to 0.1.0). 5 record, newest first (0.7.68 down to 0.1.0). 6 7 = 0.7.68 = 8 * Requests for an old `news-sitemap.xml` address now go to your current sitemap instead of returning "not found". 9 * Imports existing SEO settings from two more plugins, Rank Math and SEOPress. A site switching from either keeps its page titles, descriptions, social sharing text and search-engine settings, for posts, pages and category archives alike — and from either one, its title templates, verification codes, social profile links and business details — plus, from Rank Math, any FAQ or How-to information it had added. 6 10 7 11 = 0.7.67 = -
sitehale/trunk/includes/class-sitehale-seo-import-options.php
r3668728 r3708357 469 469 * @return array 470 470 */ 471 /** 472 * Rank Math's site-level titles/robots options → our slots. 473 * 474 * Shapes read off a live Rank Math 1.0.279 install, not from memory: 475 * `rank-math-options-titles` holds `title_separator`, `homepage_title`, 476 * `pt_<post_type>_title`/`_description`, `tax_<taxonomy>_title`/ 477 * `_description`, `author_archive_title`, `date_archive_title`, 478 * `search_title`, `404_title`, and the `noindex_*` toggles. 479 * 480 * ⚠️ ITS VARIABLES ARE SINGLE PERCENTS, and one of them is a trap: 481 * `%term%` is the taxonomy TITLE while `%term_description%` is its 482 * description, so a naive prefix match maps both to the same slot. The 483 * longest key is replaced first for that reason — the same care 484 * `map_yoast_slots` takes with `%%excerpt%%` / `%%excerpt_only%%`. 485 * 486 * Anything still carrying `%…%` after translation is REFUSED, never 487 * published: a template we cannot resolve renders the literal token to 488 * a crawler, which is the AIOSEO smart-tag lesson in a third syntax. 489 * 490 * @param array $titles `rank-math-options-titles`. 491 * @return array{templates:array,separator:string,robots_defaults:array,drops:array} 492 */ 493 public static function map_rankmath_slots( $titles ) { 494 $out = array( 'templates' => array(), 'separator' => '', 495 'robots_defaults' => array(), 'drops' => array() ); 496 if ( ! is_array( $titles ) || ! $titles ) { 497 return $out; 498 } 499 500 // Longest first: %term_description% must not be eaten by %term%. 501 $vocab = array( 502 '%term_description%' => '#taxonomy_description', 503 '%category_title%' => '#taxonomy_title', 504 '%search_query%' => '#search_term', 505 '%customterm%' => '#taxonomy_title', 506 '%sitedesc%' => '#tagline', 507 '%sitename%' => '#site_title', 508 '%excerpt%' => '#post_excerpt', 509 '%seo_title%' => '#post_title', 510 '%pt_plural%' => '#archive_title', 511 '%pt_single%' => '#archive_title', 512 '%category%' => '#taxonomy_title', 513 '%caption%' => '#attachment_caption', 514 '%term%' => '#taxonomy_title', 515 '%title%' => '#post_title', 516 '%name%' => '#author_name', 517 '%date%' => '#archive_date', 518 '%sep%' => '#separator_sa', 519 ); 520 521 $fixed = array( 522 'homepage_title' => 'site_title', 523 'homepage_description' => 'site_description', 524 'author_archive_title' => 'author_title', 525 'author_archive_description' => 'author_description', 526 'date_archive_title' => 'date_title', 527 'search_title' => 'search_title', 528 '404_title' => 'error_title', 529 ); 530 531 $translate = static function ( $tpl ) use ( $vocab ) { 532 // Pagination is ours to add, never theirs to carry. 533 $tpl = str_replace( array( '%page%', '%pagenumber%', '%pagetotal%' ), '', $tpl ); 534 $tpl = strtr( $tpl, $vocab ); 535 $tpl = trim( preg_replace( '/\s+/u', ' ', $tpl ) ); 536 // ⚠️ Still in Rank Math syntax = untranslatable. NOT a bare `%` 537 // test: "50% off" is a legitimate template literal. 538 return preg_match( '/%[a-z][a-z0-9_]*(\([^)]*\))?%/i', $tpl ) ? null : $tpl; 539 }; 540 541 foreach ( $titles as $key => $value ) { 542 if ( 'title_separator' === $key && is_string( $value ) && '' !== trim( $value ) ) { 543 $out['separator'] = self::separator( trim( $value ) ); 544 continue; 545 } 546 if ( ! is_string( $value ) || '' === trim( $value ) ) { 547 continue; 548 } 549 $slot = null; 550 if ( isset( $fixed[ $key ] ) ) { 551 $slot = $fixed[ $key ]; 552 } elseif ( preg_match( '/^pt_(.+)_title$/', $key, $m ) ) { 553 $slot = 'post' === $m[1] ? 'post_title' : 'post_title:' . $m[1]; 554 } elseif ( preg_match( '/^pt_(.+)_description$/', $key, $m ) ) { 555 $slot = 'post' === $m[1] ? 'post_description' : 'post_description:' . $m[1]; 556 } elseif ( preg_match( '/^tax_(.+)_title$/', $key, $m ) ) { 557 $slot = 'term_title:' . $m[1]; 558 } elseif ( preg_match( '/^tax_(.+)_description$/', $key, $m ) ) { 559 $slot = 'term_description:' . $m[1]; 560 } 561 if ( null === $slot ) { 562 continue; // Not a slot we render. Silence, not a drop. 563 } 564 $tpl = $translate( trim( $value ) ); 565 if ( null === $tpl ) { 566 $out['drops'][ 'template.' . $key ] = 567 'carries Rank Math variables with no equivalent here: ' . trim( $value ); 568 continue; 569 } 570 if ( '' !== $tpl ) { 571 $out['templates'][ $slot ] = $tpl; 572 } 573 } 574 575 // Robots defaults. TRUE FLAGS ONLY, and only the policies our head 576 // actually honours — `noindex_empty_taxonomies` has no equivalent 577 // (we do not render a term archive differently for being empty) and 578 // `disable_author_archives` REDIRECTS rather than noindexes, so 579 // importing it as noindex would state something the site does not do. 580 $policy = array( 581 'noindex_search' => 'noindex_search', 582 'noindex_archive_subpages' => 'noindex_paginated', 583 ); 584 foreach ( $policy as $theirs => $ours ) { 585 if ( isset( $titles[ $theirs ] ) && 'on' === strtolower( (string) $titles[ $theirs ] ) ) { 586 $out['robots_defaults'][ $ours ] = true; 587 } 588 } 589 590 // ⚠️ THE PER-TYPE ROBOTS NEED THEIR GATE READ TOO. Rank Math stores 591 // `<thing>_robots` as a list AND `<thing>_custom_robots` as on/off, 592 // and the list is inert while the gate is off — a fresh install ships 593 // `pt_attachment_robots: ['noindex']` with its gate ON and 594 // `pt_post_robots: ['index']` with its gate OFF. Reading the list 595 // alone would import directives the site does not serve, which is 596 // the defect this work has now hit three times. 597 // 598 // Only the two our head actually honours are carried: author and 599 // attachment archives. `date_archive_robots` and the per-taxonomy 600 // lists have no equivalent — our policy has no key for them — and a 601 // value stored where nothing reads it is a lie with a long fuse. 602 $gated = array( 603 'author' => 'noindex_author', 604 'pt_attachment' => 'noindex_attachment', 605 ); 606 foreach ( $gated as $theirs => $ours ) { 607 $on = isset( $titles[ $theirs . '_custom_robots' ] ) 608 && 'on' === strtolower( (string) $titles[ $theirs . '_custom_robots' ] ); 609 $list = isset( $titles[ $theirs . '_robots' ] ) && is_array( $titles[ $theirs . '_robots' ] ) 610 ? array_map( 'strtolower', $titles[ $theirs . '_robots' ] ) : array(); 611 if ( $on && in_array( 'noindex', $list, true ) ) { 612 $out['robots_defaults'][ $ours ] = true; 613 } 614 } 615 616 // The share-card shape is a site-level choice our store already has a 617 // slot for, and it is the difference a parity run sees on every page. 618 $card = isset( $titles['twitter_card_type'] ) ? (string) $titles['twitter_card_type'] : ''; 619 if ( in_array( $card, array( 'summary', 'summary_large_image' ), true ) ) { 620 $out['twitter_card'] = $card; 621 } 622 return $out; 623 } 624 625 /** 626 * Rank Math's knowledge-graph identity and social URLs. 627 * 628 * `knowledgegraph_type` is `person` or `company`; only a company is an 629 * Organization, and importing a person's name as an organization name 630 * would be a factual claim about the business (rule 3). 631 * 632 * @param array $titles `rank-math-options-titles`. 633 * @param array $general `rank-math-options-general`. 634 * @return array{profiles:array,organization:array,tags:array,drops:array} 635 */ 636 public static function map_rankmath_identity( $titles, $general ) { 637 $out = array( 'profiles' => array(), 'organization' => array(), 638 'tags' => array(), 'drops' => array() ); 639 $titles = is_array( $titles ) ? $titles : array(); 640 $general = is_array( $general ) ? $general : array(); 641 642 $type = strtolower( (string) ( isset( $titles['knowledgegraph_type'] ) ? $titles['knowledgegraph_type'] : '' ) ); 643 $name = trim( (string) ( isset( $titles['knowledgegraph_name'] ) ? $titles['knowledgegraph_name'] : '' ) ); 644 if ( 'company' === $type && '' !== $name ) { 645 $out['organization']['name'] = $name; 646 } elseif ( '' !== $name && 'person' === $type ) { 647 $out['drops']['organization.name'] = 648 'Rank Math calls this site a person, not a company — importing the ' 649 . 'name as an Organization would state something about the business'; 650 } 651 $logo = trim( (string) ( isset( $titles['knowledgegraph_logo'] ) ? $titles['knowledgegraph_logo'] : '' ) ); 652 if ( 'company' === $type && 0 === strpos( $logo, 'http' ) ) { 653 $out['organization']['logo'] = $logo; 654 } 655 656 // Social profile URLs. A handle where a URL belongs is refused for the 657 // same reason map_identity() refuses one: sameAs takes URLs, and 658 // guessing the domain is a claim about an account that may not be ours. 659 $profiles = array( 660 'social_url_facebook' => 'facebookPageUrl', 661 'social_url_instagram' => 'instagramUrl', 662 'social_url_linkedin' => 'linkedinUrl', 663 'social_url_youtube' => 'youtubeUrl', 664 'social_url_pinterest' => 'pinterestUrl', 665 ); 666 foreach ( $profiles as $theirs => $ours ) { 667 $value = trim( (string) ( isset( $titles[ $theirs ] ) ? $titles[ $theirs ] : '' ) ); 668 if ( '' === $value ) { 669 continue; 670 } 671 if ( 0 !== strpos( $value, 'http' ) ) { 672 $out['drops'][ 'profile.' . $ours ] = 'not an absolute URL: ' . $value; 673 continue; 674 } 675 $out['profiles'][ $ours ] = $value; 676 } 677 $handle = trim( (string) ( isset( $titles['twitter_author_names'] ) ? $titles['twitter_author_names'] : '' ) ); 678 if ( '' !== $handle ) { 679 $out['organization']['twitter_handle'] = '@' . ltrim( $handle, '@' ); 680 } 681 682 // Webmaster verification, same meta names map_verification() uses. 683 $verify = array( 684 'google_verify' => 'google-site-verification', 685 'bing_verify' => 'msvalidate.01', 686 'yandex_verify' => 'yandex-verification', 687 'baidu_verify' => 'baidu-site-verification', 688 'pinterest_verify' => 'p:domain_verify', 689 'norton_verify' => 'norton-safeweb-site-verification', 690 ); 691 foreach ( $verify as $theirs => $meta ) { 692 $value = trim( (string) ( isset( $general[ $theirs ] ) ? $general[ $theirs ] : '' ) ); 693 if ( '' !== $value ) { 694 $out['tags'][ $meta ] = $value; 695 } 696 } 697 return $out; 698 } 699 700 /** 701 * SEOPress's site-level options → our slots. 702 * 703 * Shapes read off a live SEOPress install, and the key names checked 704 * against its own source. Three options carry it: 705 * `seopress_titles_option_name` (separator, templates, noindex flags), 706 * `seopress_social_option_name` (knowledge graph, accounts, card) and 707 * `seopress_advanced_option_name` (verification codes). 708 * 709 * ⚠️ ITS TEMPLATES ARE NESTED, not flat: `single_titles[<post_type>]`, 710 * `tax_titles[<taxonomy>]` and `archive_titles[<post_type>]`, each an 711 * array of `title`/`description`. A flat scan finds the four archive 712 * templates and silently misses every per-type one — which is most of 713 * the site. 714 * 715 * ⚠️ AND ITS TAXONOMY TOKENS ARE PER-TAXONOMY: `%%_category_title%%` 716 * (leading underscore) and `%%tag_title%%`, not one generic term token. 717 * Ours is generic, so both map to `#taxonomy_title` — and the longest 718 * key is replaced first so `%%_category_description%%` is not eaten by 719 * `%%_category_title%%`'s prefix. 720 * 721 * @param array $titles `seopress_titles_option_name`. 722 * @return array{templates:array,separator:string,robots_defaults:array,drops:array} 723 */ 724 public static function map_seopress_slots( $titles ) { 725 $out = array( 'templates' => array(), 'separator' => '', 726 'robots_defaults' => array(), 'drops' => array() ); 727 if ( ! is_array( $titles ) || ! $titles ) { 728 return $out; 729 } 730 731 $vocab = array( 732 '%%_category_description%%' => '#taxonomy_description', 733 '%%_category_title%%' => '#taxonomy_title', 734 '%%term_description%%' => '#taxonomy_description', 735 '%%tag_description%%' => '#taxonomy_description', 736 '%%search_keywords%%' => '#search_term', 737 '%%archive_title%%' => '#archive_title', 738 '%%archive_date%%' => '#archive_date', 739 '%%post_excerpt%%' => '#post_excerpt', 740 '%%post_author%%' => '#author_name', 741 '%%cpt_plural%%' => '#archive_title', 742 '%%post_title%%' => '#post_title', 743 '%%term_title%%' => '#taxonomy_title', 744 '%%tag_title%%' => '#taxonomy_title', 745 '%%sitetitle%%' => '#site_title', 746 '%%tagline%%' => '#tagline', 747 '%%sep%%' => '#separator_sa', 748 ); 749 750 $translate = static function ( $tpl ) use ( $vocab ) { 751 $tpl = str_replace( array( '%%current_pagination%%', '%%page%%' ), '', (string) $tpl ); 752 $tpl = strtr( $tpl, $vocab ); 753 $tpl = trim( preg_replace( '/\s+/u', ' ', $tpl ) ); 754 // A dangling separator is what a stripped pagination token leaves 755 // behind ("%%cpt_plural%% %%current_pagination%% %%sep%%"). 756 $tpl = trim( preg_replace( '/(\s*#separator_sa\s*)+$/', '', $tpl ) ); 757 return preg_match( '/%%[^%]+%%/', $tpl ) ? null : $tpl; 758 }; 759 760 $take = function ( $slot, $raw, $key ) use ( &$out, $translate ) { 761 if ( ! is_string( $raw ) || '' === trim( $raw ) ) { 762 return; 763 } 764 $tpl = $translate( trim( $raw ) ); 765 if ( null === $tpl ) { 766 $out['drops'][ 'template.' . $key ] = 767 'carries SEOPress variables with no equivalent here: ' . trim( $raw ); 768 return; 769 } 770 if ( '' !== $tpl ) { 771 $out['templates'][ $slot ] = $tpl; 772 } 773 }; 774 775 if ( ! empty( $titles['seopress_titles_sep'] ) ) { 776 $out['separator'] = self::separator( trim( (string) $titles['seopress_titles_sep'] ) ); 777 } 778 $take( 'site_title', isset( $titles['seopress_titles_home_site_title'] ) ? $titles['seopress_titles_home_site_title'] : '', 'home_site_title' ); 779 $take( 'site_description', isset( $titles['seopress_titles_home_site_desc'] ) ? $titles['seopress_titles_home_site_desc'] : '', 'home_site_desc' ); 780 $take( 'author_title', isset( $titles['seopress_titles_archives_author_title'] ) ? $titles['seopress_titles_archives_author_title'] : '', 'author_title' ); 781 $take( 'date_title', isset( $titles['seopress_titles_archives_date_title'] ) ? $titles['seopress_titles_archives_date_title'] : '', 'date_title' ); 782 $take( 'search_title', isset( $titles['seopress_titles_archives_search_title'] ) ? $titles['seopress_titles_archives_search_title'] : '', 'search_title' ); 783 $take( 'error_title', isset( $titles['seopress_titles_archives_404_title'] ) ? $titles['seopress_titles_archives_404_title'] : '', '404_title' ); 784 785 $nested = array( 786 'seopress_titles_single_titles' => array( 'post_title', 'post_description' ), 787 'seopress_titles_tax_titles' => array( 'term_title', 'term_description' ), 788 'seopress_titles_archive_titles' => array( 'archive_title', 'archive_description' ), 789 ); 790 foreach ( $nested as $option_key => $slots ) { 791 if ( empty( $titles[ $option_key ] ) || ! is_array( $titles[ $option_key ] ) ) { 792 continue; 793 } 794 foreach ( $titles[ $option_key ] as $subtype => $pair ) { 795 if ( ! is_array( $pair ) ) { 796 continue; 797 } 798 foreach ( array( 'title' => 0, 'description' => 1 ) as $which => $i ) { 799 if ( ! isset( $pair[ $which ] ) ) { 800 continue; 801 } 802 $slot = $slots[ $i ]; 803 // `post` is the default slot; everything else is qualified, 804 // exactly as the AIOSEO and Yoast mappers do it. 805 if ( 'post_title' === $slot || 'post_description' === $slot ) { 806 $slot = ( 'post' === $subtype ) ? $slot : $slot . ':' . $subtype; 807 } else { 808 $slot = $slot . ':' . $subtype; 809 } 810 $take( $slot, $pair[ $which ], $option_key . '.' . $subtype . '.' . $which ); 811 } 812 } 813 } 814 815 // Noindex flags. '1' is on; only the policies our head honours are 816 // carried. `archives_date_noindex` has no key here, and storing it 817 // where nothing reads it would be a value that quietly means nothing. 818 $flags = array( 819 'seopress_titles_archives_author_noindex' => 'noindex_author', 820 'seopress_titles_attachments_noindex' => 'noindex_attachment', 821 'seopress_titles_archives_search_title_noindex' => 'noindex_search', 822 ); 823 foreach ( $flags as $theirs => $ours ) { 824 if ( isset( $titles[ $theirs ] ) && '1' === (string) $titles[ $theirs ] ) { 825 $out['robots_defaults'][ $ours ] = true; 826 } 827 } 828 return $out; 829 } 830 831 /** 832 * SEOPress's knowledge graph, social accounts, card shape and 833 * verification codes. 834 * 835 * @param array $social `seopress_social_option_name`. 836 * @param array $advanced `seopress_advanced_option_name`. 837 * @return array{profiles:array,organization:array,tags:array,twitter_card:string,drops:array} 838 */ 839 public static function map_seopress_identity( $social, $advanced ) { 840 $out = array( 'profiles' => array(), 'organization' => array(), 841 'tags' => array(), 'twitter_card' => '', 'drops' => array() ); 842 $social = is_array( $social ) ? $social : array(); 843 $advanced = is_array( $advanced ) ? $advanced : array(); 844 845 $type = (string) ( isset( $social['seopress_social_knowledge_type'] ) ? $social['seopress_social_knowledge_type'] : '' ); 846 $name = trim( (string) ( isset( $social['seopress_social_knowledge_name'] ) ? $social['seopress_social_knowledge_name'] : '' ) ); 847 // Person is not an Organization (rule 3, as for Rank Math). 848 if ( '' !== $name && '' !== $type && 'Person' !== $type ) { 849 $out['organization']['name'] = $name; 850 $logo = trim( (string) ( isset( $social['seopress_social_knowledge_img'] ) ? $social['seopress_social_knowledge_img'] : '' ) ); 851 if ( 0 === strpos( $logo, 'http' ) ) { 852 $out['organization']['logo'] = $logo; 853 } 854 } elseif ( '' !== $name && 'Person' === $type ) { 855 $out['drops']['organization.name'] = 856 'SEOPress calls this site a person, not an organization'; 857 } 858 859 $profiles = array( 860 'seopress_social_accounts_facebook' => 'facebookPageUrl', 861 'seopress_social_accounts_instagram' => 'instagramUrl', 862 'seopress_social_accounts_linkedin' => 'linkedinUrl', 863 'seopress_social_accounts_youtube' => 'youtubeUrl', 864 'seopress_social_accounts_pinterest' => 'pinterestUrl', 865 ); 866 foreach ( $profiles as $theirs => $ours ) { 867 $value = trim( (string) ( isset( $social[ $theirs ] ) ? $social[ $theirs ] : '' ) ); 868 if ( '' === $value ) { 869 continue; 870 } 871 if ( 0 !== strpos( $value, 'http' ) ) { 872 $out['drops'][ 'profile.' . $ours ] = 'not an absolute URL: ' . $value; 873 continue; 874 } 875 $out['profiles'][ $ours ] = $value; 876 } 877 // ⚠️ SEOPress stores the X account as a USERNAME, not a URL — its own 878 // field validates `^[A-Za-z0-9_]{1,15}$`. So this one is a handle by 879 // design and is stored as a handle, where a Rank Math or AIOSEO URL 880 // would be parsed down to one. 881 $handle = trim( (string) ( isset( $social['seopress_social_accounts_twitter'] ) ? $social['seopress_social_accounts_twitter'] : '' ) ); 882 if ( '' !== $handle ) { 883 $out['organization']['twitter_handle'] = '@' . ltrim( $handle, '@' ); 884 } 885 886 // 'large' is its own word for summary_large_image; anything else is 887 // the default card. 888 $size = strtolower( (string) ( isset( $social['seopress_social_twitter_card_img_size'] ) ? $social['seopress_social_twitter_card_img_size'] : '' ) ); 889 if ( 'large' === $size ) { 890 $out['twitter_card'] = 'summary_large_image'; 891 } elseif ( 'default' === $size ) { 892 $out['twitter_card'] = 'summary'; 893 } 894 895 $verify = array( 896 'seopress_advanced_advanced_google' => 'google-site-verification', 897 'seopress_advanced_advanced_bing' => 'msvalidate.01', 898 'seopress_advanced_advanced_yandex' => 'yandex-verification', 899 'seopress_advanced_advanced_pinterest' => 'p:domain_verify', 900 'seopress_advanced_advanced_baidu' => 'baidu-site-verification', 901 ); 902 foreach ( $verify as $theirs => $meta ) { 903 $value = trim( (string) ( isset( $advanced[ $theirs ] ) ? $advanced[ $theirs ] : '' ) ); 904 if ( '' !== $value ) { 905 $out['tags'][ $meta ] = $value; 906 } 907 } 908 return $out; 909 } 910 911 /** 912 * Is SEOPress the plugin actually describing this site's output? 913 * Same shape and reason as the other two predicates. 914 */ 915 public static function seopress_is_renderer() { 916 return defined( 'SEOPRESS_VERSION' ) 917 && ! defined( 'AIOSEO_VERSION' ) 918 && ! defined( 'WPSEO_VERSION' ) 919 && ! defined( 'RANK_MATH_VERSION' ); 920 } 921 471 922 public static function map_yoast_social( $social ) { 472 923 $out = array(); … … 959 1410 } 960 1411 1412 /** 1413 * Is Rank Math the plugin actually describing this site's output? 1414 * 1415 * Same shape as `yoast_is_renderer()` and for the same reason: a 1416 * deactivated plugin leaves every one of its options behind, so 1417 * "its settings exist" says nothing about what the site serves. The 1418 * constant is defined only while it is running. 1419 * 1420 * AIOSEO and Yoast both outrank it: on a site carrying two, the one 1421 * that renders is the one whose settings are real, and those two are 1422 * checked first everywhere else in this file. 1423 */ 1424 public static function rankmath_is_renderer() { 1425 return defined( 'RANK_MATH_VERSION' ) 1426 && ! defined( 'AIOSEO_VERSION' ) 1427 && ! defined( 'WPSEO_VERSION' ); 1428 } 1429 961 1430 public static function collect() { 962 1431 $read = static function ( $name ) { … … 1015 1484 } 1016 1485 1486 // ⚠️ RANK MATH'S SITE-LEVEL OPTIONS, on the same terms as Yoast's: 1487 // only where it is the one rendering. Its per-post meta has been 1488 // importable since the Sep 2026 work, and a cutover without these 1489 // leaves the site with its authored titles and NONE of the templates 1490 // that produced every other page's — the hollyfrost failure in a 1491 // different plugin's clothes. 1492 // 1493 // FILLS, never stomps: anything AIOSEO or Yoast already gave stands. 1494 $rm_slots = array( 'templates' => array(), 'separator' => '', 1495 'robots_defaults' => array(), 'drops' => array() ); 1496 $rm_identity = array( 'profiles' => array(), 'organization' => array(), 1497 'tags' => array(), 'drops' => array() ); 1498 if ( self::rankmath_is_renderer() ) { 1499 $rm_titles = $read( 'rank-math-options-titles' ); 1500 $rm_slots = self::map_rankmath_slots( $rm_titles ); 1501 $rm_identity = self::map_rankmath_identity( $rm_titles, $read( 'rank-math-options-general' ) ); 1502 foreach ( $rm_slots['templates'] as $slot => $tpl ) { 1503 if ( ! isset( $slots['templates'][ $slot ] ) ) { 1504 $slots['templates'][ $slot ] = $tpl; 1505 } 1506 } 1507 foreach ( $rm_slots['robots_defaults'] as $k => $v ) { 1508 if ( ! isset( $robots_policy[ $k ] ) ) { 1509 $robots_policy[ $k ] = $v; 1510 } 1511 } 1512 if ( ! empty( $rm_slots['twitter_card'] ) && ! isset( $social_policy['twitter_card'] ) ) { 1513 $social_policy['twitter_card'] = $rm_slots['twitter_card']; 1514 } 1515 foreach ( $rm_identity['profiles'] as $k => $v ) { 1516 if ( ! isset( $identity['profiles'][ $k ] ) ) { 1517 $identity['profiles'][ $k ] = $v; 1518 } 1519 } 1520 foreach ( $rm_identity['organization'] as $k => $v ) { 1521 if ( ! isset( $identity['organization'][ $k ] ) ) { 1522 $identity['organization'][ $k ] = $v; 1523 } 1524 } 1525 foreach ( $rm_identity['tags'] as $k => $v ) { 1526 if ( ! isset( $verification['tags'][ $k ] ) ) { 1527 $verification['tags'][ $k ] = $v; 1528 } 1529 } 1530 } 1531 1532 // SEOPress, on the same terms as the other two: only where it renders. 1533 $sp_slots = array( 'templates' => array(), 'separator' => '', 1534 'robots_defaults' => array(), 'drops' => array() ); 1535 $sp_identity = array( 'profiles' => array(), 'organization' => array(), 1536 'tags' => array(), 'twitter_card' => '', 'drops' => array() ); 1537 if ( self::seopress_is_renderer() ) { 1538 $sp_slots = self::map_seopress_slots( $read( 'seopress_titles_option_name' ) ); 1539 $sp_identity = self::map_seopress_identity( 1540 $read( 'seopress_social_option_name' ), 1541 $read( 'seopress_advanced_option_name' ) 1542 ); 1543 foreach ( $sp_slots['templates'] as $slot => $tpl ) { 1544 if ( ! isset( $slots['templates'][ $slot ] ) ) { 1545 $slots['templates'][ $slot ] = $tpl; 1546 } 1547 } 1548 foreach ( $sp_slots['robots_defaults'] as $k => $v ) { 1549 if ( ! isset( $robots_policy[ $k ] ) ) { 1550 $robots_policy[ $k ] = $v; 1551 } 1552 } 1553 foreach ( $sp_identity['profiles'] as $k => $v ) { 1554 if ( ! isset( $identity['profiles'][ $k ] ) ) { 1555 $identity['profiles'][ $k ] = $v; 1556 } 1557 } 1558 foreach ( $sp_identity['organization'] as $k => $v ) { 1559 if ( ! isset( $identity['organization'][ $k ] ) ) { 1560 $identity['organization'][ $k ] = $v; 1561 } 1562 } 1563 foreach ( $sp_identity['tags'] as $k => $v ) { 1564 if ( ! isset( $verification['tags'][ $k ] ) ) { 1565 $verification['tags'][ $k ] = $v; 1566 } 1567 } 1568 if ( '' !== $sp_identity['twitter_card'] && ! isset( $social_policy['twitter_card'] ) ) { 1569 $social_policy['twitter_card'] = $sp_identity['twitter_card']; 1570 } 1571 } 1572 1017 1573 $aioseo_sep = self::separator( 1018 1574 isset( $global['searchAppearance']['global']['separator'] ) … … 1062 1618 // which is right by luck on asmc (`sc-dash`) and wrong on any 1063 1619 // client who chose a pipe or a bullet. 1064 'separator' => ( '' !== $aioseo_sep ) ? $aioseo_sep : $yoast_slots['separator'], 1620 // First renderer with one wins, in the same order as everything else 1621 // here: AIOSEO, then Yoast, then Rank Math. Each branch above only 1622 // ran if that plugin is the one rendering, so at most one is set. 1623 'separator' => ( '' !== $aioseo_sep ) 1624 ? $aioseo_sep 1625 : ( '' !== $yoast_slots['separator'] 1626 ? $yoast_slots['separator'] 1627 : ( '' !== $rm_slots['separator'] ? $rm_slots['separator'] : $sp_slots['separator'] ) ), 1065 1628 'yoast_drops' => $yoast_slots['drops'], 1066 1629 'current' => get_option( SiteHale_SEO_Store::OPTION, array() ), -
sitehale/trunk/includes/class-sitehale-seo-migration.php
r3668728 r3708357 200 200 201 201 /** Sources this build has an importer for. Anything else has not been read. */ 202 const READ_SOURCES = array( 'aioseo', 'yoast', 'none' ); 202 /** 203 * Sources the importer may legitimately report having read. 204 * 205 * ⚠️ AN UNKNOWN SOURCE HOLDS THE GATE SHUT, which is the safe direction: 206 * the button deactivates the rival, so a source nobody has a reader for 207 * must never resolve to "migrated". 208 * 209 * Rank Math and SEOPress joined on 2026-09-22, and only once BOTH halves 210 * were read. Their per-post meta landed first and that was deliberately 211 * not enough — their taxonomy archives' titles and descriptions live in 212 * term meta (Yoast's in the `wpseo_taxonomy_meta` option), and a flip on 213 * a post-only import strips exactly those pages. The term reader is what 214 * earned them the place, which is why `is_clear()` still demands 215 * `terms` be `scanned` or `absent` alongside this list. 216 * 217 * @var string[] 218 */ 219 const READ_SOURCES = array( 'aioseo', 'yoast', 'rankmath', 'seopress', 'none' ); 203 220 204 221 /** -
sitehale/trunk/includes/class-sitehale-seo-sitemap.php
r3707986 r3708357 64 64 const RETIRED = array( 65 65 'sitemap_index.xml', 66 // Found by the 404 log on macsolutions within minutes of 0.7.67 67 // (Garret, 2026-09-22): something still asks for it and gets a 404. 68 // AIOSEO advertised it on kava too — the robots unit documents that 69 // it has 404'd since 2026-08-05. A news sitemap is a different KIND 70 // of sitemap, so the 301 goes to the index like every other name 71 // here: the index is true, and a 404 is not. 72 'news-sitemap.xml', 66 73 'sitemap-index.xml', 67 74 'sitemap.rss', -
sitehale/trunk/readme.txt
r3707986 r3708357 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0.7.6 77 Stable tag: 0.7.68 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 196 196 197 197 == Changelog == 198 199 = 0.7.68 = 200 * Requests for an old `news-sitemap.xml` address now go to your current sitemap instead of returning "not found". 201 * Imports existing SEO settings from two more plugins, Rank Math and SEOPress. A site switching from either keeps its page titles, descriptions, social sharing text and search-engine settings, for posts, pages and category archives alike — and from either one, its title templates, verification codes, social profile links and business details — plus, from Rank Math, any FAQ or How-to information it had added. 198 202 199 203 = 0.7.67 = -
sitehale/trunk/sitehale.php
r3707986 r3708357 4 4 * Plugin URI: https://sitehale.com/how-it-works/ 5 5 * Description: Free Site Checkup (cron, PHP errors, database, updates, admins, SEO basics, disk, mail) plus the companion to the SiteHale managed care service. Sends nothing anywhere until you click Connect. 6 * Version: 0.7.6 76 * Version: 0.7.68 7 7 * Requires at least: 5.7 8 8 * Requires PHP: 7.4 … … 30 30 } 31 31 32 define( 'SITEHALE_VERSION', '0.7.6 7' );32 define( 'SITEHALE_VERSION', '0.7.68' ); 33 33 define( 'SITEHALE_PLUGIN_FILE', __FILE__ ); 34 34 define( 'SITEHALE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)