| @@ -232,26 +232,53 @@ | ||
| 232 | 232 | if (!$seo_manager) { |
| 233 | 233 | return; |
| 234 | 234 | } |
| 235 | 235 | |
| 236 | - // Lightweight breadcrumb check — query the DB directly to avoid loading | |
| 237 | - // the full Site_Identity_Manager (115KB) on every frontend page | |
| 238 | - global $wpdb; | |
| 239 | - $table = $wpdb->prefix . 'thinkrank_seo_settings'; | |
| 236 | + // Both checks below read the site_identity settings category. Loading the | |
| 237 | + // full Site_Identity_Manager (115KB) on every front-end page to get them | |
| 238 | + // would be worse, but the two ad-hoc queries that replaced it did not share | |
| 239 | + // the object cache the manager populates on `wp` — so the category was | |
| 240 | + // fetched twice per request, uncached, on top of the manager's own read | |
| 241 | + // (#402). | |
| 242 | + // | |
| 243 | + // Read that cache entry directly. Abstract_SEO_Manager caches the merged | |
| 244 | + // settings under this key on every get_settings('site') call, and `wp` runs | |
| 245 | + // before wp_enqueue_scripts, so on a normal front-end render this is a hit | |
| 246 | + // and costs nothing. The fallback is one query for the whole category | |
| 247 | + // rather than two for parts of it. | |
| 248 | + $identity = wp_cache_get('seo_settings_site_identity_site_0', 'thinkrank_seo'); | |
| 240 | 249 | |
| 241 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Lightweight frontend check avoids loading heavy manager class | |
| 242 | - $breadcrumbs_enabled = $wpdb->get_var( | |
| 243 | - $wpdb->prepare( | |
| 244 | - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name uses WordPress prefix, safe to interpolate | |
| 245 | - "SELECT setting_value FROM `{$table}` WHERE context_type = %s AND context_id = %d AND setting_category = %s AND setting_key = %s AND is_active = 1", | |
| 246 | - 'site', | |
| 247 | - 0, | |
| 248 | - 'site_identity', | |
| 249 | - 'breadcrumbs_enabled' | |
| 250 | - ) | |
| 251 | - ); | |
| 250 | + if (!is_array($identity)) { | |
| 251 | + global $wpdb; | |
| 252 | + $table = $wpdb->prefix . 'thinkrank_seo_settings'; | |
| 252 | 253 | |
| 253 | - if ($breadcrumbs_enabled) { | |
| 254 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Lightweight frontend check avoids loading heavy manager class | |
| 255 | + $rows = $wpdb->get_results( | |
| 256 | + $wpdb->prepare( | |
| 257 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name uses WordPress prefix, safe to interpolate | |
| 258 | + "SELECT setting_key, setting_value FROM `{$table}` WHERE context_type = %s AND context_id = %d AND setting_category = %s AND is_active = 1", | |
| 259 | + 'site', | |
| 260 | + 0, | |
| 261 | + 'site_identity' | |
| 262 | + ), | |
| 263 | + ARRAY_A | |
| 264 | + ); | |
| 265 | + | |
| 266 | + $identity = []; | |
| 267 | + foreach ((array) $rows as $row) { | |
| 268 | + $identity[$row['setting_key']] = $row['setting_value']; | |
| 269 | + } | |
| 270 | + | |
| 271 | + // Deliberately not written back to the object cache: the manager caches | |
| 272 | + // the values *merged with its defaults* under this key, and seeding it | |
| 273 | + // with raw rows would hand every later reader a partial record. | |
| 274 | + } | |
| 275 | + | |
| 276 | + $identity_value = static function (string $key) use ($identity): string { | |
| 277 | + return isset($identity[$key]) ? trim((string) $identity[$key]) : ''; | |
| 278 | + }; | |
| 279 | + | |
| 280 | + if (!empty($identity['breadcrumbs_enabled'])) { | |
| 254 | 281 | wp_enqueue_style( |
| 255 | 282 | 'thinkrank-breadcrumbs', |
| 256 | 283 | THINKRANK_PLUGIN_URL . 'static/css/breadcrumbs.css', |
| 257 | 284 | [], |
| @@ -258,35 +285,15 @@ | ||
| 258 | 285 | THINKRANK_VERSION |
| 259 | 286 | ); |
| 260 | 287 | } |
| 261 | 288 | |
| 262 | - // Lightweight hero check — only load hero CSS when the hero will actually | |
| 263 | - // render. Mirrors the render gate in SEO_Manager::generate_hero_html(): | |
| 264 | - // a title, a subtitle, or a COMPLETE CTA (both text and URL). A CTA with | |
| 265 | - // text but no URL renders nothing, so it must not pull in the stylesheet. | |
| 266 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Lightweight frontend check avoids loading heavy manager class | |
| 267 | - $hero_fields = $wpdb->get_results( | |
| 268 | - $wpdb->prepare( | |
| 269 | - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name uses WordPress prefix, safe to interpolate | |
| 270 | - "SELECT setting_key, setting_value FROM `{$table}` WHERE context_type = %s AND context_id = %d AND setting_category = %s AND setting_key IN (%s, %s, %s, %s) AND is_active = 1", | |
| 271 | - 'site', | |
| 272 | - 0, | |
| 273 | - 'site_identity', | |
| 274 | - 'hero_title', | |
| 275 | - 'hero_subtitle', | |
| 276 | - 'hero_cta_text', | |
| 277 | - 'hero_cta_url' | |
| 278 | - ), | |
| 279 | - OBJECT_K | |
| 280 | - ); | |
| 281 | - | |
| 282 | - $hero_value = static function ($key) use ($hero_fields) { | |
| 283 | - return isset($hero_fields[$key]) ? trim((string) $hero_fields[$key]->setting_value) : ''; | |
| 284 | - }; | |
| 285 | - | |
| 286 | - $hero_renders = '' !== $hero_value('hero_title') | |
| 287 | - || '' !== $hero_value('hero_subtitle') | |
| 288 | - || ('' !== $hero_value('hero_cta_text') && '' !== $hero_value('hero_cta_url')); | |
| 289 | + // Hero check — only load hero CSS when the hero will actually render. | |
| 290 | + // Mirrors the render gate in SEO_Manager::generate_hero_html(): a title, a | |
| 291 | + // subtitle, or a COMPLETE CTA (both text and URL). A CTA with text but no | |
| 292 | + // URL renders nothing, so it must not pull in the stylesheet. | |
| 293 | + $hero_renders = '' !== $identity_value('hero_title') | |
| 294 | + || '' !== $identity_value('hero_subtitle') | |
| 295 | + || ('' !== $identity_value('hero_cta_text') && '' !== $identity_value('hero_cta_url')); | |
| 289 | 296 | |
| 290 | 297 | if ($hero_renders) { |
| 291 | 298 | wp_enqueue_style( |
| 292 | 299 | 'thinkrank-hero', |