Plugin Directory

Changeset 3789031


Ignore:
Timestamp:
11/07/2026 06:12:18 AM (10 hours ago)
Author:
jwum
Message:

1.0.124: delivery check reads the saved section

Location:
seobetter
Files:
10 edited
1 copied

Legend:

Unmodified
Added
Removed
  • seobetter/tags/1.0.124/includes/Cloud_API.php

    r3788942 r3789031  
    522522                if ( ( $resp['refunded'] ?? null ) === true ) { $fail['refunded'] = true; }
    523523                if ( ( $resp['counted'] ?? null ) === false ) { $fail['counted'] = false; }
     524                // 1.0.124 - the cloud refused a reused product job id (409 job_used): the writer moves on to a new one.
     525                if ( ( $resp['code'] ?? '' ) === 'job_used' ) { $fail['code'] = 'job_used'; }
    524526                if ( isset( $resp['cloud_cap'] ) ) { License_Manager::store_cloud_usage( (int) ( $resp['cloud_used'] ?? 0 ), (int) $resp['cloud_cap'] ); }
    525527            }
    … …  
    591593            return [ 'status' => 'ok', 'calls' => $json['calls'] ];
    592594        }
     595        // 1.0.124 - a reused product job id gets no prompt: say so, so the writer moves on to a new one.
     596        if ( ( $json['reason'] ?? '' ) === 'job_used' ) { return [ 'status' => 'error', 'reason' => 'job_used' ]; }
    593597        return [ 'status' => 'error', 'reason' => 'bad-response' ];
    594598    }
  • seobetter/tags/1.0.124/includes/Product_Facts.php

    r3788998 r3789031  
    3737
    3838    /** Delivery cost or free-delivery threshold, shown in words. */
    39     public const DELIVERY_COST_RE = '/\b(free|flat[- ]rate)\s+(shipping|delivery|postage)\b|\b(shipping|delivery|postage)\s*(from|costs?|is|of|:)?\s*(\p{Sc}|[A-Z]{3}\s?)?\d|\b(shipping|delivery|postage)\b[^.!?]{0,40}\b(over|above|on orders|orders over|spend)\b\s*(\p{Sc}|[A-Z]{3}\s?)?\d/iu';
     39    public const DELIVERY_COST_RE = '/\b(free|flat[- ]rate)\s+(shipping|delivery|postage)\b|\b(shipping|delivery|postage)\s*(from|costs?|is|of|:)?\s*(\p{Sc}|[A-Z]{3}\s?)?\d|\b(shipping|delivery|postage)\b[^.!?]{0,40}\b(over|above|on orders|orders over|spend)\b\s*(\p{Sc}|[A-Z]{3}\s?)?\d|\b(shipping|delivery|postage)\s*:[^!?\n]{0,40}?(\p{Sc}\s?\d|\b(?:AUD|USD|NZD|CAD|GBP|EUR)\s?\d)/iu';
    4040    /** Delivery time, shown in words. */
    41     public const DELIVERY_TIME_RE = '/\b(deliver(y|ed|s)?|ships?|shipped|dispatch(ed|es)?|arrives?|posted)\b[^.!?]{0,40}?\b(\d+\s*(-|to|\x{2013})\s*\d+|\d+)\s*(business\s+|working\s+)?(days?|hours?|weeks?)\b|\b(next|same)[- ]day\s+(delivery|dispatch|shipping)\b/iu';
     41    public const DELIVERY_TIME_RE = '/\b(deliver(y|ed|s)?|ships?|shipped|shipping|postage|dispatch(ed|es)?|arrives?|posted)\b[^.!?]{0,40}?\b(\d+\s*(-|to|\x{2013})\s*\d+|\d+)\s*(business\s+|working\s+)?(days?|hours?|weeks?)\b|\b(next|same)[- ]day\s+(delivery|dispatch|shipping)\b/iu';
    4242    /** A return policy, shown in words. */
    43     public const RETURNS_RE = '/\b\d+[- ]?(day|days)\s+(free\s+)?(returns?|refunds?|money[- ]back|exchanges?)\b|\b(free|easy|hassle[- ]free)\s+returns?\b|\breturns?\s+(policy|within|accepted|are free)\b|\brefund\s+(policy|within)\b|\bmoney[- ]back\s+guarantee\b|\breturn\s+it\s+within\b/iu';
     43    public const RETURNS_RE = '/\b\d+[- ]?(day|days)\s+(free\s+)?(returns?|refunds?|money[- ]back|exchanges?)\b|\b(free|easy|hassle[- ]free)\s+returns?\b|\breturns?\s+(policy|within|accepted|are free)\b|\brefund\s+(policy|within)\b|\bmoney[- ]back\s+guarantee\b|\breturn\s+it\s+within\b|\breturns?\s*:\s*\d+[- ]?(day|days)\b|\b\d+\s+days?\s+to\s+return\b|\breturn\s+within\s+\d+\s+days?\b/iu';
    4444
    4545    /** Whether $text states delivery (cost, threshold or time) and returns. */
    … …  
    4949            'returns'  => (bool) preg_match( self::RETURNS_RE, $text ),
    5050        ];
     51    }
     52
     53    /** 1.0.124 - the wrapper class of the SEOBetter Delivery and returns block the Pro writer saves. */
     54    public const DELIVERY_BLOCK_CLASS = 'seobetter-delivery-returns';
     55
     56    /**
     57     * 1.0.124 - what the SEOBetter Delivery and returns block in $html
     58     * states. The block is copied from the store's own Store policies (or
     59     * its WooCommerce shipping methods and returns page), one line per item,
     60     * so its lines count by their kind: a Shipping, Delivery or Delivery
     61     * options line is a delivery fact, a Returns line is the return policy.
     62     * Lines carry a class (sb-dr-shipping, sb-dr-delivery, sb-dr-options,
     63     * sb-dr-returns); a 2.0.20 block without classes counts by its labels or
     64     * by the delivery patterns.
     65     *
     66     * @return array{present:bool,delivery:bool,returns:bool}
     67     */
     68    public static function block_signals( string $html ): array {
     69        $out = [ 'present' => false, 'delivery' => false, 'returns' => false ];
     70        if ( stripos( $html, self::DELIVERY_BLOCK_CLASS ) === false ) { return $out; }
     71        if ( ! preg_match( '#<div\b[^>]*class=["\'][^"\']*\b' . self::DELIVERY_BLOCK_CLASS . '\b[^"\']*["\'][^>]*>([\s\S]*?)</ul>\s*</div>#i', $html, $m ) ) { return $out; }
     72        $out['present'] = true;
     73        preg_match_all( '#<li\b([^>]*)>([\s\S]*?)</li>#i', $m[1], $lis, PREG_SET_ORDER );
     74        foreach ( $lis as $li ) {
     75            $cls  = preg_match( '/class=["\']([^"\']*)/i', $li[1], $c ) ? strtolower( $c[1] ) : '';
     76            $text = trim( (string) preg_replace( '/\s+/u', ' ', html_entity_decode( strip_tags( $li[2] ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) );
     77            $sig  = self::delivery_signals( $text );
     78            // A line counts by its kind only with a real value after its label: a number, an
     79            // amount, days or a link. An empty line or a bare "Returns:" earns nothing.
     80            $value = trim( (string) preg_replace( '/^(shipping|delivery options|delivery|returns?)\s*:/i', '', $text ) );
     81            $real  = $value !== '' && ( (bool) preg_match( '/\d|\p{Sc}|\bdays?\b/iu', $value ) || (bool) preg_match( '#href=["\']https?://#i', $li[2] ) );
     82            if ( $real && ( preg_match( '/\bsb-dr-(shipping|delivery|options)\b/', $cls ) || preg_match( '/^(shipping|delivery|delivery options)\s*:/i', $text ) ) ) { $out['delivery'] = true; }
     83            if ( $real && ( str_contains( $cls, 'sb-dr-returns' ) || preg_match( '/^returns?\s*:/i', $text ) ) ) { $out['returns'] = true; }
     84            if ( $sig['delivery'] ) { $out['delivery'] = true; }
     85            if ( $sig['returns'] ) { $out['returns'] = true; }
     86        }
     87        return $out;
     88    }
     89
     90    /**
     91     * 1.0.124 - $html (a product description) without the elements a shopper
     92     * cannot see, by the same rules read_page() uses: hidden by an inline
     93     * style or the hidden attribute, unless it is collapsible UI (a tab,
     94     * accordion or details panel); scripts, styles and templates dropped.
     95     * Hidden delivery text earns nothing.
     96     */
     97    public static function visible_html( string $html ): string {
     98        if ( trim( $html ) === '' ) { return ''; }
     99        $doc = self::load_dom( '<html><body><div id="sb-visible-root">' . $html . '</div></body></html>' );
     100        if ( ! $doc ) {
     101            $clean = (string) preg_replace( '#<(script|style|noscript|template)\b[^>]*>.*?</\1>#is', ' ', $html );
     102            return (string) preg_replace( '#<(\w+)\b[^>]*(\shidden\b|style\s*=\s*["\'][^"\']*(display\s*:\s*none|visibility\s*:\s*hidden|font-size\s*:\s*0))[^>]*>.*?</\1>#is', ' ', $clean );
     103        }
     104        $xp = new \DOMXPath( $doc );
     105        foreach ( iterator_to_array( $xp->query( '//script|//style|//noscript|//template' ) ) as $n ) {
     106            if ( $n->parentNode ) { $n->parentNode->removeChild( $n ); }
     107        }
     108        $controlled = [];
     109        foreach ( $xp->query( '//*[@aria-controls]' ) as $el ) {
     110            foreach ( preg_split( '/\s+/', trim( (string) $el->getAttribute( 'aria-controls' ) ) ) ?: [] as $id ) {
     111                if ( $id !== '' ) { $controlled[ $id ] = true; }
     112            }
     113        }
     114        $hidden = [];
     115        foreach ( $xp->query( '//*[@style or @hidden]' ) as $el ) {
     116            if ( self::is_hidden( $el ) && ! self::is_collapsible( $el, $controlled ) ) { $hidden[] = $el; }
     117        }
     118        foreach ( $hidden as $el ) { if ( $el->parentNode ) { $el->parentNode->removeChild( $el ); } }
     119        $root = $doc->getElementById( 'sb-visible-root' );
     120        if ( ! $root ) { return ''; }
     121        $out = '';
     122        foreach ( $root->childNodes as $n ) { $out .= (string) $doc->saveHTML( $n ); }
     123        return $out;
     124    }
     125
     126    /** 1.0.124 - the plain visible text of $html (visible_html(), tags removed, entities decoded, blocks kept apart). */
     127    public static function visible_text( string $html ): string {
     128        $v = self::visible_html( $html );
     129        if ( $v === '' ) { return ''; }
     130        $v = (string) preg_replace( '#<(br|/p|/li|/div|/h[1-6]|/td|/th|/tr)\b[^>]*>#i', ' $0', $v );
     131        return trim( (string) preg_replace( '/\s+/u', ' ', html_entity_decode( strip_tags( $v ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) );
    51132    }
    52133
    … …  
    115196            'description_html'     => (string) $product->get_description(),
    116197            'short_description'    => self::plain( (string) $product->get_short_description() ),
     198            'short_description_html' => (string) $product->get_short_description(),
    117199            'cross_sells'          => count( array_filter( (array) $product->get_cross_sell_ids() ) ),
    118200            'upsells'              => count( array_filter( (array) $product->get_upsell_ids() ) ),
    … …  
    171253        if ( $html === '' ) { return null; }
    172254        $page = self::compact_page( self::read_page( $html ) );
     255        // 1.0.124 - the saved description has the SEOBetter Delivery and returns block and the page
     256        // does not (a builder template that ignores the description, or a full-page cache that
     257        // answered before its purge): flagged lacks_block and cached like any page for PAGE_TTL
     258        // under this modified time, so a score does not fetch the page every time. The delivery
     259        // check scores from the saved description in that case (Product_Scorer::delivery_visible).
     260        $saved = (string) ( $post->post_content ?? '' );
     261        if ( stripos( $saved, self::DELIVERY_BLOCK_CLASS ) !== false && stripos( $html, self::DELIVERY_BLOCK_CLASS ) === false ) {
     262            $page['lacks_block'] = true;
     263        }
    173264        set_transient( $key, $page, self::PAGE_TTL );
    174265        return $page;
    … …  
    222313            'sslverify'           => $own ? (bool) apply_filters( 'https_local_ssl_verify', false ) : true,
    223314            'user-agent'          => 'SEOBetter/' . $ver . ' (product score; ' . home_url( '/' ) . ')',
    224             'headers'             => [ 'Accept' => 'text/html' ],
     315            // 1.0.124 - ask every cache on the way (LiteSpeed, a CDN) for a fresh page.
     316            'headers'             => [ 'Accept' => 'text/html', 'Cache-Control' => 'no-cache', 'Pragma' => 'no-cache' ],
    225317            'limit_response_size' => self::PAGE_MAX_BYTES,
    226318        ] );
    … …  
    9351027            'availability' => '', 'availability_date' => '', 'condition' => '', 'sku' => '', 'gtin' => '', 'mpn' => '', 'brand' => '',
    9361028            'attributes' => [], 'attributes_visible' => 0, 'weight' => '', 'dimensions' => '', 'variations' => [], 'categories' => [], 'category_ids' => [],
    937             'images' => [], 'description' => '', 'description_html' => '', 'short_description' => '', 'cross_sells' => 0, 'upsells' => 0,
     1029            'images' => [], 'description' => '', 'description_html' => '', 'short_description' => '', 'short_description_html' => '', 'cross_sells' => 0, 'upsells' => 0,
    9381030            'review_count' => 0, 'rating' => 0.0, 'reviews_visible' => false, 'visible_review_count' => 0, 'modified' => '', 'sale_to' => '',
    9391031            'noindex' => false, 'schema_nodes' => [], 'visible_price' => null, 'visible_availability' => '', 'visible_text' => '',
  • seobetter/tags/1.0.124/includes/Product_Scorer.php

    r3788998 r3789031  
    772772        $max          = 8;
    773773        $not_rendered = ! $f['rendered'];
    774         $text         = $not_rendered ? trim( $f['description'] . ' ' . $f['short_description'] ) : (string) $f['visible_text'];
     774        // 1.0.124 - the saved description and short description count on a rendered page too: they
     775        // render on the product page (the Description tab is visible content for shoppers even when
     776        // collapsed), so a stale cached page or a theme that hides its tab panel never hides them.
     777        // Only their VISIBLE part counts (Product_Facts::visible_html(), read_page()'s hidden rules):
     778        // delivery text hidden by its own markup earns nothing.
     779        $vis_desc     = Product_Facts::visible_html( (string) ( $f['description_html'] ?? '' ) );
     780        $vis_short    = Product_Facts::visible_html( (string) ( $f['short_description_html'] ?? '' ) );
     781        // (A facts array with plain text only, and no HTML, is read as it is.)
     782        $own          = trim( ( trim( (string) ( $f['description_html'] ?? '' ) ) !== '' ? Product_Facts::visible_text( $vis_desc ) : (string) $f['description'] ) . ' '
     783            . ( trim( (string) ( $f['short_description_html'] ?? '' ) ) !== '' ? Product_Facts::visible_text( $vis_short ) : (string) $f['short_description'] ) );
     784        $text         = $not_rendered ? $own : trim( (string) $f['visible_text'] . ' ' . $own );
    775785        $sig          = Product_Facts::delivery_signals( $text );
     786        // The SEOBetter Delivery and returns block, copied from Store policies: each line by its kind.
     787        $blk          = Product_Facts::block_signals( $vis_desc );
    776788        // A rendered page: the signals were found on the FULL page text before the cache compacted it.
    777         $delivery     = $sig['delivery'] || ( ! $not_rendered && $f['delivery_found'] === true );
    778         $returns      = $sig['returns'] || ( ! $not_rendered && $f['returns_found'] === true );
     789        $delivery     = $sig['delivery'] || $blk['delivery'] || ( ! $not_rendered && $f['delivery_found'] === true );
     790        $returns      = $sig['returns'] || $blk['returns'] || ( ! $not_rendered && $f['returns_found'] === true );
    779791        $points       = ( $delivery ? 5 : ( $not_rendered ? 2.5 : 0 ) ) + ( $returns ? 3 : ( $not_rendered ? 1.5 : 0 ) );
    780792        $notes        = [];
    … …  
    958970            'id' => 0, 'type' => '', 'name' => '', 'price' => null, 'on_sale' => false, 'currency' => '', 'availability' => '', 'availability_date' => '',
    959971            'condition' => '', 'sku' => '', 'gtin' => '', 'mpn' => '', 'brand' => '', 'attributes' => [], 'attributes_visible' => 0, 'weight' => '',
    960             'dimensions' => '', 'variations' => [], 'categories' => [], 'category_ids' => [], 'images' => [], 'description' => '', 'description_html' => '',
     972            'dimensions' => '', 'variations' => [], 'categories' => [], 'category_ids' => [], 'images' => [], 'description' => '', 'description_html' => '', 'short_description_html' => '',
    961973            'short_description' => '', 'cross_sells' => 0, 'upsells' => 0, 'review_count' => 0, 'rating' => 0.0, 'reviews_visible' => false,
    962974            'visible_review_count' => 0, 'modified' => '', 'sale_to' => '', 'noindex' => false, 'schema_nodes' => [], 'visible_price' => null,
  • seobetter/tags/1.0.124/readme.txt

    r3788998 r3789031  
    55Requires at least: 6.0
    66Tested up to: 7.1
    7 Stable tag: 1.0.123
     7Stable tag: 1.0.124
    88Requires PHP: 8.0
    99License: GPLv2 or later
    … …  
    232232== Changelog ==
    233233
     234= 1.0.124 =
     235* Fixed: the product score's "Delivery and returns visible" check now counts the visible delivery and returns details in the saved product description, including the Delivery and returns section in a collapsed product tab, so a cached copy of the page or a template that leaves the description out no longer hides them. Text hidden from shoppers still earns nothing.
     236* Fixed: when the product page does not show the Delivery and returns section the saved description has, the result is kept for 10 minutes instead of fetching the page on every score, and the page check asks caches for a fresh copy.
     237* Fixed: more store wordings count as delivery and returns details, such as "Shipping: Flat rate $9.95", "Postage: 3-7 working days" and "Returns: 30 days".
     238
    234239= 1.0.123 =
    235240* Fixed: the product score's "how to use it" check now counts serving and feeding instructions, such as "give 1-2 sticks a day" or a feeding guide, and never counts a question.
    … …  
    326331== Upgrade Notice ==
    327332
     333= 1.0.124 =
     334The product score now counts the delivery and returns details in your saved product description, also inside a collapsed product tab.
     335
    328336= 1.0.123 =
    329337The product score now counts feeding and serving instructions as "how to use it" and rereads the page after a description is saved.
  • seobetter/tags/1.0.124/seobetter.php

    r3788998 r3789031  
    44 * Plugin URI: https://seobetter.com
    55 * Description: AI-powered content generation optimized for Google AI Overviews, ChatGPT, Perplexity, Gemini & more. Generate articles that AI models cite. Works alongside Yoast, RankMath, or AIOSEO.
    6  * Version: 1.0.123
     6 * Version: 1.0.124
    77 * Author: SEOBetter
    88 * License: GPLv2 or later
    … …  
    9090define( 'SEOBETTER_LOADED', __FILE__ );
    9191
    92 define( 'SEOBETTER_VERSION', '1.0.123' );
     92define( 'SEOBETTER_VERSION', '1.0.124' );
    9393
    9494// v424 — Founder $99/yr Stripe Payment Link (the live launch offer used by the
  • seobetter/trunk/includes/Cloud_API.php

    r3788942 r3789031  
    522522                if ( ( $resp['refunded'] ?? null ) === true ) { $fail['refunded'] = true; }
    523523                if ( ( $resp['counted'] ?? null ) === false ) { $fail['counted'] = false; }
     524                // 1.0.124 - the cloud refused a reused product job id (409 job_used): the writer moves on to a new one.
     525                if ( ( $resp['code'] ?? '' ) === 'job_used' ) { $fail['code'] = 'job_used'; }
    524526                if ( isset( $resp['cloud_cap'] ) ) { License_Manager::store_cloud_usage( (int) ( $resp['cloud_used'] ?? 0 ), (int) $resp['cloud_cap'] ); }
    525527            }
    … …  
    591593            return [ 'status' => 'ok', 'calls' => $json['calls'] ];
    592594        }
     595        // 1.0.124 - a reused product job id gets no prompt: say so, so the writer moves on to a new one.
     596        if ( ( $json['reason'] ?? '' ) === 'job_used' ) { return [ 'status' => 'error', 'reason' => 'job_used' ]; }
    593597        return [ 'status' => 'error', 'reason' => 'bad-response' ];
    594598    }
  • seobetter/trunk/includes/Product_Facts.php

    r3788998 r3789031  
    3737
    3838    /** Delivery cost or free-delivery threshold, shown in words. */
    39     public const DELIVERY_COST_RE = '/\b(free|flat[- ]rate)\s+(shipping|delivery|postage)\b|\b(shipping|delivery|postage)\s*(from|costs?|is|of|:)?\s*(\p{Sc}|[A-Z]{3}\s?)?\d|\b(shipping|delivery|postage)\b[^.!?]{0,40}\b(over|above|on orders|orders over|spend)\b\s*(\p{Sc}|[A-Z]{3}\s?)?\d/iu';
     39    public const DELIVERY_COST_RE = '/\b(free|flat[- ]rate)\s+(shipping|delivery|postage)\b|\b(shipping|delivery|postage)\s*(from|costs?|is|of|:)?\s*(\p{Sc}|[A-Z]{3}\s?)?\d|\b(shipping|delivery|postage)\b[^.!?]{0,40}\b(over|above|on orders|orders over|spend)\b\s*(\p{Sc}|[A-Z]{3}\s?)?\d|\b(shipping|delivery|postage)\s*:[^!?\n]{0,40}?(\p{Sc}\s?\d|\b(?:AUD|USD|NZD|CAD|GBP|EUR)\s?\d)/iu';
    4040    /** Delivery time, shown in words. */
    41     public const DELIVERY_TIME_RE = '/\b(deliver(y|ed|s)?|ships?|shipped|dispatch(ed|es)?|arrives?|posted)\b[^.!?]{0,40}?\b(\d+\s*(-|to|\x{2013})\s*\d+|\d+)\s*(business\s+|working\s+)?(days?|hours?|weeks?)\b|\b(next|same)[- ]day\s+(delivery|dispatch|shipping)\b/iu';
     41    public const DELIVERY_TIME_RE = '/\b(deliver(y|ed|s)?|ships?|shipped|shipping|postage|dispatch(ed|es)?|arrives?|posted)\b[^.!?]{0,40}?\b(\d+\s*(-|to|\x{2013})\s*\d+|\d+)\s*(business\s+|working\s+)?(days?|hours?|weeks?)\b|\b(next|same)[- ]day\s+(delivery|dispatch|shipping)\b/iu';
    4242    /** A return policy, shown in words. */
    43     public const RETURNS_RE = '/\b\d+[- ]?(day|days)\s+(free\s+)?(returns?|refunds?|money[- ]back|exchanges?)\b|\b(free|easy|hassle[- ]free)\s+returns?\b|\breturns?\s+(policy|within|accepted|are free)\b|\brefund\s+(policy|within)\b|\bmoney[- ]back\s+guarantee\b|\breturn\s+it\s+within\b/iu';
     43    public const RETURNS_RE = '/\b\d+[- ]?(day|days)\s+(free\s+)?(returns?|refunds?|money[- ]back|exchanges?)\b|\b(free|easy|hassle[- ]free)\s+returns?\b|\breturns?\s+(policy|within|accepted|are free)\b|\brefund\s+(policy|within)\b|\bmoney[- ]back\s+guarantee\b|\breturn\s+it\s+within\b|\breturns?\s*:\s*\d+[- ]?(day|days)\b|\b\d+\s+days?\s+to\s+return\b|\breturn\s+within\s+\d+\s+days?\b/iu';
    4444
    4545    /** Whether $text states delivery (cost, threshold or time) and returns. */
    … …  
    4949            'returns'  => (bool) preg_match( self::RETURNS_RE, $text ),
    5050        ];
     51    }
     52
     53    /** 1.0.124 - the wrapper class of the SEOBetter Delivery and returns block the Pro writer saves. */
     54    public const DELIVERY_BLOCK_CLASS = 'seobetter-delivery-returns';
     55
     56    /**
     57     * 1.0.124 - what the SEOBetter Delivery and returns block in $html
     58     * states. The block is copied from the store's own Store policies (or
     59     * its WooCommerce shipping methods and returns page), one line per item,
     60     * so its lines count by their kind: a Shipping, Delivery or Delivery
     61     * options line is a delivery fact, a Returns line is the return policy.
     62     * Lines carry a class (sb-dr-shipping, sb-dr-delivery, sb-dr-options,
     63     * sb-dr-returns); a 2.0.20 block without classes counts by its labels or
     64     * by the delivery patterns.
     65     *
     66     * @return array{present:bool,delivery:bool,returns:bool}
     67     */
     68    public static function block_signals( string $html ): array {
     69        $out = [ 'present' => false, 'delivery' => false, 'returns' => false ];
     70        if ( stripos( $html, self::DELIVERY_BLOCK_CLASS ) === false ) { return $out; }
     71        if ( ! preg_match( '#<div\b[^>]*class=["\'][^"\']*\b' . self::DELIVERY_BLOCK_CLASS . '\b[^"\']*["\'][^>]*>([\s\S]*?)</ul>\s*</div>#i', $html, $m ) ) { return $out; }
     72        $out['present'] = true;
     73        preg_match_all( '#<li\b([^>]*)>([\s\S]*?)</li>#i', $m[1], $lis, PREG_SET_ORDER );
     74        foreach ( $lis as $li ) {
     75            $cls  = preg_match( '/class=["\']([^"\']*)/i', $li[1], $c ) ? strtolower( $c[1] ) : '';
     76            $text = trim( (string) preg_replace( '/\s+/u', ' ', html_entity_decode( strip_tags( $li[2] ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) );
     77            $sig  = self::delivery_signals( $text );
     78            // A line counts by its kind only with a real value after its label: a number, an
     79            // amount, days or a link. An empty line or a bare "Returns:" earns nothing.
     80            $value = trim( (string) preg_replace( '/^(shipping|delivery options|delivery|returns?)\s*:/i', '', $text ) );
     81            $real  = $value !== '' && ( (bool) preg_match( '/\d|\p{Sc}|\bdays?\b/iu', $value ) || (bool) preg_match( '#href=["\']https?://#i', $li[2] ) );
     82            if ( $real && ( preg_match( '/\bsb-dr-(shipping|delivery|options)\b/', $cls ) || preg_match( '/^(shipping|delivery|delivery options)\s*:/i', $text ) ) ) { $out['delivery'] = true; }
     83            if ( $real && ( str_contains( $cls, 'sb-dr-returns' ) || preg_match( '/^returns?\s*:/i', $text ) ) ) { $out['returns'] = true; }
     84            if ( $sig['delivery'] ) { $out['delivery'] = true; }
     85            if ( $sig['returns'] ) { $out['returns'] = true; }
     86        }
     87        return $out;
     88    }
     89
     90    /**
     91     * 1.0.124 - $html (a product description) without the elements a shopper
     92     * cannot see, by the same rules read_page() uses: hidden by an inline
     93     * style or the hidden attribute, unless it is collapsible UI (a tab,
     94     * accordion or details panel); scripts, styles and templates dropped.
     95     * Hidden delivery text earns nothing.
     96     */
     97    public static function visible_html( string $html ): string {
     98        if ( trim( $html ) === '' ) { return ''; }
     99        $doc = self::load_dom( '<html><body><div id="sb-visible-root">' . $html . '</div></body></html>' );
     100        if ( ! $doc ) {
     101            $clean = (string) preg_replace( '#<(script|style|noscript|template)\b[^>]*>.*?</\1>#is', ' ', $html );
     102            return (string) preg_replace( '#<(\w+)\b[^>]*(\shidden\b|style\s*=\s*["\'][^"\']*(display\s*:\s*none|visibility\s*:\s*hidden|font-size\s*:\s*0))[^>]*>.*?</\1>#is', ' ', $clean );
     103        }
     104        $xp = new \DOMXPath( $doc );
     105        foreach ( iterator_to_array( $xp->query( '//script|//style|//noscript|//template' ) ) as $n ) {
     106            if ( $n->parentNode ) { $n->parentNode->removeChild( $n ); }
     107        }
     108        $controlled = [];
     109        foreach ( $xp->query( '//*[@aria-controls]' ) as $el ) {
     110            foreach ( preg_split( '/\s+/', trim( (string) $el->getAttribute( 'aria-controls' ) ) ) ?: [] as $id ) {
     111                if ( $id !== '' ) { $controlled[ $id ] = true; }
     112            }
     113        }
     114        $hidden = [];
     115        foreach ( $xp->query( '//*[@style or @hidden]' ) as $el ) {
     116            if ( self::is_hidden( $el ) && ! self::is_collapsible( $el, $controlled ) ) { $hidden[] = $el; }
     117        }
     118        foreach ( $hidden as $el ) { if ( $el->parentNode ) { $el->parentNode->removeChild( $el ); } }
     119        $root = $doc->getElementById( 'sb-visible-root' );
     120        if ( ! $root ) { return ''; }
     121        $out = '';
     122        foreach ( $root->childNodes as $n ) { $out .= (string) $doc->saveHTML( $n ); }
     123        return $out;
     124    }
     125
     126    /** 1.0.124 - the plain visible text of $html (visible_html(), tags removed, entities decoded, blocks kept apart). */
     127    public static function visible_text( string $html ): string {
     128        $v = self::visible_html( $html );
     129        if ( $v === '' ) { return ''; }
     130        $v = (string) preg_replace( '#<(br|/p|/li|/div|/h[1-6]|/td|/th|/tr)\b[^>]*>#i', ' $0', $v );
     131        return trim( (string) preg_replace( '/\s+/u', ' ', html_entity_decode( strip_tags( $v ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ) );
    51132    }
    52133
    … …  
    115196            'description_html'     => (string) $product->get_description(),
    116197            'short_description'    => self::plain( (string) $product->get_short_description() ),
     198            'short_description_html' => (string) $product->get_short_description(),
    117199            'cross_sells'          => count( array_filter( (array) $product->get_cross_sell_ids() ) ),
    118200            'upsells'              => count( array_filter( (array) $product->get_upsell_ids() ) ),
    … …  
    171253        if ( $html === '' ) { return null; }
    172254        $page = self::compact_page( self::read_page( $html ) );
     255        // 1.0.124 - the saved description has the SEOBetter Delivery and returns block and the page
     256        // does not (a builder template that ignores the description, or a full-page cache that
     257        // answered before its purge): flagged lacks_block and cached like any page for PAGE_TTL
     258        // under this modified time, so a score does not fetch the page every time. The delivery
     259        // check scores from the saved description in that case (Product_Scorer::delivery_visible).
     260        $saved = (string) ( $post->post_content ?? '' );
     261        if ( stripos( $saved, self::DELIVERY_BLOCK_CLASS ) !== false && stripos( $html, self::DELIVERY_BLOCK_CLASS ) === false ) {
     262            $page['lacks_block'] = true;
     263        }
    173264        set_transient( $key, $page, self::PAGE_TTL );
    174265        return $page;
    … …  
    222313            'sslverify'           => $own ? (bool) apply_filters( 'https_local_ssl_verify', false ) : true,
    223314            'user-agent'          => 'SEOBetter/' . $ver . ' (product score; ' . home_url( '/' ) . ')',
    224             'headers'             => [ 'Accept' => 'text/html' ],
     315            // 1.0.124 - ask every cache on the way (LiteSpeed, a CDN) for a fresh page.
     316            'headers'             => [ 'Accept' => 'text/html', 'Cache-Control' => 'no-cache', 'Pragma' => 'no-cache' ],
    225317            'limit_response_size' => self::PAGE_MAX_BYTES,
    226318        ] );
    … …  
    9351027            'availability' => '', 'availability_date' => '', 'condition' => '', 'sku' => '', 'gtin' => '', 'mpn' => '', 'brand' => '',
    9361028            'attributes' => [], 'attributes_visible' => 0, 'weight' => '', 'dimensions' => '', 'variations' => [], 'categories' => [], 'category_ids' => [],
    937             'images' => [], 'description' => '', 'description_html' => '', 'short_description' => '', 'cross_sells' => 0, 'upsells' => 0,
     1029            'images' => [], 'description' => '', 'description_html' => '', 'short_description' => '', 'short_description_html' => '', 'cross_sells' => 0, 'upsells' => 0,
    9381030            'review_count' => 0, 'rating' => 0.0, 'reviews_visible' => false, 'visible_review_count' => 0, 'modified' => '', 'sale_to' => '',
    9391031            'noindex' => false, 'schema_nodes' => [], 'visible_price' => null, 'visible_availability' => '', 'visible_text' => '',
  • seobetter/trunk/includes/Product_Scorer.php

    r3788998 r3789031  
    772772        $max          = 8;
    773773        $not_rendered = ! $f['rendered'];
    774         $text         = $not_rendered ? trim( $f['description'] . ' ' . $f['short_description'] ) : (string) $f['visible_text'];
     774        // 1.0.124 - the saved description and short description count on a rendered page too: they
     775        // render on the product page (the Description tab is visible content for shoppers even when
     776        // collapsed), so a stale cached page or a theme that hides its tab panel never hides them.
     777        // Only their VISIBLE part counts (Product_Facts::visible_html(), read_page()'s hidden rules):
     778        // delivery text hidden by its own markup earns nothing.
     779        $vis_desc     = Product_Facts::visible_html( (string) ( $f['description_html'] ?? '' ) );
     780        $vis_short    = Product_Facts::visible_html( (string) ( $f['short_description_html'] ?? '' ) );
     781        // (A facts array with plain text only, and no HTML, is read as it is.)
     782        $own          = trim( ( trim( (string) ( $f['description_html'] ?? '' ) ) !== '' ? Product_Facts::visible_text( $vis_desc ) : (string) $f['description'] ) . ' '
     783            . ( trim( (string) ( $f['short_description_html'] ?? '' ) ) !== '' ? Product_Facts::visible_text( $vis_short ) : (string) $f['short_description'] ) );
     784        $text         = $not_rendered ? $own : trim( (string) $f['visible_text'] . ' ' . $own );
    775785        $sig          = Product_Facts::delivery_signals( $text );
     786        // The SEOBetter Delivery and returns block, copied from Store policies: each line by its kind.
     787        $blk          = Product_Facts::block_signals( $vis_desc );
    776788        // A rendered page: the signals were found on the FULL page text before the cache compacted it.
    777         $delivery     = $sig['delivery'] || ( ! $not_rendered && $f['delivery_found'] === true );
    778         $returns      = $sig['returns'] || ( ! $not_rendered && $f['returns_found'] === true );
     789        $delivery     = $sig['delivery'] || $blk['delivery'] || ( ! $not_rendered && $f['delivery_found'] === true );
     790        $returns      = $sig['returns'] || $blk['returns'] || ( ! $not_rendered && $f['returns_found'] === true );
    779791        $points       = ( $delivery ? 5 : ( $not_rendered ? 2.5 : 0 ) ) + ( $returns ? 3 : ( $not_rendered ? 1.5 : 0 ) );
    780792        $notes        = [];
    … …  
    958970            'id' => 0, 'type' => '', 'name' => '', 'price' => null, 'on_sale' => false, 'currency' => '', 'availability' => '', 'availability_date' => '',
    959971            'condition' => '', 'sku' => '', 'gtin' => '', 'mpn' => '', 'brand' => '', 'attributes' => [], 'attributes_visible' => 0, 'weight' => '',
    960             'dimensions' => '', 'variations' => [], 'categories' => [], 'category_ids' => [], 'images' => [], 'description' => '', 'description_html' => '',
     972            'dimensions' => '', 'variations' => [], 'categories' => [], 'category_ids' => [], 'images' => [], 'description' => '', 'description_html' => '', 'short_description_html' => '',
    961973            'short_description' => '', 'cross_sells' => 0, 'upsells' => 0, 'review_count' => 0, 'rating' => 0.0, 'reviews_visible' => false,
    962974            'visible_review_count' => 0, 'modified' => '', 'sale_to' => '', 'noindex' => false, 'schema_nodes' => [], 'visible_price' => null,
  • seobetter/trunk/readme.txt

    r3788998 r3789031  
    55Requires at least: 6.0
    66Tested up to: 7.1
    7 Stable tag: 1.0.123
     7Stable tag: 1.0.124
    88Requires PHP: 8.0
    99License: GPLv2 or later
    … …  
    232232== Changelog ==
    233233
     234= 1.0.124 =
     235* Fixed: the product score's "Delivery and returns visible" check now counts the visible delivery and returns details in the saved product description, including the Delivery and returns section in a collapsed product tab, so a cached copy of the page or a template that leaves the description out no longer hides them. Text hidden from shoppers still earns nothing.
     236* Fixed: when the product page does not show the Delivery and returns section the saved description has, the result is kept for 10 minutes instead of fetching the page on every score, and the page check asks caches for a fresh copy.
     237* Fixed: more store wordings count as delivery and returns details, such as "Shipping: Flat rate $9.95", "Postage: 3-7 working days" and "Returns: 30 days".
     238
    234239= 1.0.123 =
    235240* Fixed: the product score's "how to use it" check now counts serving and feeding instructions, such as "give 1-2 sticks a day" or a feeding guide, and never counts a question.
    … …  
    326331== Upgrade Notice ==
    327332
     333= 1.0.124 =
     334The product score now counts the delivery and returns details in your saved product description, also inside a collapsed product tab.
     335
    328336= 1.0.123 =
    329337The product score now counts feeding and serving instructions as "how to use it" and rereads the page after a description is saved.
  • seobetter/trunk/seobetter.php

    r3788998 r3789031  
    44 * Plugin URI: https://seobetter.com
    55 * Description: AI-powered content generation optimized for Google AI Overviews, ChatGPT, Perplexity, Gemini & more. Generate articles that AI models cite. Works alongside Yoast, RankMath, or AIOSEO.
    6  * Version: 1.0.123
     6 * Version: 1.0.124
    77 * Author: SEOBetter
    88 * License: GPLv2 or later
    … …  
    9090define( 'SEOBETTER_LOADED', __FILE__ );
    9191
    92 define( 'SEOBETTER_VERSION', '1.0.123' );
     92define( 'SEOBETTER_VERSION', '1.0.124' );
    9393
    9494// v424 — Founder $99/yr Stripe Payment Link (the live launch offer used by the
Note: See TracChangeset for help on using the changeset viewer.