PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.1
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.1
2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 All 57 releases
← All changes | includes/seo/class-seo-analyzer.php +16 -173 2.14.2 → 2.14.1 View file →
@@ -67,37 +67,8 @@
67 67 'toggle', // Elementor
68 68 'faq', // Widely used in third-party add-on element names
69 69 ];
70 70
71 - /**
72 - * Builder meta keys that never describe the published page.
73 - *
74 - * `_fl_builder_draft` holds Beaver Builder changes that were never
75 - * published, so a FAQ that exists only there is not on the page (#945).
76 - *
77 - * @since 2.14.2
78 - * @var string[]
79 - */
80 - private const UNPUBLISHED_BUILDER_META_KEYS = ['_fl_builder_draft'];
81 -
82 - /**
83 - * Builder layouts that only count while their builder renders the post.
84 - *
85 - * Elementor and Beaver Builder both keep their stored layout after the
86 - * author switches the page back to the block editor, so the layout alone
87 - * does not mean the builder renders it. Each key maps to the builder's own
88 - * flag and the value that means "on": Elementor's `_elementor_edit_mode`
89 - * is `builder`, Beaver's `_fl_builder_enabled` is any non-empty value
90 - * (null here). The same tests as FAQ_Content::builder() (#945).
91 - *
92 - * @since 2.14.2
93 - * @var array<string,array{0:string,1:string|null}>
94 - */
95 - private const FLAGGED_BUILDER_LAYOUTS = [
96 - '_elementor_data' => ['_elementor_edit_mode', 'builder'],
97 - '_fl_builder_data' => ['_fl_builder_enabled', null],
98 - ];
99 -
100 71 // Check result statuses.
101 72 public const PASSED = 'passed';
102 73 public const WARNING = 'warning';
103 74 public const FAILED = 'failed';
@@ -1721,9 +1692,12 @@
1721 1692 }
1722 1693
1723 1694 $collecting = false;
1724 1695
1725 - if ('disallow' === $field && self::disallow_blocks_everything($value)) {
1696 + // `/` is the canonical full block; `/*` is the same instruction
1697 + // written for a wildcard-aware crawler, and every answer engine on
1698 + // the list is one.
1699 + if ('disallow' === $field && ('/' === $value || '/*' === $value)) {
1726 1700 foreach ($current as $agent) {
1727 1701 $groups[$agent] = true;
1728 1702 }
1729 1703 }
@@ -1732,46 +1706,8 @@
1732 1706 return $groups;
1733 1707 }
1734 1708
1735 1709 /**
1736 - * Whether a `Disallow:` value matches every URL on the site.
1737 - *
1738 - * `/` is the canonical full block; `/*` is the same instruction written
1739 - * for a wildcard-aware crawler, and every answer engine on the list is
1740 - * one. A value is a path pattern in which `*` matches any run of
1741 - * characters, so a bare `*` (and `**`, `/**`) matches every path too, and
1742 - * Google's parser treats it that way. Recognising only `/` and `/*`
1743 - * reported a site that blocked everyone with `Disallow: *` as reachable
1744 - * by every answer engine (#890).
1745 - *
1746 - * A trailing `$` anchors the pattern to the end of the URL. After a `*`
1747 - * it changes nothing, since "any run of characters, then the end" still
1748 - * matches every path, so `*$` and `/*$` are full blocks as well. Without
1749 - * a `*` it narrows the match: `/$` is the home page only, and `/*.pdf$`
1750 - * is PDFs only, so neither counts.
1751 - *
1752 - * An empty value means "allow everything" and must never count as a
1753 - * block, so it is rejected here rather than left to the caller.
1754 - *
1755 - * `Allow:` lines are deliberately not weighed against this: a full
1756 - * robots.txt evaluator with longest-match precedence is a separate change.
1757 - *
1758 - * @since 2.14.2
1759 - * @param string $value Trimmed `Disallow:` value, comment already removed.
1760 - * @return bool
1761 - */
1762 - private static function disallow_blocks_everything(string $value): bool {
1763 - if ('' === $value) {
1764 - return false;
1765 - }
1766 -
1767 - // Optional leading slash, then either nothing (`/`) or a run of `*`
1768 - // with an optional end anchor (`*`, `/*`, `/*$`, `**$`). A `$` with
1769 - // no `*` before it never matches here, so `/$` stays a partial block.
1770 - return 1 === preg_match('#^/?(?:\*+\$?)?$#', $value);
1771 - }
1772 -
1773 - /**
1774 1710 * /llms.txt should be published — it is the one file whose entire purpose
1775 1711 * is telling an AI assistant what this site is and what to read.
1776 1712 *
1777 1713 * @since 2.5.0
@@ -1926,10 +1862,10 @@
1926 1862 }
1927 1863
1928 1864 // Builder trees: ThinkRank's own elements, and the builders' generic
1929 1865 // accordion/toggle/FAQ elements, which are what a non-ThinkRank FAQ
1930 - // is actually built from. Only layouts the visitor is served count.
1931 - $meta_keys = self::rendered_builder_meta_keys();
1866 + // is actually built from.
1867 + $meta_keys = self::builder_meta_keys();
1932 1868
1933 1869 if (empty($meta_keys)) {
1934 1870 return false;
1935 1871 }
@@ -1947,10 +1883,8 @@
1947 1883 $marker_clauses[] = 'pm.meta_value LIKE %s';
1948 1884 $marker_values[] = '%' . $wpdb->esc_like($marker) . '%';
1949 1885 }
1950 1886
1951 - [$gate_sql, $gate_values] = self::builder_layout_renders_sql('pm');
1952 -
1953 1887 // Same exemption: both placeholder runs are sized from fixed lists —
1954 1888 // the builder meta keys and the marker list — and every value is
1955 1889 // prepared.
1956 1890 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
@@ -1959,12 +1893,11 @@
1959 1893 "SELECT 1 FROM {$wpdb->postmeta} pm
1960 1894 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
1961 1895 WHERE p.post_status = 'publish'
1962 1896 AND pm.meta_key IN ({$key_placeholders})
1963 - AND (" . implode(' OR ', $marker_clauses) . ")
1964 - {$gate_sql}
1965 - LIMIT 1",
1966 - ...array_merge($meta_keys, $marker_values, $gate_values)
1897 + AND (" . implode(' OR ', $marker_clauses) . ')
1898 + LIMIT 1',
1899 + ...array_merge($meta_keys, $marker_values)
1967 1900 )
1968 1901 );
1969 1902 // phpcs:enable
1970 1903
@@ -2081,9 +2014,9 @@
2081 2014 $content_clauses[] = 'p.post_content LIKE %s';
2082 2015 $content_values[] = '%' . $wpdb->esc_like($marker) . '%';
2083 2016 }
2084 2017
2085 - $meta_keys = self::rendered_builder_meta_keys();
2018 + $meta_keys = self::builder_meta_keys();
2086 2019 $meta_sql = '';
2087 2020 $values = $content_values;
2088 2021
2089 2022 if (!empty($meta_keys)) {
@@ -2093,19 +2026,16 @@
2093 2026 $meta_clauses[] = 'pm.meta_value LIKE %s';
2094 2027 $meta_values[] = '%' . $wpdb->esc_like($marker) . '%';
2095 2028 }
2096 2029
2097 - [$gate_sql, $gate_values] = self::builder_layout_renders_sql('pm');
2098 -
2099 2030 $key_placeholders = implode(', ', array_fill(0, count($meta_keys), '%s'));
2100 2031 $meta_sql = " OR EXISTS (
2101 2032 SELECT 1 FROM {$wpdb->postmeta} pm
2102 2033 WHERE pm.post_id = p.ID
2103 2034 AND pm.meta_key IN ({$key_placeholders})
2104 - AND (" . implode(' OR ', $meta_clauses) . ")
2105 - {$gate_sql}
2106 - )";
2107 - $values = array_merge($content_values, $meta_keys, $meta_values, $gate_values);
2035 + AND (" . implode(' OR ', $meta_clauses) . ')
2036 + )';
2037 + $values = array_merge($content_values, $meta_keys, $meta_values);
2108 2038 }
2109 2039
2110 2040 $values[] = self::CONTENT_SAMPLE_SIZE;
2111 2041
@@ -2159,13 +2089,9 @@
2159 2089 return false;
2160 2090 }
2161 2091
2162 2092 // Elementor stores its tree as JSON, so the widget name appears verbatim.
2163 - // Read only while Elementor renders the post: the tree survives
2164 - // switching the page back to the block editor (#945).
2165 - $elementor = self::builder_layout_renders($post_id, '_elementor_data')
2166 - ? get_post_meta($post_id, '_elementor_data', true)
2167 - : '';
2093 + $elementor = get_post_meta($post_id, '_elementor_data', true);
2168 2094 if (is_string($elementor)
2169 2095 && (false !== strpos($elementor, '"' . self::ANSWER_FAQ_NAME . '"')
2170 2096 || false !== strpos($elementor, '"' . self::ANSWER_HOWTO_NAME . '"'))) {
2171 2097 return true;
@@ -2177,12 +2103,9 @@
2177 2103 return true;
2178 2104 }
2179 2105
2180 2106 // Beaver Builder keeps its layout in postmeta as a map of node objects.
2181 - // Read only while Beaver renders the post, for the same reason (#945).
2182 - $layout = self::builder_layout_renders($post_id, '_fl_builder_data')
2183 - ? get_post_meta($post_id, '_fl_builder_data', true)
2184 - : [];
2107 + $layout = get_post_meta($post_id, '_fl_builder_data', true);
2185 2108 if (is_array($layout)) {
2186 2109 foreach ($layout as $node) {
2187 2110 $settings = is_object($node) ? ($node->settings ?? null) : ($node['settings'] ?? null);
2188 2111 $settings = is_object($settings) ? get_object_vars($settings) : $settings;
@@ -2199,9 +2122,9 @@
2199 2122 // while the page was publishing exactly that (#686). Builder_Content
2200 2123 // already knows every key involved — Oxygen 6 is Breakdance under the
2201 2124 // hood, and older releases used two other keys — so ask it rather than
2202 2125 // keeping a second list that can drift.
2203 - foreach (self::rendered_builder_meta_keys() as $meta_key) {
2126 + foreach (self::builder_meta_keys() as $meta_key) {
2204 2127 if ('_fl_builder_data' === $meta_key || '_elementor_data' === $meta_key) {
2205 2128 continue; // Handled above, in their own storage shapes.
2206 2129 }
2207 2130
@@ -2234,88 +2157,8 @@
2234 2157 require_once $file;
2235 2158 }
2236 2159
2237 2160 return Builder_Content::builder_meta_keys();
2238 - }
2239 -
2240 - /**
2241 - * Builder meta keys that can hold the published page.
2242 - *
2243 - * {@see builder_meta_keys()} without the unpublished draft keys, which
2244 - * Builder_Content lists because the word-count index watches them, not
2245 - * because they are ever served (#945).
2246 - *
2247 - * @since 2.14.2
2248 - * @return string[]
2249 - */
2250 - private static function rendered_builder_meta_keys(): array {
2251 - return array_values(array_diff(self::builder_meta_keys(), self::UNPUBLISHED_BUILDER_META_KEYS));
2252 - }
2253 -
2254 - /**
2255 - * Whether a builder layout key holds what the visitor is served.
2256 - *
2257 - * The answer-content readers' guard for #945. Builder_Content gains the
2258 - * same guards for its resolver in #910, and once that lands this can ask
2259 - * it instead; until then it reads the builders' own flags, exactly as
2260 - * FAQ_Content::builder() does.
2261 - *
2262 - * @since 2.14.2
2263 - * @param int $post_id Post being inspected.
2264 - * @param string $meta_key Builder meta key about to be read.
2265 - * @return bool
2266 - */
2267 - private static function builder_layout_renders(int $post_id, string $meta_key): bool {
2268 - if (in_array($meta_key, self::UNPUBLISHED_BUILDER_META_KEYS, true)) {
2269 - return false;
2270 - }
2271 -
2272 - if (!isset(self::FLAGGED_BUILDER_LAYOUTS[$meta_key])) {
2273 - return true;
2274 - }
2275 -
2276 - [$flag, $on] = self::FLAGGED_BUILDER_LAYOUTS[$meta_key];
2277 - $value = get_post_meta($post_id, $flag, true);
2278 -
2279 - return null === $on ? !empty($value) : $on === (string) $value;
2280 - }
2281 -
2282 - /**
2283 - * {@see builder_layout_renders()} as an SQL condition on a postmeta row.
2284 - *
2285 - * The site-wide queries match markers in SQL with nothing confirming each
2286 - * row in PHP afterwards, so the flag test has to happen there too. Rows
2287 - * under any other key pass untouched. `!empty()` in PHP is false for '' and
2288 - * '0', which is what the "any non-empty value" branch excludes.
2289 - *
2290 - * @since 2.14.2
2291 - * @param string $alias Alias of the postmeta row being tested.
2292 - * @return array{0:string,1:array<int,string>} ` AND (...)` clause and its values.
2293 - */
2294 - private static function builder_layout_renders_sql(string $alias): array {
2295 - global $wpdb;
2296 -
2297 - $flagged = array_keys(self::FLAGGED_BUILDER_LAYOUTS);
2298 - $clauses = [$alias . '.meta_key NOT IN (' . implode(', ', array_fill(0, count($flagged), '%s')) . ')'];
2299 - $values = $flagged;
2300 -
2301 - foreach (self::FLAGGED_BUILDER_LAYOUTS as $meta_key => [$flag, $on]) {
2302 - $test = null === $on ? "flag.meta_value NOT IN ('', '0')" : 'flag.meta_value = %s';
2303 - $clauses[] = "({$alias}.meta_key = %s AND EXISTS (
2304 - SELECT 1 FROM {$wpdb->postmeta} flag
2305 - WHERE flag.post_id = {$alias}.post_id
2306 - AND flag.meta_key = %s
2307 - AND {$test}
2308 - ))";
2309 - $values[] = $meta_key;
2310 - $values[] = $flag;
2311 -
2312 - if (null !== $on) {
2313 - $values[] = $on;
2314 - }
2315 - }
2316 -
2317 - return [' AND (' . implode(' OR ', $clauses) . ')', $values];
2318 2161 }
2319 2162
2320 2163 /**
2321 2164 * Whether a stored builder blob names a ThinkRank FAQ or How-To.