| @@ -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 | |