| @@ -62,8 +62,60 @@ | ||
| 62 | 62 | return $seo_manager->get_current_breadcrumbs(); |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | 65 | /** |
| 66 | + * Display the ThinkRank hero section (Site Identity → Hero & Branding) | |
| 67 | + * | |
| 68 | + * Usage in theme templates: | |
| 69 | + * <?php thinkrank_hero(); ?> | |
| 70 | + * | |
| 71 | + * Also available as the [thinkrank_hero] shortcode for use in content. | |
| 72 | + * | |
| 73 | + * @since 1.19.0 | |
| 74 | + * | |
| 75 | + * @param array $args Optional. Reserved for future display arguments. | |
| 76 | + * @return void | |
| 77 | + */ | |
| 78 | +function thinkrank_hero($args = []) { | |
| 79 | + /** | |
| 80 | + * Fires to display the ThinkRank hero section. | |
| 81 | + * | |
| 82 | + * @since 1.19.0 | |
| 83 | + * | |
| 84 | + * @param array $args Hero display arguments. | |
| 85 | + */ | |
| 86 | + do_action('thinkrank_hero', $args); | |
| 87 | +} | |
| 88 | + | |
| 89 | +/** | |
| 90 | + * Get the ThinkRank hero section data without displaying it | |
| 91 | + * | |
| 92 | + * @since 1.19.0 | |
| 93 | + * | |
| 94 | + * @return array|null Hero data (title, subtitle, cta_text, cta_url, | |
| 95 | + * background_image, html) or null if not available | |
| 96 | + */ | |
| 97 | +function thinkrank_get_hero() { | |
| 98 | + // Check if ThinkRank is loaded | |
| 99 | + if (!function_exists('thinkrank')) { | |
| 100 | + return null; | |
| 101 | + } | |
| 102 | + | |
| 103 | + $plugin = thinkrank(); | |
| 104 | + if (!$plugin) { | |
| 105 | + return null; | |
| 106 | + } | |
| 107 | + | |
| 108 | + $seo_manager = $plugin->get_component('frontend_seo'); | |
| 109 | + | |
| 110 | + if (!$seo_manager || !method_exists($seo_manager, 'get_current_hero')) { | |
| 111 | + return null; | |
| 112 | + } | |
| 113 | + | |
| 114 | + return $seo_manager->get_current_hero(); | |
| 115 | +} | |
| 116 | + | |
| 117 | +/** | |
| 66 | 118 | * Check if ThinkRank SEO is handling the current page |
| 67 | 119 | * |
| 68 | 120 | * @since 1.0.0 |
| 69 | 121 | * |
| @@ -116,27 +168,24 @@ | ||
| 116 | 168 | return $seo_manager->get_current_metadata(); |
| 117 | 169 | } |
| 118 | 170 | |
| 119 | 171 | /** |
| 120 | - * Display custom breadcrumb styles | |
| 172 | + * Get breadcrumb inline CSS | |
| 121 | 173 | * |
| 122 | 174 | * @since 1.0.0 |
| 123 | 175 | * |
| 124 | - * @return void | |
| 176 | + * @return string CSS styles for breadcrumbs | |
| 125 | 177 | */ |
| 126 | -function thinkrank_breadcrumb_styles() { | |
| 127 | - ?> | |
| 128 | - <style> | |
| 178 | +function thinkrank_get_breadcrumb_css(): string { | |
| 179 | + return ' | |
| 129 | 180 | .thinkrank-breadcrumbs { |
| 130 | 181 | margin: 1rem 0; |
| 131 | 182 | font-size: 0.9rem; |
| 132 | 183 | } |
| 133 | - | |
| 134 | 184 | .thinkrank-breadcrumbs .breadcrumb-prefix { |
| 135 | 185 | font-weight: 600; |
| 136 | 186 | margin-right: 0.5rem; |
| 137 | 187 | } |
| 138 | - | |
| 139 | 188 | .thinkrank-breadcrumbs .breadcrumb-list { |
| 140 | 189 | list-style: none; |
| 141 | 190 | margin: 0; |
| 142 | 191 | padding: 0; |
| @@ -143,9 +192,8 @@ | ||
| 143 | 192 | display: flex; |
| 144 | 193 | flex-wrap: wrap; |
| 145 | 194 | align-items: center; |
| 146 | 195 | } |
| 147 | - | |
| 148 | 196 | .thinkrank-breadcrumbs .breadcrumb-item { |
| 149 | 197 | margin: 0; |
| 150 | 198 | padding: 0; |
| 151 | 199 | display: flex; |
| @@ -150,33 +198,27 @@ | ||
| 150 | 198 | padding: 0; |
| 151 | 199 | display: flex; |
| 152 | 200 | align-items: center; |
| 153 | 201 | } |
| 154 | - | |
| 155 | 202 | .thinkrank-breadcrumbs .breadcrumb-item a { |
| 156 | 203 | color: #0073aa; |
| 157 | 204 | text-decoration: none; |
| 158 | 205 | } |
| 159 | - | |
| 160 | 206 | .thinkrank-breadcrumbs .breadcrumb-item a:hover { |
| 161 | 207 | text-decoration: underline; |
| 162 | 208 | } |
| 163 | - | |
| 164 | 209 | .thinkrank-breadcrumbs .breadcrumb-item.current span { |
| 165 | 210 | color: #666; |
| 166 | 211 | font-weight: 500; |
| 167 | 212 | } |
| 168 | - | |
| 169 | 213 | .thinkrank-breadcrumbs .breadcrumb-separator { |
| 170 | 214 | margin: 0 0.5rem; |
| 171 | 215 | color: #999; |
| 172 | - } | |
| 173 | - </style> | |
| 174 | - <?php | |
| 216 | + }'; | |
| 175 | 217 | } |
| 176 | 218 | |
| 177 | -// Auto-include breadcrumb styles if breadcrumbs are enabled | |
| 178 | -add_action('wp_head', function() { | |
| 219 | +// Enqueue breadcrumb styles properly if breadcrumbs are enabled | |
| 220 | +add_action('wp_enqueue_scripts', function() { | |
| 179 | 221 | // Check if ThinkRank is loaded |
| 180 | 222 | if (!function_exists('thinkrank')) { |
| 181 | 223 | return; |
| 182 | 224 | } |
| @@ -186,17 +228,78 @@ | ||
| 186 | 228 | return; |
| 187 | 229 | } |
| 188 | 230 | |
| 189 | 231 | $seo_manager = $plugin->get_component('frontend_seo'); |
| 232 | + if (!$seo_manager) { | |
| 233 | + return; | |
| 234 | + } | |
| 190 | 235 | |
| 191 | - if ($seo_manager) { | |
| 192 | - // Check if Site Identity Manager exists and breadcrumbs are enabled | |
| 193 | - if (class_exists('ThinkRank\\SEO\\Site_Identity_Manager')) { | |
| 194 | - $identity_manager = new \ThinkRank\SEO\Site_Identity_Manager(); | |
| 195 | - $settings = $identity_manager->get_settings('site'); | |
| 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'); | |
| 196 | 249 | |
| 197 | - if (!empty($settings['breadcrumbs_enabled'])) { | |
| 198 | - thinkrank_breadcrumb_styles(); | |
| 199 | - } | |
| 250 | + if (!is_array($identity)) { | |
| 251 | + global $wpdb; | |
| 252 | + $table = $wpdb->prefix . 'thinkrank_seo_settings'; | |
| 253 | + | |
| 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']; | |
| 200 | 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. | |
| 201 | 274 | } |
| 202 | -}, 100); | |
| 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'])) { | |
| 281 | + wp_enqueue_style( | |
| 282 | + 'thinkrank-breadcrumbs', | |
| 283 | + THINKRANK_PLUGIN_URL . 'static/css/breadcrumbs.css', | |
| 284 | + [], | |
| 285 | + THINKRANK_VERSION | |
| 286 | + ); | |
| 287 | + } | |
| 288 | + | |
| 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')); | |
| 296 | + | |
| 297 | + if ($hero_renders) { | |
| 298 | + wp_enqueue_style( | |
| 299 | + 'thinkrank-hero', | |
| 300 | + THINKRANK_PLUGIN_URL . 'static/css/hero.css', | |
| 301 | + [], | |
| 302 | + THINKRANK_VERSION | |
| 303 | + ); | |
| 304 | + } | |
| 305 | +}); | |