PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.12.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.12.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 1.26.0 1.25.0 trunk All 53 releases
← All changes | includes/seo/class-schema-management-system.php +56 -38 2.11.0 → 2.12.0 View file →
@@ -1629,8 +1629,19 @@
1629 1629 } elseif ($context_id && in_array($context_type, ['post', 'page', 'product'], true)) {
1630 1630 // Post/page/product data
1631 1631 $post = get_post($context_id);
1632 1632 if ($post) {
1633 + // Resolve the featured image's URL to its ID here, where the ID
1634 + // is in hand, so the schema builder does not query for an
1635 + // attachment it was just given (#847). Offered as a hint rather
1636 + // than asserted: `post_thumbnail_url` can swap the URL for one
1637 + // the featured image does not own.
1638 + $thumbnail_url = get_the_post_thumbnail_url($post->ID, 'full');
1639 +
1640 + if ($thumbnail_url) {
1641 + Attachment_Lookup::id_from_url((string) $thumbnail_url, (int) get_post_thumbnail_id($post->ID));
1642 + }
1643 +
1633 1644 $content_data = [
1634 1645 'title' => $post->post_title,
1635 1646 'url' => get_permalink($post->ID),
1636 1647 'excerpt' => $post->post_excerpt ?: \ThinkRank\Core\Seo_Text::trim_words($post->post_content, 30),
@@ -1644,9 +1655,9 @@
1644 1655 // Google rejects as "Invalid value in field datePublished"
1645 1656 // and drops the Article rich result (#465).
1646 1657 'date' => get_the_date('c', $post),
1647 1658 'modified' => get_the_modified_date('c', $post),
1648 - 'image' => get_the_post_thumbnail_url($post->ID, 'full'),
1659 + 'image' => $thumbnail_url,
1649 1660 'focus_keywords' => Focus_Keywords::get($post->ID),
1650 1661 'business_data' => $this->get_business_data_from_local_seo(),
1651 1662 'site_data' => $this->get_site_data_for_schema(),
1652 1663 'social_data' => $this->get_social_data_for_schema()
@@ -2039,12 +2050,10 @@
2039 2050
2040 2051 /**
2041 2052 * Get deployed schemas for frontend integration
2042 2053 *
2043 - * PERFORMANCE OPTIMIZED: This method now uses:
2044 - * 1. Schema caching layer (90% reduction in database queries)
2045 - * 2. Window function approach instead of correlated subquery (80-90% query performance improvement)
2046 - * 3. Composite index: idx_context_schema_active
2054 + * Returns the newest active, deployed row of each schema type for the
2055 + * context. Results are cached per context (see Schema_Cache_Manager).
2047 2056 *
2048 2057 * @since 1.0.0
2049 2058 *
2050 2059 * @param string $context_type Context type
@@ -2067,48 +2076,57 @@
2067 2076
2068 2077 // Use existing seo_schema table
2069 2078 $table_name = $wpdb->prefix . 'thinkrank_seo_schema';
2070 2079
2071 - // OPTIMIZED QUERY: Use window function approach to eliminate correlated subquery
2072 - // This leverages the new composite index: idx_context_schema_active (context_type, schema_type, is_active, created_at DESC)
2080 + // The newest row per type used to be picked with ROW_NUMBER() OVER
2081 + // (PARTITION BY schema_type ...). Window functions need MySQL 8.0 /
2082 + // MariaDB 10.2, and WordPress still runs on MySQL 5.7, where that is
2083 + // a syntax error on every page view and no deployed schema is ever
2084 + // output. A row is the newest of its type when no other row of the
2085 + // same context and type outranks it, so NOT EXISTS keeps the
2086 + // greatest-per-group in the database, and schema_data — JSON, and
2087 + // large — is only transferred for the rows that are output.
2088 + //
2089 + // `<=>` is NULL-safe equality: the site context stores context_id
2090 + // as NULL, and `n.context_id = s.context_id` is never true for it.
2091 + // schema_id breaks a same-second tie, which the window function
2092 + // left to chance.
2093 + $args = [$context_type];
2073 2094 if (null === $context_id) {
2074 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Schema retrieval requires direct database access
2075 - $sql = sprintf(
2076 - 'SELECT schema_type, schema_data FROM (SELECT schema_type, schema_data, ROW_NUMBER() OVER (PARTITION BY schema_type ORDER BY created_at DESC) as rn FROM %s WHERE context_type = %%s AND context_id IS NULL AND is_active = 1 AND validation_status IN (\'deployed\', \'valid\')) ranked WHERE rn = 1 ORDER BY schema_type',
2077 - $table_name
2078 - );
2079 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Schema retrieval requires direct database access
2080 - $deployed_schemas = $wpdb->get_results(
2081 - $wpdb->prepare(
2082 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- SQL is properly prepared with placeholders
2083 - $sql,
2084 - $context_type
2085 - ),
2086 - ARRAY_A
2087 - );
2095 + $context_where = 's.context_id IS NULL';
2088 2096 } else {
2089 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Schema retrieval requires direct database access
2090 - $sql = sprintf(
2091 - 'SELECT schema_type, schema_data FROM (SELECT schema_type, schema_data, ROW_NUMBER() OVER (PARTITION BY schema_type ORDER BY created_at DESC) as rn FROM %s WHERE context_type = %%s AND context_id = %%d AND is_active = 1 AND validation_status IN (\'deployed\', \'valid\')) ranked WHERE rn = 1 ORDER BY schema_type',
2092 - $table_name
2093 - );
2094 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Schema retrieval requires direct database access
2095 - $deployed_schemas = $wpdb->get_results(
2096 - $wpdb->prepare(
2097 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- SQL is properly prepared with placeholders
2098 - $sql,
2099 - $context_type,
2100 - $context_id
2101 - ),
2102 - ARRAY_A
2103 - );
2097 + $context_where = 's.context_id = %d';
2098 + $args[] = $context_id;
2104 2099 }
2105 2100
2101 + $sql = sprintf(
2102 + 'SELECT s.schema_type, s.schema_data FROM %1$s s'
2103 + . ' WHERE s.context_type = %%s AND %2$s AND s.is_active = 1 AND s.validation_status IN (\'deployed\', \'valid\')'
2104 + . ' AND NOT EXISTS ('
2105 + . 'SELECT 1 FROM %1$s n'
2106 + . ' WHERE n.context_type = s.context_type AND n.context_id <=> s.context_id AND n.schema_type = s.schema_type'
2107 + . ' AND n.is_active = 1 AND n.validation_status IN (\'deployed\', \'valid\')'
2108 + . ' AND (n.created_at > s.created_at OR (n.created_at = s.created_at AND n.schema_id > s.schema_id))'
2109 + . ')'
2110 + . ' ORDER BY s.schema_type',
2111 + $table_name,
2112 + $context_where
2113 + );
2114 +
2115 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Schema retrieval requires direct database access
2116 + $deployed_schemas = $wpdb->get_results(
2117 + $wpdb->prepare(
2118 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- SQL is properly prepared with placeholders
2119 + $sql,
2120 + ...$args
2121 + ),
2122 + ARRAY_A
2123 + );
2124 +
2106 2125 // Deliberately no early return on an empty result: it has to reach the
2107 2126 // cache write below. Most URLs have no deployed schema, so gating the
2108 2127 // write on a non-empty result made the majority of front-end requests
2109 - // permanent cache misses, re-running a ROW_NUMBER() OVER (PARTITION BY
2110 - // ...) query with two filesorts on every pageview (#392).
2128 + // permanent cache misses, re-running the query on every pageview (#392).
2111 2129 $deployed_schemas = $deployed_schemas ?: [];
2112 2130
2113 2131 // Process schemas for return
2114 2132 $processed_schemas = [];