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.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 1.27.0 All 56 releases
← All changes | includes/seo/class-seo-analyzer.php +88 -13 2.9.0 → 2.14.1 View file →
@@ -551,9 +551,9 @@
551 551 if (!get_option('blog_public')) {
552 552 return [
553 553 'label' => __('Site is visible to search engines', 'thinkrank'),
554 554 'status' => self::FAILED,
555 - 'message' => __('Your site is telling search engines not to index it — it will not appear in search results.', 'thinkrank'),
555 + 'message' => __('Your site is telling search engines not to index it, so it will not appear in search results.', 'thinkrank'),
556 556 'how_to_fix' => __('Untick "Discourage search engines from indexing this site" under Settings → Reading.', 'thinkrank'),
557 557 ];
558 558 }
559 559
@@ -874,9 +874,9 @@
874 874 'label' => $label,
875 875 'status' => $coverage >= self::COVERAGE_WARN ? self::WARNING : self::FAILED,
876 876 /* translators: 1: posts missing a meta description, 2: sampled posts. */
877 877 'message' => sprintf(__('%1$d of your %2$d most recent posts are missing a meta description. Search engines fall back to arbitrary page text for their snippets.', 'thinkrank'), $missing, $total),
878 - 'how_to_fix' => __('Add meta descriptions in the ThinkRank SEO panel when editing a post — or use Bulk SEO Optimization to generate them with AI.', 'thinkrank'),
878 + 'how_to_fix' => __('Add meta descriptions in the ThinkRank SEO panel when editing a post, or use Bulk SEO Optimization to generate them with AI.', 'thinkrank'),
879 879 'value' => $value,
880 880 'affected_posts' => $this->affected_posts($without),
881 881 ];
882 882 }
@@ -1245,8 +1245,13 @@
1245 1245 * REST request would fire every shortcode and block renderer on the site.
1246 1246 * The structural signals these checks look for (headings, lists, tables,
1247 1247 * the opening passage) survive in the stored markup.
1248 1248 *
1249 + * Except on a builder page, where they do not: the words and headings are
1250 + * in builder meta and `post_content` is empty, or, on a Bricks page, holds
1251 + * blocks Bricks never renders. Those rows read from builder_content(), so
1252 + * a builder-built site is graded on its pages rather than skipped (#892).
1253 + *
1249 1254 * @since 2.5.0
1250 1255 * @return array<int,array{id: int, content: string, text: string, modified: int}>
1251 1256 */
1252 1257 private function get_content_sample(): array {
@@ -1255,10 +1260,12 @@
1255 1260 }
1256 1261
1257 1262 $post_ids = $this->sample_post_ids();
1258 1263
1264 + // Meta too: builder_content() asks every sampled post whether Bricks
1265 + // owns it, and one query beats a hundred.
1259 1266 if (function_exists('_prime_post_caches')) {
1260 - _prime_post_caches($post_ids, false, false);
1267 + _prime_post_caches($post_ids, false, true);
1261 1268 }
1262 1269
1263 1270 $sample = [];
1264 1271
@@ -1269,9 +1276,9 @@
1269 1276 }
1270 1277
1271 1278 $modified = isset($post->post_modified_gmt) ? strtotime((string) $post->post_modified_gmt . ' UTC') : false;
1272 1279
1273 - $content = (string) $post->post_content;
1280 + $content = $this->builder_content($post) ?? (string) $post->post_content;
1274 1281
1275 1282 $sample[] = [
1276 1283 'id' => (int) $post->ID,
1277 1284 'content' => $content,
@@ -1285,8 +1292,45 @@
1285 1292 return $sample;
1286 1293 }
1287 1294
1288 1295 /**
1296 + * A sampled post's content from its page builder, when that is where it is.
1297 + *
1298 + * Null for every post whose `post_content` is what the visitor reads, so
1299 + * those keep the raw-markup path and never pay for rendering. A post with
1300 + * an empty `post_content`, or a Bricks page that discards it, resolves
1301 + * through Builder_Content, the extractor every other server-side scoring
1302 + * path reads through (#617). With nothing in `post_content` to render, the
1303 + * resolution only reads stored builder meta: no shortcode or block
1304 + * renderer runs.
1305 + *
1306 + * Guarded like builder_meta_keys(), so a partial checkout degrades to the
1307 + * raw markup instead of fataling mid-audit.
1308 + *
1309 + * @since 2.15.0
1310 + *
1311 + * @param \WP_Post $post Sampled post.
1312 + * @return string|null Resolved content, or null to use `post_content`.
1313 + */
1314 + private function builder_content(\WP_Post $post): ?string {
1315 + if ([] === self::builder_meta_keys()) {
1316 + return null;
1317 + }
1318 +
1319 + if ('' !== trim((string) $post->post_content)
1320 + && !Builder_Content::bricks_supersedes_post_content((int) $post->ID)
1321 + ) {
1322 + return null;
1323 + }
1324 +
1325 + try {
1326 + return Builder_Content::resolve($post);
1327 + } catch (\Throwable $e) {
1328 + return null;
1329 + }
1330 + }
1331 +
1332 + /**
1289 1333 * Shared shape for the content-sampled GEO checks: count how many posts in
1290 1334 * the sample satisfy a predicate and grade it on the coverage thresholds
1291 1335 * the Content category already uses.
1292 1336 *
@@ -1307,23 +1351,30 @@
1307 1351 string $fail_text,
1308 1352 string $how_to_fix,
1309 1353 string $empty_text
1310 1354 ): array {
1311 - // A page whose body is a shortcode or a builder layout leaves no
1312 - // extractable text, so every prose-shaped question here answers "no"
1313 - // for it — Cart, Checkout, My account and Shop would drag the category
1314 - // down over content nobody wants quoted in an AI answer. Skipping them
1315 - // is deliberate: this grades the pages that could be cited.
1316 - $sample = [];
1317 - foreach ($this->get_content_sample() as $row) {
1355 + // A page whose body is only a shortcode or a commerce block (Checkout,
1356 + // My account, Shop) leaves no extractable text, so every prose-shaped
1357 + // question here answers "no" for it and would drag the category down
1358 + // over content nobody wants quoted in an AI answer. Skipping them is
1359 + // deliberate: this grades the pages that could be cited. Builder pages
1360 + // are not in this group: get_content_sample() reads their builder
1361 + // storage, so they have text and are graded (#892).
1362 + $sampled = $this->get_content_sample();
1363 + $sample = [];
1364 + $textless = [];
1365 + foreach ($sampled as $row) {
1318 1366 if ('' !== $row['text']) {
1319 1367 $sample[] = $row;
1368 + } else {
1369 + $textless[] = $row['id'];
1320 1370 }
1321 1371 }
1322 1372
1323 1373 $total = count($sample);
1324 1374
1325 - if (0 === $total) {
1375 + // Nothing published: nothing to report.
1376 + if ([] === $sampled) {
1326 1377 return [
1327 1378 'label' => $label,
1328 1379 'status' => self::PASSED,
1329 1380 'message' => $empty_text,
@@ -1329,8 +1380,32 @@
1329 1380 'message' => $empty_text,
1330 1381 ];
1331 1382 }
1332 1383
1384 + // Pages exist, but none yielded text. That is a sample this check
1385 + // could not read, not an empty site, and it used to pass with "No
1386 + // published content to check yet", awarding the weight for content
1387 + // nobody examined (#892). Say so instead.
1388 + if (0 === $total) {
1389 + return [
1390 + 'label' => $label,
1391 + 'status' => self::WARNING,
1392 + 'message' => sprintf(
1393 + /* translators: %d: number of sampled pages. */
1394 + _n(
1395 + 'ThinkRank found no readable text on the %d page it sampled, so this could not be checked.',
1396 + 'ThinkRank found no readable text on any of the %d pages it sampled, so this could not be checked.',
1397 + count($textless),
1398 + 'thinkrank'
1399 + ),
1400 + count($textless)
1401 + ),
1402 + 'how_to_fix' => __('Pages built only from shortcodes, or with a page builder ThinkRank does not support, have no text it can read. Publish pages with written content, or check that your page builder is supported.', 'thinkrank'),
1403 + 'value' => sprintf('0/%d', count($textless)),
1404 + 'affected_posts' => $this->affected_posts($textless),
1405 + ];
1406 + }
1407 +
1333 1408 $matching = 0;
1334 1409 $failing = [];
1335 1410 foreach ($sample as $row) {
1336 1411 if ($predicate($row)) {
@@ -2240,9 +2315,9 @@
2240 2315 /* translators: 1: matching posts, 2: sampled posts. */
2241 2316 __('%1$d of your %2$d most recent pages use question-style headings.', 'thinkrank'),
2242 2317 /* translators: 1: posts without one, 2: sampled posts. */
2243 2318 __('%1$d of your %2$d most recent pages have no question-style heading. Assistants match a user\'s question against your headings first.', 'thinkrank'),
2244 - __('Phrase at least one H2 or H3 per page as the question it answers — "How does X work?" rather than "Overview".', 'thinkrank'),
2319 + __('Phrase at least one H2 or H3 per page as the question it answers: "How does X work?" rather than "Overview".', 'thinkrank'),
2245 2320 __('No published content to check yet.', 'thinkrank')
2246 2321 );
2247 2322 }
2248 2323