Plugin Directory

Changeset 3705859


Ignore:
Timestamp:
09/21/2026 03:23:50 PM (4 weeks ago)
Author:
sitehale
Message:

SiteHale 0.7.66

Location:
sitehale/trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • sitehale/trunk/changelog.txt

    r3699407 r3705859  
    33readme.txt carries only recent releases: wp.org truncates a Changelog
    44section past 5,000 words, silently, at import. This file is the full
    5 record, newest first (0.7.65 down to 0.1.0).
     5record, newest first (0.7.66 down to 0.1.0).
     6
     7= 0.7.66 =
     8* Fixes your pages' search-engine details going missing after a plugin change. Before SiteHale describes your pages to search engines, it checks that no other plugin is already doing so, and it repeats that check whenever the site changes. It re-ran the check by itself only after a SiteHale update. After you switched another plugin on or off, or when the check expired after 30 days, your pages had no custom titles, descriptions or social-sharing details until enough visitors had arrived to complete the check. On a quiet site that could take days. SiteHale now finishes the check itself as soon as it's needed. Sites where another plugin describes your pages, and sites that have SiteHale's search-engine features switched off, are unaffected.
    69
    710= 0.7.65 =
  • sitehale/trunk/includes/class-sitehale-seo-guard.php

    r3676052 r3705859  
    300300                // response has already gone to the browser so the loopbacks cost the
    301301                // visitor nothing.
     302                self::schedule_warm();
     303        }
     304
     305        /**
     306         * Queue the warm loopbacks for this request's `shutdown`.
     307         *
     308         * The ONE place a warm is dispatched from, whichever event asked for it.
     309         * Both callers need the same deferral (see above) and the same callback, and
     310         * WordPress collapses an identical callback at an identical priority into a
     311         * single registration, so a request that trips both triggers — a SiteHale
     312         * update changes the fingerprint too — still sends one set of loopbacks.
     313         */
     314        private static function schedule_warm() {
    302315                if ( class_exists( 'SiteHale_SEO_Handover' ) ) {
    303316                        add_action( 'shutdown', array( 'SiteHale_SEO_Handover', 'warm_guard' ), 5 );
     317                }
     318        }
     319
     320        /** Last dark-state warm: array( 'fp' => fingerprint, 'at' => unix time ). */
     321        const OPT_LAST_REWARM = 'sitehale_seo_guard_rewarm';
     322
     323        /** Minimum gap between retries while the SAME dark state persists. */
     324        const REWARM_RETRY = 21600; // 6 hours.
     325
     326        /**
     327         * Whether a dark guard should send the warm loopbacks now. PURE.
     328         *
     329         * 🔴 THE VERSION TRIGGER ABOVE COVERED ONE CAUSE OF A DARK HEAD OUT OF FOUR.
     330         * `is_fresh()` discards the evidence when the plugin set changes, when the
     331         * theme changes, when our version changes, and when it is 30 days old. Only
     332         * the third ever woke the module. Found 2026-09-21 retiring cognihance's
     333         * store: deactivating nine WooCommerce plugins took the title, description,
     334         * OpenGraph and JSON-LD off every page, and the guard read
     335         * "evidence is stale — the plugin set, theme or plugin version changed".
     336         * Nothing was going to bring them back but visitors, and nothing reads the
     337         * rendered head to notice they were gone. The 30-day TTL does the same to
     338         * EVERY client once a month, with nobody touching anything.
     339         *
     340         * So this keys on the STATE, not on a list of causes: an undecided guard is
     341         * a dark head whatever made it undecided, and a cause nobody has thought of
     342         * yet is covered without anyone having to think of it.
     343         *
     344         *   clear     never — the head is rendering.
     345         *   conflict  never — dark ON PURPOSE; a rival owns the head, and warming
     346         *             cannot change that (the fingerprint notices when it goes).
     347         *   unknown   at once if the site has changed since the last warm, so a
     348         *             deactivation is answered on the very next request; otherwise
     349         *             no more often than REWARM_RETRY, so a site that can never
     350         *             reach `clear` — a one-page site with no singular URL, a host
     351         *             that refuses its own loopbacks — costs four loopbacks every
     352         *             six hours rather than four on every page view.
     353         *
     354         * @param string $state       Guard verdict: clear|conflict|unknown.
     355         * @param string $fingerprint The site's fingerprint now.
     356         * @param mixed  $last        Stored OPT_LAST_REWARM value (any shape).
     357         * @param int    $now         Unix time.
     358         * @param int    $retry       Seconds between retries of an unchanged site.
     359         * @return bool
     360         */
     361        public static function rewarm_due( $state, $fingerprint, $last, $now, $retry = self::REWARM_RETRY ) {
     362                if ( 'unknown' !== $state ) {
     363                        return false;
     364                }
     365                // Malformed or absent history is "never warmed", not a reason to wait.
     366                if ( ! is_array( $last ) || ! isset( $last['fp'], $last['at'] ) ) {
     367                        return true;
     368                }
     369                if ( (string) $last['fp'] !== (string) $fingerprint ) {
     370                        return true;
     371                }
     372                return ( (int) $now - (int) $last['at'] ) >= (int) $retry;
     373        }
     374
     375        /**
     376         * Send the warm loopbacks when the guard is undecided. See rewarm_due().
     377         *
     378         * ⚠️ NEVER UNDER WP-CLI. `--skip-plugins` and `--skip-themes` filter
     379         * `active_plugins` and `stylesheet` inside the CLI process, so the
     380         * fingerprint a CLI command computes is not the site's. Recording it would
     381         * flip-flop against every web request — and this repo's own automation
     382         * passes skip flags — firing a spurious warm on both sides of each CLI call,
     383         * and a read-only command such as `plugin list` would start writing options.
     384         * A change made FROM the CLI is still answered: the next web request sees
     385         * the fingerprint it did not record and warms from there.
     386         *
     387         * ⚠️ WRITTEN BEFORE DISPATCH, for the same reason as the version option: the
     388         * loopbacks are themselves requests to this site, still undecided, and they
     389         * must find this warm already recorded or each would dispatch another.
     390         */
     391        private static function maybe_rewarm_when_dark() {
     392                if ( defined( 'WP_CLI' ) && WP_CLI ) {
     393                        return;
     394                }
     395                // Only where a head is WANTED: warming a site that renders nothing by
     396                // design restores nothing. See SiteHale_SEO_Head::wants_render().
     397                if ( ! class_exists( 'SiteHale_SEO_Head' ) || ! SiteHale_SEO_Head::wants_render() ) {
     398                        return;
     399                }
     400                $state = self::state();
     401                if ( 'unknown' !== $state['state'] ) {
     402                        return; // The common case costs no query beyond the verdict itself.
     403                }
     404                $fp   = self::fingerprint();
     405                $now  = time();
     406                $last = get_option( self::OPT_LAST_REWARM, array() );
     407                if ( ! self::rewarm_due( $state['state'], $fp, $last, $now ) ) {
     408                        return;
     409                }
     410                update_option( self::OPT_LAST_REWARM, array( 'fp' => $fp, 'at' => $now ), false );
     411                self::schedule_warm();
     412        }
     413
     414        /**
     415         * A decided verdict ends the dark episode, so it ends the warm history.
     416         *
     417         * Called from store(), the one place evidence is written — which only runs
     418         * while the guard is still observing, so this costs nothing on a site that
     419         * is simply rendering. It reads the verdict to decide; it never persists
     420         * one (the stored record stays evidence, not a conclusion).
     421         *
     422         * ⚠️ rewarm_due() waits out REWARM_RETRY when the fingerprint matches the
     423         * last warm — right within ONE episode, wrong across two. Measured on the
     424         * testbox 2026-09-21: a plugin activated then deactivated returns the site
     425         * to the fingerprint of a warm from twenty minutes earlier, the evidence
     426         * was discarded in between, and the head stayed dark with the retry window
     427         * reading "already tried". Plugin off, plugin back on is the most ordinary
     428         * troubleshooting there is. Clearing here means every new episode warms at
     429         * once, whatever caused it. `conflict` counts as decided too: a rival
     430         * toggled on and back off is the same trap from the other side.
     431         *
     432         * @param array $rec The evidence record just written.
     433         */
     434        private static function end_episode_if_decided( $rec ) {
     435                $decided = self::verdict(
     436                        isset( $rec['contexts'] ) ? $rec['contexts'] : array(),
     437                        isset( $rec['rival'] ) ? $rec['rival'] : ''
     438                );
     439                if ( 'unknown' !== $decided['state'] ) {
     440                        delete_option( self::OPT_LAST_REWARM );
    304441                }
    305442        }
    … …  
    310447                }
    311448                self::maybe_reobserve_after_update();
     449                self::maybe_rewarm_when_dark();
    312450                // Outside every other emitter. `sitehale-term-seo.php` opens its own
    313451                // buffer at priority 1 and closes it at 999; ours must enclose that
    … …  
    725863                // belongs in the autoload cache.
    726864                update_option( self::OPTION, $rec, true );
     865
     866                // Ends the dark episode's warm history once the evidence decides.
     867                self::end_episode_if_decided( $rec );
    727868        }
    728869}
  • sitehale/trunk/includes/class-sitehale-seo-head.php

    r3699407 r3705859  
    165165                        return false;
    166166                }
    167                 $opts = get_option( SiteHale_SEO_Store::OPTION, array() );
    168                 // An explicit stored value always wins, in both directions. Absent, the
    169                 // default depends on who is running this site — see renders_by_default().
    170                 $on = isset( $opts['render'] ) ? (bool) $opts['render'] : self::renders_by_default();
     167                $on = self::wants_render();
    171168                if ( $on && ! SiteHale_SEO_Guard::clear() ) {
    172169                        $on = false;
    … …  
    178175                 */
    179176                return (bool) apply_filters( 'sitehale_seo_render', $on );
     177        }
     178
     179        /**
     180         * Whether this site WANTS its head rendered, before the guard has its say.
     181         *
     182         * Split out of should_render() so the guard can ask it: a dark guard is only
     183         * worth waking on a site that would render once it is clear. A free
     184         * wordpress.org install renders nothing by design, so there is no head to
     185         * restore and its footprint stays what it was — a warm on update, no more.
     186         *
     187         * @return bool
     188         */
     189        public static function wants_render() {
     190                if ( ! SiteHale_SEO_Store::enabled() ) {
     191                        return false;
     192                }
     193                $opts = get_option( SiteHale_SEO_Store::OPTION, array() );
     194                // An explicit stored value always wins, in both directions. Absent, the
     195                // default depends on who is running this site — see renders_by_default().
     196                return isset( $opts['render'] ) ? (bool) $opts['render'] : self::renders_by_default();
    180197        }
    181198
  • sitehale/trunk/readme.txt

    r3699407 r3705859  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.7.65
     7Stable tag: 0.7.66
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    195195== Changelog ==
    196196
     197= 0.7.66 =
     198* Fixes your pages' search-engine details going missing after a plugin change. Before SiteHale describes your pages to search engines, it checks that no other plugin is already doing so, and it repeats that check whenever the site changes. It re-ran the check by itself only after a SiteHale update. After you switched another plugin on or off, or when the check expired after 30 days, your pages had no custom titles, descriptions or social-sharing details until enough visitors had arrived to complete the check. On a quiet site that could take days. SiteHale now finishes the check itself as soon as it's needed. Sites where another plugin describes your pages, and sites that have SiteHale's search-engine features switched off, are unaffected.
     199
    197200= 0.7.65 =
    198201* Keeps your RSS feeds out of Google's index again. The SEO plugin SiteHale replaced told search engines not to index feeds, and SiteHale did not carry that over. Every post and product has its own comments feed, so Google began finding feed copies of your pages and reporting them in Search Console as "Duplicate without user-selected canonical". SiteHale now sends the same noindex instruction on every feed that the previous plugin did. Your pages, and the feeds themselves, are unchanged for readers and feed apps.
  • sitehale/trunk/sitehale.php

    r3699407 r3705859  
    44 * Plugin URI:        https://sitehale.com/how-it-works/
    55 * 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.65
     6 * Version:           0.7.66
    77 * Requires at least: 5.7
    88 * Requires PHP:      7.4
    … …  
    3030}
    3131
    32 define( 'SITEHALE_VERSION', '0.7.65' );
     32define( 'SITEHALE_VERSION', '0.7.66' );
    3333define( 'SITEHALE_PLUGIN_FILE', __FILE__ );
    3434define( 'SITEHALE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
Note: See TracChangeset for help on using the changeset viewer.