Changeset 3789031
- Timestamp:
- 11/07/2026 06:12:18 AM (10 hours ago)
- Location:
- seobetter
- Files:
-
- 10 edited
- 1 copied
-
tags/1.0.124 (copied) (copied from seobetter/trunk )
-
tags/1.0.124/includes/Cloud_API.php (modified) (2 diffs)
-
tags/1.0.124/includes/Product_Facts.php (modified) (6 diffs)
-
tags/1.0.124/includes/Product_Scorer.php (modified) (2 diffs)
-
tags/1.0.124/readme.txt (modified) (3 diffs)
-
tags/1.0.124/seobetter.php (modified) (2 diffs)
-
trunk/includes/Cloud_API.php (modified) (2 diffs)
-
trunk/includes/Product_Facts.php (modified) (6 diffs)
-
trunk/includes/Product_Scorer.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/seobetter.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
seobetter/tags/1.0.124/includes/Cloud_API.php
r3788942 r3789031 522 522 if ( ( $resp['refunded'] ?? null ) === true ) { $fail['refunded'] = true; } 523 523 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'; } 524 526 if ( isset( $resp['cloud_cap'] ) ) { License_Manager::store_cloud_usage( (int) ( $resp['cloud_used'] ?? 0 ), (int) $resp['cloud_cap'] ); } 525 527 } … … 591 593 return [ 'status' => 'ok', 'calls' => $json['calls'] ]; 592 594 } 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' ]; } 593 597 return [ 'status' => 'error', 'reason' => 'bad-response' ]; 594 598 } -
seobetter/tags/1.0.124/includes/Product_Facts.php
r3788998 r3789031 37 37 38 38 /** 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'; 40 40 /** 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'; 42 42 /** 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'; 44 44 45 45 /** Whether $text states delivery (cost, threshold or time) and returns. */ … … 49 49 'returns' => (bool) preg_match( self::RETURNS_RE, $text ), 50 50 ]; 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' ) ) ); 51 132 } 52 133 … … 115 196 'description_html' => (string) $product->get_description(), 116 197 'short_description' => self::plain( (string) $product->get_short_description() ), 198 'short_description_html' => (string) $product->get_short_description(), 117 199 'cross_sells' => count( array_filter( (array) $product->get_cross_sell_ids() ) ), 118 200 'upsells' => count( array_filter( (array) $product->get_upsell_ids() ) ), … … 171 253 if ( $html === '' ) { return null; } 172 254 $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 } 173 264 set_transient( $key, $page, self::PAGE_TTL ); 174 265 return $page; … … 222 313 'sslverify' => $own ? (bool) apply_filters( 'https_local_ssl_verify', false ) : true, 223 314 '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' ], 225 317 'limit_response_size' => self::PAGE_MAX_BYTES, 226 318 ] ); … … 935 1027 'availability' => '', 'availability_date' => '', 'condition' => '', 'sku' => '', 'gtin' => '', 'mpn' => '', 'brand' => '', 936 1028 '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, 938 1030 'review_count' => 0, 'rating' => 0.0, 'reviews_visible' => false, 'visible_review_count' => 0, 'modified' => '', 'sale_to' => '', 939 1031 'noindex' => false, 'schema_nodes' => [], 'visible_price' => null, 'visible_availability' => '', 'visible_text' => '', -
seobetter/tags/1.0.124/includes/Product_Scorer.php
r3788998 r3789031 772 772 $max = 8; 773 773 $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 ); 775 785 $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 ); 776 788 // 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 ); 779 791 $points = ( $delivery ? 5 : ( $not_rendered ? 2.5 : 0 ) ) + ( $returns ? 3 : ( $not_rendered ? 1.5 : 0 ) ); 780 792 $notes = []; … … 958 970 'id' => 0, 'type' => '', 'name' => '', 'price' => null, 'on_sale' => false, 'currency' => '', 'availability' => '', 'availability_date' => '', 959 971 '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' => '', 961 973 'short_description' => '', 'cross_sells' => 0, 'upsells' => 0, 'review_count' => 0, 'rating' => 0.0, 'reviews_visible' => false, 962 974 'visible_review_count' => 0, 'modified' => '', 'sale_to' => '', 'noindex' => false, 'schema_nodes' => [], 'visible_price' => null, -
seobetter/tags/1.0.124/readme.txt
r3788998 r3789031 5 5 Requires at least: 6.0 6 6 Tested up to: 7.1 7 Stable tag: 1.0.12 37 Stable tag: 1.0.124 8 8 Requires PHP: 8.0 9 9 License: GPLv2 or later … … 232 232 == Changelog == 233 233 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 234 239 = 1.0.123 = 235 240 * 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. … … 326 331 == Upgrade Notice == 327 332 333 = 1.0.124 = 334 The product score now counts the delivery and returns details in your saved product description, also inside a collapsed product tab. 335 328 336 = 1.0.123 = 329 337 The 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 4 4 * Plugin URI: https://seobetter.com 5 5 * 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.12 36 * Version: 1.0.124 7 7 * Author: SEOBetter 8 8 * License: GPLv2 or later … … 90 90 define( 'SEOBETTER_LOADED', __FILE__ ); 91 91 92 define( 'SEOBETTER_VERSION', '1.0.12 3' );92 define( 'SEOBETTER_VERSION', '1.0.124' ); 93 93 94 94 // v424 — Founder $99/yr Stripe Payment Link (the live launch offer used by the -
seobetter/trunk/includes/Cloud_API.php
r3788942 r3789031 522 522 if ( ( $resp['refunded'] ?? null ) === true ) { $fail['refunded'] = true; } 523 523 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'; } 524 526 if ( isset( $resp['cloud_cap'] ) ) { License_Manager::store_cloud_usage( (int) ( $resp['cloud_used'] ?? 0 ), (int) $resp['cloud_cap'] ); } 525 527 } … … 591 593 return [ 'status' => 'ok', 'calls' => $json['calls'] ]; 592 594 } 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' ]; } 593 597 return [ 'status' => 'error', 'reason' => 'bad-response' ]; 594 598 } -
seobetter/trunk/includes/Product_Facts.php
r3788998 r3789031 37 37 38 38 /** 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'; 40 40 /** 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'; 42 42 /** 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'; 44 44 45 45 /** Whether $text states delivery (cost, threshold or time) and returns. */ … … 49 49 'returns' => (bool) preg_match( self::RETURNS_RE, $text ), 50 50 ]; 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' ) ) ); 51 132 } 52 133 … … 115 196 'description_html' => (string) $product->get_description(), 116 197 'short_description' => self::plain( (string) $product->get_short_description() ), 198 'short_description_html' => (string) $product->get_short_description(), 117 199 'cross_sells' => count( array_filter( (array) $product->get_cross_sell_ids() ) ), 118 200 'upsells' => count( array_filter( (array) $product->get_upsell_ids() ) ), … … 171 253 if ( $html === '' ) { return null; } 172 254 $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 } 173 264 set_transient( $key, $page, self::PAGE_TTL ); 174 265 return $page; … … 222 313 'sslverify' => $own ? (bool) apply_filters( 'https_local_ssl_verify', false ) : true, 223 314 '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' ], 225 317 'limit_response_size' => self::PAGE_MAX_BYTES, 226 318 ] ); … … 935 1027 'availability' => '', 'availability_date' => '', 'condition' => '', 'sku' => '', 'gtin' => '', 'mpn' => '', 'brand' => '', 936 1028 '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, 938 1030 'review_count' => 0, 'rating' => 0.0, 'reviews_visible' => false, 'visible_review_count' => 0, 'modified' => '', 'sale_to' => '', 939 1031 'noindex' => false, 'schema_nodes' => [], 'visible_price' => null, 'visible_availability' => '', 'visible_text' => '', -
seobetter/trunk/includes/Product_Scorer.php
r3788998 r3789031 772 772 $max = 8; 773 773 $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 ); 775 785 $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 ); 776 788 // 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 ); 779 791 $points = ( $delivery ? 5 : ( $not_rendered ? 2.5 : 0 ) ) + ( $returns ? 3 : ( $not_rendered ? 1.5 : 0 ) ); 780 792 $notes = []; … … 958 970 'id' => 0, 'type' => '', 'name' => '', 'price' => null, 'on_sale' => false, 'currency' => '', 'availability' => '', 'availability_date' => '', 959 971 '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' => '', 961 973 'short_description' => '', 'cross_sells' => 0, 'upsells' => 0, 'review_count' => 0, 'rating' => 0.0, 'reviews_visible' => false, 962 974 'visible_review_count' => 0, 'modified' => '', 'sale_to' => '', 'noindex' => false, 'schema_nodes' => [], 'visible_price' => null, -
seobetter/trunk/readme.txt
r3788998 r3789031 5 5 Requires at least: 6.0 6 6 Tested up to: 7.1 7 Stable tag: 1.0.12 37 Stable tag: 1.0.124 8 8 Requires PHP: 8.0 9 9 License: GPLv2 or later … … 232 232 == Changelog == 233 233 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 234 239 = 1.0.123 = 235 240 * 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. … … 326 331 == Upgrade Notice == 327 332 333 = 1.0.124 = 334 The product score now counts the delivery and returns details in your saved product description, also inside a collapsed product tab. 335 328 336 = 1.0.123 = 329 337 The 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 4 4 * Plugin URI: https://seobetter.com 5 5 * 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.12 36 * Version: 1.0.124 7 7 * Author: SEOBetter 8 8 * License: GPLv2 or later … … 90 90 define( 'SEOBETTER_LOADED', __FILE__ ); 91 91 92 define( 'SEOBETTER_VERSION', '1.0.12 3' );92 define( 'SEOBETTER_VERSION', '1.0.124' ); 93 93 94 94 // v424 — Founder $99/yr Stripe Payment Link (the live launch offer used by the
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)