PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.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 1.26.0 1.25.0 trunk 1.0.0 1.0.1 All 51 releases
← All changes | includes/admin/class-metabox-manager.php +126 -12 2.9.0 → 2.10.0 View file →
@@ -1064,8 +1064,12 @@
1064 1064 'patternPreviews' => Pattern_Resolver::previews($post_id),
1065 1065 // Token => value map (keys without %), for live client-side preview of
1066 1066 // a custom pattern typed into a metabox field.
1067 1067 'patternVariables' => Pattern_Resolver::variables($post_id),
1068 + // The uncapped copy the Content Analysis panel measures. Localized
1069 + // here so all six editors get it, rather than each builder adding
1070 + // its own (#778). `contentPreview` stays as the AI budget.
1071 + 'contentFull' => $post instanceof \WP_Post ? $this->get_analysis_content($post) : '',
1068 1072 ];
1069 1073 }
1070 1074
1071 1075 /**
@@ -1445,18 +1449,68 @@
1445 1449 return null;
1446 1450 }
1447 1451
1448 1452 /**
1449 - * Get content preview for AI analysis
1450 - *
1451 - * @param \WP_Post $post Post object
1452 - * @return string Content preview
1453 + * Characters of page text an AI caller is given.
1454 + *
1455 + * A prompt budget, not a description of the page — one caller forwards this
1456 + * straight into an AI payload, so it stays capped.
1457 + *
1458 + * @since 2.10.0
1459 + * @var int
1453 1460 */
1454 - public function get_content_preview(\WP_Post $post): string {
1455 - $content = $post->post_title . "\n\n";
1461 + private const AI_PREVIEW_LENGTH = 4000;
1456 1462
1457 - if (!empty($post->post_excerpt)) {
1458 - $content .= $post->post_excerpt . "\n\n";
1463 + /**
1464 + * Safety ceiling for the analysis copy.
1465 + *
1466 + * High enough that no real article reaches it, low enough that a runaway
1467 + * builder tree cannot put a megabyte of text through wp_localize_script
1468 + * into every editor page load.
1469 + *
1470 + * @since 2.10.0
1471 + * @var int
1472 + */
1473 + private const ANALYSIS_MAX_LENGTH = 200000;
1474 +
1475 + /**
1476 + * Resolved body text per post, for this request.
1477 + *
1478 + * Every builder integration localizes get_localized_data() (which carries
1479 + * the analysis copy) and then adds get_content_preview() on top, and the
1480 + * classic screen renders the preview into the form as well as localizing
1481 + * the analysis copy. Each of those resolved the post through
1482 + * Builder_Content from scratch, so a builder page walked its whole tree
1483 + * twice on every editor load. Both now derive from one resolution.
1484 + *
1485 + * Keyed on the post's ID and modified time, so a post saved and re-read
1486 + * within the same request is resolved again.
1487 + *
1488 + * @since 2.10.0
1489 + * @var array<string,string>
1490 + */
1491 + private array $resolved_bodies = [];
1492 +
1493 + /**
1494 + * The post's body text, resolved through whatever built it.
1495 + *
1496 + * Body only. The title and excerpt used to be prepended here, which is
1497 + * right for an AI prompt but wrong for a measurement: the Analysis panel
1498 + * counted them as body copy, so Word Count and Keyword Density in a
1499 + * builder included the title and excerpt, while the same panel in the
1500 + * block editor (which reads the editor's content) did not. They are added
1501 + * back for the AI preview only.
1502 + *
1503 + * @since 2.10.0
1504 + *
1505 + * @param \WP_Post $post Post object.
1506 + * @return string Plain text, whitespace collapsed, uncapped.
1507 + */
1508 + private function resolve_post_body(\WP_Post $post): string {
1509 + $key = $post->ID . '|' . (string) ($post->post_modified_gmt ?? '');
1510 +
1511 + if (isset($this->resolved_bodies[$key])) {
1512 + return $this->resolved_bodies[$key];
1459 1513 }
1460 1514
1461 1515 // Resolve through Builder_Content: a page builder keeps its words
1462 1516 // outside post_content (Oxygen empties it entirely), and the editor
@@ -1464,17 +1518,77 @@
1464 1518 // browser is just the title and the live panel reports "No content".
1465 1519 if (!class_exists('\\ThinkRank\\SEO\\Builder_Content')) {
1466 1520 require_once THINKRANK_PLUGIN_DIR . 'includes/seo/class-builder-content.php';
1467 1521 }
1468 - $content .= \ThinkRank\SEO\Builder_Content::resolve($post);
1469 1522
1470 - // Clean and limit content
1523 + $this->resolved_bodies[$key] = self::collapse_text(\ThinkRank\SEO\Builder_Content::resolve($post));
1524 +
1525 + return $this->resolved_bodies[$key];
1526 + }
1527 +
1528 + /**
1529 + * Strip tags and collapse whitespace.
1530 + *
1531 + * @since 2.10.0
1532 + *
1533 + * @param string $content Markup or text.
1534 + * @return string
1535 + */
1536 + private static function collapse_text(string $content): string {
1471 1537 $content = wp_strip_all_tags($content);
1472 - $content = preg_replace('/\s+/', ' ', $content);
1473 1538
1539 + return trim((string) preg_replace('/\s+/', ' ', $content));
1540 + }
1541 +
1542 + /**
1543 + * The post's text for the editor's Content Analysis panel.
1544 + *
1545 + * Uncapped, because this is a measurement rather than a budget. In a page
1546 + * builder there is no `core/editor` store, no TinyMCE instance and no
1547 + * `#content` textarea, so the localized string is the ONLY thing the panel
1548 + * can count — and it was being handed the 4,000-character AI preview. Word
1549 + * Count, Keyword Density, Readability and Content Quality therefore
1550 + * described the first 4,000 characters and nothing after, freezing at
1551 + * roughly 600-700 words however long the page grew (#778).
1552 + *
1553 + * Still bounded, at a ceiling no real article reaches: this rides along on
1554 + * every editor page load.
1555 + *
1556 + * The body only, like the editor content it stands in for; the title and
1557 + * excerpt belong to the AI preview (see resolve_post_body()).
1558 + *
1559 + * @since 2.10.0
1560 + *
1561 + * @param \WP_Post $post Post object.
1562 + * @return string
1563 + */
1564 + public function get_analysis_content(\WP_Post $post): string {
1565 + return \ThinkRank\Core\Seo_Text::trim_to_length(
1566 + $this->resolve_post_body($post),
1567 + self::ANALYSIS_MAX_LENGTH
1568 + );
1569 + }
1570 +
1571 + /**
1572 + * Get content preview for AI analysis
1573 + *
1574 + * @param \WP_Post $post Post object
1575 + * @return string Content preview
1576 + */
1577 + public function get_content_preview(\WP_Post $post): string {
1578 + // The title and excerpt lead the preview: they are context an AI
1579 + // caller wants first, and they are not part of the measured body.
1580 + $lead = $post->post_title . "\n\n";
1581 +
1582 + if (!empty($post->post_excerpt)) {
1583 + $lead .= $post->post_excerpt . "\n\n";
1584 + }
1585 +
1586 + $preview = trim(self::collapse_text($lead) . ' ' . $this->resolve_post_body($post));
1587 +
1474 1588 // substr() counts BYTES: on Thai or CJK this handed the model a third
1475 1589 // of the intended content and cut the last character in half (#687).
1476 - return trim(\ThinkRank\Core\Seo_Text::trim_to_length($content, 4000));
1590 + return \ThinkRank\Core\Seo_Text::trim_to_length($preview, self::AI_PREVIEW_LENGTH);
1477 1591 }
1478 1592
1479 1593 /**
1480 1594 * AJAX handler for generating post metadata