Changeset 3472173
- Timestamp:
- 03/01/2026 05:53:44 PM (5 weeks ago)
- Location:
- chatreact
- Files:
-
- 2 added
- 14 edited
- 1 copied
-
tags/1.1.2 (copied) (copied from chatreact/trunk)
-
tags/1.1.2/admin/class-admin.php (modified) (2 diffs)
-
tags/1.1.2/admin/views/admin-page.php (modified) (2 diffs)
-
tags/1.1.2/assets/js/admin.js (modified) (2 diffs)
-
tags/1.1.2/assets/screenshots (added)
-
tags/1.1.2/chatreact.php (modified) (3 diffs)
-
tags/1.1.2/includes/class-chatreact.php (modified) (5 diffs)
-
tags/1.1.2/includes/class-rest-api.php (modified) (2 diffs)
-
tags/1.1.2/readme.txt (modified) (2 diffs)
-
trunk/admin/class-admin.php (modified) (2 diffs)
-
trunk/admin/views/admin-page.php (modified) (2 diffs)
-
trunk/assets/js/admin.js (modified) (2 diffs)
-
trunk/assets/screenshots (added)
-
trunk/chatreact.php (modified) (3 diffs)
-
trunk/includes/class-chatreact.php (modified) (5 diffs)
-
trunk/includes/class-rest-api.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
chatreact/tags/1.1.2/admin/class-admin.php
r3468481 r3472173 63 63 add_action( 'wp_ajax_chatreact_save_integration_key', array( $this, 'ajax_save_integration_key' ) ); 64 64 add_action( 'wp_ajax_chatreact_save_sitemap_settings', array( $this, 'ajax_save_sitemap_settings' ) ); 65 add_action( 'wp_ajax_chatreact_clear_faq_cache', array( $this, 'ajax_clear_faq_cache' ) ); 66 add_action( 'wp_ajax_chatreact_save_faq_cache_settings', array( $this, 'ajax_save_faq_cache_settings' ) ); 65 67 // Note: wp_footer hook for render_assigned_widgets is registered in chatreact.php 66 68 // to ensure it works both in admin and frontend contexts … … 599 601 600 602 /** 603 * AJAX: Clear FAQ cache (transients + page caches) 604 */ 605 public function ajax_clear_faq_cache() { 606 check_ajax_referer( 'chatreact_admin_nonce', 'nonce' ); 607 608 if ( ! current_user_can( 'manage_options' ) ) { 609 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'chatreact' ) ) ); 610 } 611 612 ChatReact::clear_faq_cache(); 613 614 wp_send_json_success( array( 615 'message' => __( 'FAQ cache cleared successfully!', 'chatreact' ), 616 ) ); 617 } 618 619 /** 620 * AJAX: Save FAQ cache settings (TTL, llms.txt) 621 */ 622 public function ajax_save_faq_cache_settings() { 623 check_ajax_referer( 'chatreact_admin_nonce', 'nonce' ); 624 625 if ( ! current_user_can( 'manage_options' ) ) { 626 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'chatreact' ) ) ); 627 } 628 629 $ttl = isset( $_POST['cache_ttl'] ) ? intval( $_POST['cache_ttl'] ) : 21600; 630 $llms_enabled = isset( $_POST['llms_enabled'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_enabled'] ) ) : '1'; 631 $llms_chatbot_id = isset( $_POST['llms_chatbot_id'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_chatbot_id'] ) ) : ''; 632 633 update_option( 'chatreact_faq_cache_ttl', $ttl ); 634 update_option( 'chatreact_llms_txt_enabled', $llms_enabled ); 635 update_option( 'chatreact_llms_txt_chatbot_id', $llms_chatbot_id ); 636 637 // Flush rewrite rules when llms.txt setting changes 638 flush_rewrite_rules(); 639 640 // Clear cache so new TTL takes effect 641 ChatReact::clear_faq_cache(); 642 643 wp_send_json_success( array( 644 'message' => __( 'Settings saved!', 'chatreact' ), 645 ) ); 646 } 647 648 /** 601 649 * AJAX: Save sitemap settings (post types and priorities) 602 650 */ -
chatreact/tags/1.1.2/admin/views/admin-page.php
r3468481 r3472173 414 414 <!-- Tab Content: FAQ --> 415 415 <div id="tab-faq" class="chatreact-tab-content"> 416 <?php 417 $chatreact_faq_cache_ttl = (int) get_option( 'chatreact_faq_cache_ttl', 21600 ); 418 $chatreact_faq_cache_last = get_option( 'chatreact_faq_cache_last', '' ); 419 $chatreact_llms_enabled = get_option( 'chatreact_llms_txt_enabled', '1' ); 420 $chatreact_llms_chatbot_id = get_option( 'chatreact_llms_txt_chatbot_id', '' ); 421 ?> 416 422 <div class="chatreact-alert chatreact-alert-info"> 417 423 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> … … 419 425 <h4 class="chatreact-alert-title"><?php esc_html_e( 'FAQ Widget', 'chatreact' ); ?></h4> 420 426 <p class="chatreact-alert-text"><?php esc_html_e( 'Display your chatbot\'s FAQs as a beautiful accordion widget. FAQs are automatically synced from your ChatReact dashboard.', 'chatreact' ); ?></p> 427 </div> 428 </div> 429 430 <!-- FAQ Cache & SEO Settings --> 431 <div class="chatreact-card"> 432 <div class="chatreact-card-header"> 433 <div> 434 <h2 class="chatreact-card-title"> 435 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg> 436 <?php esc_html_e( 'Cache & SEO Settings', 'chatreact' ); ?> 437 </h2> 438 <p class="chatreact-card-subtitle"><?php esc_html_e( 'FAQ data is cached server-side for fast page loads and SEO visibility. JSON-LD structured data and static HTML are rendered for search engines and LLMs.', 'chatreact' ); ?></p> 439 </div> 440 <div> 441 <button type="button" id="chatreact-clear-faq-cache" class="chatreact-btn chatreact-btn-secondary chatreact-btn-sm"> 442 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg> 443 <?php esc_html_e( 'Clear Cache', 'chatreact' ); ?> 444 </button> 445 </div> 446 </div> 447 448 <?php if ( $chatreact_faq_cache_last ) : ?> 449 <div class="chatreact-endpoint-stats" style="margin-bottom: 16px;"> 450 <span> 451 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg> 452 <?php 453 printf( 454 /* translators: %s: last cache date */ 455 esc_html__( 'Last cached: %s', 'chatreact' ), 456 esc_html( $chatreact_faq_cache_last ) 457 ); 458 ?> 459 </span> 460 </div> 461 <?php endif; ?> 462 463 <div class="chatreact-integration-key-section"> 464 <label class="chatreact-label" for="chatreact-faq-cache-ttl"> 465 <?php esc_html_e( 'Cache Duration', 'chatreact' ); ?> 466 </label> 467 <select id="chatreact-faq-cache-ttl" class="chatreact-sitemap-priority-select" style="max-width: 300px;"> 468 <option value="3600" <?php selected( $chatreact_faq_cache_ttl, 3600 ); ?>><?php esc_html_e( '1 hour', 'chatreact' ); ?></option> 469 <option value="10800" <?php selected( $chatreact_faq_cache_ttl, 10800 ); ?>><?php esc_html_e( '3 hours', 'chatreact' ); ?></option> 470 <option value="21600" <?php selected( $chatreact_faq_cache_ttl, 21600 ); ?>><?php esc_html_e( '6 hours (recommended)', 'chatreact' ); ?></option> 471 <option value="43200" <?php selected( $chatreact_faq_cache_ttl, 43200 ); ?>><?php esc_html_e( '12 hours', 'chatreact' ); ?></option> 472 <option value="86400" <?php selected( $chatreact_faq_cache_ttl, 86400 ); ?>><?php esc_html_e( '24 hours', 'chatreact' ); ?></option> 473 <option value="0" <?php selected( $chatreact_faq_cache_ttl, 0 ); ?>><?php esc_html_e( 'Disabled (not recommended)', 'chatreact' ); ?></option> 474 </select> 475 <p class="chatreact-field-hint"><?php esc_html_e( 'How long FAQ data is cached locally. The cache is also cleared when triggered from the ChatReact dashboard.', 'chatreact' ); ?></p> 476 </div> 477 478 <div class="chatreact-integration-key-section" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e2e8f0;"> 479 <label class="chatreact-label"> 480 <?php esc_html_e( 'llms.txt for AI Crawlers', 'chatreact' ); ?> 481 </label> 482 <div style="margin-bottom: 12px;"> 483 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> 484 <input type="checkbox" id="chatreact-llms-enabled" value="1" <?php checked( $chatreact_llms_enabled, '1' ); ?> /> 485 <?php esc_html_e( 'Serve /llms.txt with FAQ content for LLMs (Claude, Perplexity, etc.)', 'chatreact' ); ?> 486 </label> 487 </div> 488 <div> 489 <label class="chatreact-label" for="chatreact-llms-chatbot-id" style="font-size: 13px;"> 490 <?php esc_html_e( 'Chatbot ID for llms.txt (leave empty to use first assigned chatbot)', 'chatreact' ); ?> 491 </label> 492 <input 493 type="text" 494 id="chatreact-llms-chatbot-id" 495 class="chatreact-input" 496 style="max-width: 400px;" 497 value="<?php echo esc_attr( $chatreact_llms_chatbot_id ); ?>" 498 placeholder="<?php esc_attr_e( 'Auto-detect from assignments', 'chatreact' ); ?>" 499 /> 500 </div> 501 <?php if ( '1' === $chatreact_llms_enabled ) : ?> 502 <div class="chatreact-endpoint-stats" style="margin-top: 8px;"> 503 <span> 504 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> 505 <code><?php echo esc_html( home_url( '/llms.txt' ) ); ?></code> 506 </span> 507 </div> 508 <?php endif; ?> 509 </div> 510 511 <div class="chatreact-sitemap-actions" style="margin-top: 16px;"> 512 <button type="button" id="chatreact-save-faq-cache-settings" class="chatreact-btn chatreact-btn-primary"> 513 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> 514 <?php esc_html_e( 'Save Settings', 'chatreact' ); ?> 515 </button> 421 516 </div> 422 517 </div> -
chatreact/tags/1.1.2/assets/js/admin.js
r3468481 r3472173 1119 1119 } 1120 1120 1121 // FAQ Cache & SEO Settings 1122 function initFaqCacheSettings() { 1123 // Clear FAQ cache 1124 $('#chatreact-clear-faq-cache').on('click', function() { 1125 var $btn = $(this); 1126 var originalHtml = $btn.html(); 1127 $btn.prop('disabled', true).html('<span class="chatreact-spinner"></span>'); 1128 1129 $.ajax({ 1130 url: chatreactAdmin.ajaxUrl, 1131 type: 'POST', 1132 data: { 1133 action: 'chatreact_clear_faq_cache', 1134 nonce: chatreactAdmin.nonce 1135 }, 1136 success: function(response) { 1137 if (response.success) { 1138 showNotice('success', response.data.message); 1139 } else { 1140 showNotice('error', response.data.message || 'Error'); 1141 } 1142 }, 1143 error: function() { 1144 showNotice('error', 'Error clearing cache'); 1145 }, 1146 complete: function() { 1147 $btn.prop('disabled', false).html(originalHtml); 1148 } 1149 }); 1150 }); 1151 1152 // Save FAQ cache settings 1153 $('#chatreact-save-faq-cache-settings').on('click', function() { 1154 var $btn = $(this); 1155 var originalHtml = $btn.html(); 1156 $btn.prop('disabled', true).html('<span class="chatreact-spinner"></span>'); 1157 1158 $.ajax({ 1159 url: chatreactAdmin.ajaxUrl, 1160 type: 'POST', 1161 data: { 1162 action: 'chatreact_save_faq_cache_settings', 1163 nonce: chatreactAdmin.nonce, 1164 cache_ttl: $('#chatreact-faq-cache-ttl').val(), 1165 llms_enabled: $('#chatreact-llms-enabled').is(':checked') ? '1' : '0', 1166 llms_chatbot_id: $('#chatreact-llms-chatbot-id').val() 1167 }, 1168 success: function(response) { 1169 if (response.success) { 1170 showNotice('success', response.data.message); 1171 } else { 1172 showNotice('error', response.data.message || 'Error'); 1173 } 1174 }, 1175 error: function() { 1176 showNotice('error', 'Error saving settings'); 1177 }, 1178 complete: function() { 1179 $btn.prop('disabled', false).html(originalHtml); 1180 } 1181 }); 1182 }); 1183 } 1184 1121 1185 // Initialize 1122 1186 $(document).ready(function() { … … 1130 1194 initPostTypesSettings(); 1131 1195 initIntegrationTab(); 1196 initFaqCacheSettings(); 1132 1197 }); 1133 1198 -
chatreact/tags/1.1.2/chatreact.php
r3469950 r3472173 4 4 * Plugin URI: https://www.chatreact.ai/docs/de/wordpress 5 5 * Description: Embed AI-powered chat widgets, contact forms, and FAQ accordions on your WordPress site. 6 * Version: 1.1. 16 * Version: 1.1.2 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 21 21 22 22 // Plugin constants 23 define( 'CHATREACT_VERSION', '1.1. 1' );23 define( 'CHATREACT_VERSION', '1.1.2' ); 24 24 define( 'CHATREACT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 25 25 define( 'CHATREACT_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 39 39 */ 40 40 function chatreact_init() { 41 load_plugin_textdomain( 'chatreact', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );42 43 41 // Initialize main plugin class 44 42 ChatReact::get_instance(); -
chatreact/tags/1.1.2/includes/class-chatreact.php
r3469381 r3472173 38 38 39 39 /** 40 * Cached FAQ data keyed by chatbot_id for JSON-LD output in wp_head 41 * 42 * @var array 43 */ 44 private static $page_faq_data = array(); 45 46 /** 40 47 * Get single instance of the class 41 48 * … … 54 61 private function __construct() { 55 62 $this->register_cache_exclusions(); 63 add_action( 'wp_footer', array( __CLASS__, 'output_faq_jsonld' ) ); 64 add_action( 'init', array( __CLASS__, 'register_llms_rewrite' ) ); 65 add_action( 'template_redirect', array( __CLASS__, 'handle_llms_txt_request' ) ); 56 66 } 57 67 … … 136 146 } 137 147 148 // ========================================================= 149 // Server-side FAQ Caching 150 // ========================================================= 151 152 /** 153 * @return int Cache TTL in seconds (default 6 hours). 154 */ 155 public static function get_cache_ttl() { 156 $ttl = (int) get_option( 'chatreact_faq_cache_ttl', 21600 ); 157 return $ttl > 0 ? $ttl : 0; 158 } 159 160 /** 161 * Fetch FAQ data from the ChatReact API with transient caching. 162 * 163 * @param string $chatbot_id Chatbot ID. 164 * @param string $lang Language code (optional). 165 * @param string $categories Comma-separated category slugs (optional). 166 * @return array|null Decoded API response or null on failure. 167 */ 168 public static function fetch_faqs_cached( $chatbot_id, $lang = '', $categories = '' ) { 169 $ttl = self::get_cache_ttl(); 170 171 $cache_key = 'cr_faqs_' . md5( $chatbot_id . '_' . $lang . '_' . $categories ); 172 173 if ( $ttl > 0 ) { 174 $cached = get_transient( $cache_key ); 175 if ( false !== $cached ) { 176 return $cached; 177 } 178 } 179 180 $api_url = self::get_api_url(); 181 $params = array(); 182 if ( $lang ) { 183 $params['lang'] = $lang; 184 } 185 if ( $categories ) { 186 $params['categories'] = $categories; 187 } 188 189 $url = $api_url . '/api/widget/' . rawurlencode( $chatbot_id ) . '/faqs'; 190 if ( ! empty( $params ) ) { 191 $url .= '?' . http_build_query( $params ); 192 } 193 194 $response = wp_remote_get( $url, array( 'timeout' => 15 ) ); 195 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { 196 return null; 197 } 198 199 $data = json_decode( wp_remote_retrieve_body( $response ), true ); 200 if ( ! is_array( $data ) ) { 201 return null; 202 } 203 204 if ( $ttl > 0 ) { 205 set_transient( $cache_key, $data, $ttl ); 206 update_option( 'chatreact_faq_cache_last', current_time( 'mysql' ) ); 207 } 208 209 return $data; 210 } 211 212 /** 213 * Fetch FAQ widget settings from the ChatReact API with transient caching. 214 * 215 * @param string $chatbot_id Chatbot ID. 216 * @param string $lang Language code (optional). 217 * @return array|null Decoded API response or null on failure. 218 */ 219 public static function fetch_faq_settings_cached( $chatbot_id, $lang = '' ) { 220 $ttl = self::get_cache_ttl(); 221 222 $cache_key = 'cr_faqset_' . md5( $chatbot_id . '_' . $lang ); 223 224 if ( $ttl > 0 ) { 225 $cached = get_transient( $cache_key ); 226 if ( false !== $cached ) { 227 return $cached; 228 } 229 } 230 231 $api_url = self::get_api_url(); 232 $lang_param = $lang ? '?lang=' . rawurlencode( $lang ) : ''; 233 234 $response = wp_remote_get( 235 $api_url . '/api/widget/' . rawurlencode( $chatbot_id ) . '/faqs/settings' . $lang_param, 236 array( 'timeout' => 15 ) 237 ); 238 239 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { 240 return null; 241 } 242 243 $data = json_decode( wp_remote_retrieve_body( $response ), true ); 244 if ( ! is_array( $data ) ) { 245 return null; 246 } 247 248 if ( $ttl > 0 ) { 249 set_transient( $cache_key, $data, $ttl ); 250 } 251 252 return $data; 253 } 254 255 // ========================================================= 256 // Cache Clearing & Page-Cache Compatibility 257 // ========================================================= 258 259 /** 260 * Delete all ChatReact FAQ-related transients. 261 */ 262 public static function clear_faq_cache() { 263 global $wpdb; 264 265 // Delete all transients that match our prefixes. 266 // WordPress stores transients as _transient_{key} and _transient_timeout_{key}. 267 $prefixes = array( 'cr_faqs_', 'cr_faqset_', 'cr_llms_' ); 268 foreach ( $prefixes as $prefix ) { 269 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 270 $wpdb->query( 271 $wpdb->prepare( 272 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", 273 '_transient_' . $prefix . '%', 274 '_transient_timeout_' . $prefix . '%' 275 ) 276 ); 277 } 278 279 delete_option( 'chatreact_faq_cache_last' ); 280 281 self::purge_page_caches(); 282 } 283 284 /** 285 * Purge page caches of popular third-party caching plugins so that 286 * server-rendered FAQ HTML and JSON-LD are refreshed. 287 */ 288 public static function purge_page_caches() { 289 // WP Rocket 290 if ( function_exists( 'rocket_clean_domain' ) ) { 291 rocket_clean_domain(); 292 } 293 // LiteSpeed Cache 294 if ( class_exists( 'LiteSpeed_Cache_API' ) ) { 295 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed's own hook 296 do_action( 'litespeed_purge_all' ); 297 } 298 // W3 Total Cache 299 if ( function_exists( 'w3tc_flush_all' ) ) { 300 w3tc_flush_all(); 301 } 302 // WP Super Cache 303 if ( function_exists( 'wp_cache_clear_cache' ) ) { 304 wp_cache_clear_cache(); 305 } 306 // WP Fastest Cache 307 if ( function_exists( 'wpfc_clear_all_cache' ) ) { 308 wpfc_clear_all_cache(); 309 } 310 // Cache Enabler 311 if ( class_exists( 'Cache_Enabler' ) && method_exists( 'Cache_Enabler', 'clear_total_cache' ) ) { 312 Cache_Enabler::clear_total_cache(); 313 } 314 // Autoptimize page cache 315 if ( function_exists( 'autoptimize_flush_pagecache' ) ) { 316 autoptimize_flush_pagecache(); 317 } 318 // WordPress built-in object cache 319 wp_cache_flush(); 320 } 321 322 // ========================================================= 323 // Server-side JSON-LD Rendering 324 // ========================================================= 325 326 /** 327 * Build FAQPage JSON-LD schema array. 328 * 329 * @param array $faqs Array of FAQ objects with question/answer keys. 330 * @return array Schema.org FAQPage structured data. 331 */ 332 public static function build_faq_jsonld( $faqs ) { 333 $entities = array(); 334 foreach ( $faqs as $faq ) { 335 $entities[] = array( 336 '@type' => 'Question', 337 'name' => $faq['question'], 338 'acceptedAnswer' => array( 339 '@type' => 'Answer', 340 'text' => wp_strip_all_tags( $faq['answer'] ), 341 ), 342 ); 343 } 344 345 return array( 346 '@context' => 'https://schema.org', 347 '@type' => 'FAQPage', 348 'mainEntity' => $entities, 349 ); 350 } 351 352 /** 353 * Output all collected FAQ JSON-LD blocks via wp_footer. 354 * Uses wp_footer because shortcodes run after wp_head, so data is not yet available in wp_head. 355 */ 356 public static function output_faq_jsonld() { 357 if ( empty( self::$page_faq_data ) ) { 358 return; 359 } 360 361 foreach ( self::$page_faq_data as $faqs ) { 362 if ( empty( $faqs ) ) { 363 continue; 364 } 365 $schema = self::build_faq_jsonld( $faqs ); 366 echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) . '</script>' . "\n"; 367 } 368 } 369 370 // ========================================================= 371 // SSR FAQ Styles (matches the JS widget CSS classes) 372 // ========================================================= 373 374 /** 375 * Generate inline CSS for SSR FAQ rendering that matches the JS widget. 376 * 377 * @param string $primary Primary accent color. 378 * @param string $text Text color. 379 * @param string $bg Background color. 380 * @param string $border Border color. 381 * @param int $radius Border radius in px. 382 * @return string <style> block. 383 */ 384 private static function get_ssr_faq_styles( $primary, $text, $bg, $border, $radius ) { 385 $primary_15 = $primary . '15'; 386 $primary_bg = $primary . '20'; 387 388 return '<style> 389 .cr-faq-container{font-family:inherit;width:100%;margin:0 auto} 390 .cr-faq-title{font-size:1.5rem;font-weight:700;color:' . esc_attr( $text ) . ';margin-bottom:1.5rem;text-align:center} 391 .cr-faq-categories{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1.5rem;justify-content:center} 392 .cr-faq-category-btn{font-family:inherit;padding:.5rem 1rem;border-radius:' . intval( $radius / 2 ) . 'px;border:1px solid ' . esc_attr( $border ) . ';background:' . esc_attr( $bg ) . ';color:' . esc_attr( $text ) . ';font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease} 393 .cr-faq-category-btn:hover{border-color:' . esc_attr( $primary ) . '} 394 .cr-faq-category-btn.active{background:' . esc_attr( $primary ) . ';border-color:' . esc_attr( $primary ) . ';color:#fff} 395 .cr-faq-list{display:flex;flex-direction:column;gap:.75rem} 396 .cr-faq-item{background:' . esc_attr( $bg ) . ';border:1px solid ' . esc_attr( $border ) . ';border-radius:' . intval( $radius ) . 'px;overflow:hidden;transition:box-shadow .2s ease} 397 .cr-faq-item:hover{box-shadow:0 4px 12px rgba(0,0,0,.1)} 398 .cr-faq-question{font-family:inherit;display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;cursor:pointer;font-weight:500;color:' . esc_attr( $text ) . ';background:transparent;border:none;width:100%;text-align:left;font-size:1rem;gap:1rem} 399 .cr-faq-question:hover{background:rgba(0,0,0,.02)} 400 .cr-faq-item.open .cr-faq-question{background:' . esc_attr( $primary_15 ) . '} 401 .cr-faq-question-text{flex:1} 402 .cr-faq-category-badge{font-size:.7rem;padding:.2rem .5rem;border-radius:999px;background:' . esc_attr( $primary_bg ) . ';color:' . esc_attr( $primary ) . ';font-weight:500;flex-shrink:0} 403 .cr-faq-icon{flex-shrink:0;width:20px;height:20px;transition:transform .2s ease;color:' . esc_attr( $primary ) . '} 404 .cr-faq-item.open .cr-faq-icon{transform:rotate(180deg)} 405 .cr-faq-answer{font-family:inherit;padding:0 1.25rem;max-height:0;overflow:hidden;transition:max-height .3s ease,padding .3s ease;color:' . esc_attr( $text ) . ';opacity:.8;line-height:1.6;font-size:.95rem;border-top:0 solid ' . esc_attr( $border ) . ';word-wrap:break-word} 406 .cr-faq-answer p{margin:.5rem 0}.cr-faq-answer p:first-child{margin-top:0}.cr-faq-answer p:last-child{margin-bottom:0} 407 .cr-faq-answer ul,.cr-faq-answer ol{margin:.5rem 0;padding-left:1.5rem}.cr-faq-answer li{margin:.25rem 0} 408 .cr-faq-answer a{color:' . esc_attr( $primary ) . ';text-decoration:underline} 409 .cr-faq-item.open .cr-faq-answer{padding:1rem 1.25rem;max-height:none;border-top-width:1px} 410 </style>'; 411 } 412 413 // ========================================================= 414 // llms.txt Support 415 // ========================================================= 416 417 /** 418 * Register rewrite rule for /llms.txt 419 */ 420 public static function register_llms_rewrite() { 421 add_rewrite_rule( '^llms\.txt/?$', 'index.php?chatreact_llms_txt=1', 'top' ); 422 add_filter( 'query_vars', function ( $vars ) { 423 $vars[] = 'chatreact_llms_txt'; 424 return $vars; 425 } ); 426 427 // Prevent WordPress from adding a trailing slash redirect 428 add_filter( 'redirect_canonical', function ( $redirect_url, $requested_url ) { 429 if ( preg_match( '/\/llms\.txt\/?$/i', $requested_url ) ) { 430 return false; 431 } 432 return $redirect_url; 433 }, 10, 2 ); 434 } 435 436 /** 437 * Handle /llms.txt requests by serving cached FAQ content as Markdown. 438 */ 439 public static function handle_llms_txt_request() { 440 if ( ! get_query_var( 'chatreact_llms_txt' ) ) { 441 return; 442 } 443 444 $enabled = get_option( 'chatreact_llms_txt_enabled', '1' ); 445 if ( '1' !== $enabled ) { 446 status_header( 404 ); 447 exit; 448 } 449 450 $chatbot_id = get_option( 'chatreact_llms_txt_chatbot_id', '' ); 451 if ( empty( $chatbot_id ) ) { 452 // Fall back to first chatbot found in widget assignments 453 $admin = ChatReact_Admin::get_instance(); 454 $assignments = $admin->get_assignments(); 455 foreach ( $assignments as $a ) { 456 if ( ! empty( $a['chatbot_id'] ) ) { 457 $chatbot_id = $a['chatbot_id']; 458 break; 459 } 460 } 461 } 462 463 if ( empty( $chatbot_id ) ) { 464 status_header( 404 ); 465 exit; 466 } 467 468 $ttl = self::get_cache_ttl(); 469 $cache_key = 'cr_llms_' . md5( $chatbot_id ); 470 $output = ( $ttl > 0 ) ? get_transient( $cache_key ) : false; 471 472 if ( false === $output ) { 473 $data = self::fetch_faqs_cached( $chatbot_id ); 474 $output = self::generate_llms_txt( $data ); 475 476 if ( $ttl > 0 ) { 477 set_transient( $cache_key, $output, $ttl ); 478 } 479 } 480 481 $max_age = $ttl > 0 ? $ttl : 21600; 482 header( 'Content-Type: text/plain; charset=utf-8' ); 483 header( 'Cache-Control: public, max-age=' . $max_age ); 484 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain text Markdown 485 exit; 486 } 487 488 /** 489 * Generate llms.txt Markdown content from FAQ data. 490 * 491 * @param array|null $data FAQ API response. 492 * @return string Markdown-formatted FAQ content. 493 */ 494 private static function generate_llms_txt( $data ) { 495 $site_name = get_bloginfo( 'name' ); 496 $site_desc = get_bloginfo( 'description' ); 497 498 $lines = array(); 499 $lines[] = '# ' . $site_name; 500 if ( $site_desc ) { 501 $lines[] = ''; 502 $lines[] = '> ' . $site_desc; 503 } 504 $lines[] = ''; 505 $lines[] = '## Frequently Asked Questions'; 506 $lines[] = ''; 507 508 if ( empty( $data ) || empty( $data['faqs'] ) ) { 509 $lines[] = 'No FAQs available.'; 510 return implode( "\n", $lines ); 511 } 512 513 $faqs = $data['faqs']; 514 $categories = isset( $data['categories'] ) ? $data['categories'] : array(); 515 516 // Group FAQs by category 517 $grouped = array(); 518 $uncategorized = array(); 519 520 foreach ( $faqs as $faq ) { 521 if ( ! empty( $faq['category']['id'] ) ) { 522 $cat_id = $faq['category']['id']; 523 if ( ! isset( $grouped[ $cat_id ] ) ) { 524 $grouped[ $cat_id ] = array( 525 'name' => $faq['category']['name'], 526 'faqs' => array(), 527 ); 528 } 529 $grouped[ $cat_id ]['faqs'][] = $faq; 530 } else { 531 $uncategorized[] = $faq; 532 } 533 } 534 535 // Output categorized FAQs 536 foreach ( $grouped as $group ) { 537 $lines[] = '### ' . $group['name']; 538 $lines[] = ''; 539 foreach ( $group['faqs'] as $faq ) { 540 $lines[] = '**Q: ' . $faq['question'] . '**'; 541 $lines[] = 'A: ' . wp_strip_all_tags( $faq['answer'] ); 542 $lines[] = ''; 543 } 544 } 545 546 // Output uncategorized FAQs 547 if ( ! empty( $uncategorized ) ) { 548 if ( ! empty( $grouped ) ) { 549 $lines[] = '### General'; 550 $lines[] = ''; 551 } 552 foreach ( $uncategorized as $faq ) { 553 $lines[] = '**Q: ' . $faq['question'] . '**'; 554 $lines[] = 'A: ' . wp_strip_all_tags( $faq['answer'] ); 555 $lines[] = ''; 556 } 557 } 558 559 return implode( "\n", $lines ); 560 } 561 138 562 /** 139 563 * Ensure the script_loader_tag filter is registered … … 264 688 265 689 /** 266 * Render an FAQ widget 267 * 268 * @param string $chatbot_id The chatbot ID 269 * @param array $options Optional widget options 270 * @return string HTML output (container div) 690 * Render an FAQ widget with server-side rendered HTML for SEO. 691 * 692 * The static HTML inside the container uses native <details>/<summary> 693 * elements so that crawlers and users without JS still see the content. 694 * The client-side faq-widget.js replaces the container contents with the 695 * interactive version once loaded. 696 * 697 * @param string $chatbot_id The chatbot ID. 698 * @param array $options Optional widget options. 699 * @return string HTML output (container div with SSR FAQ content). 271 700 */ 272 701 public static function render_faq_widget( $chatbot_id, $options = array() ) { … … 323 752 self::$script_data[ $handle ] = $data; 324 753 325 // Return only the container div 326 return sprintf( '<div id="%s"></div>', esc_attr( $container_id ) ); 327 } 754 // ------ Server-side rendered FAQ content ------ 755 $faq_data = self::fetch_faqs_cached( $chatbot_id, $options['language'], $options['categories'] ); 756 $settings = self::fetch_faq_settings_cached( $chatbot_id, $options['language'] ); 757 758 $ssr_html = ''; 759 760 if ( ! empty( $faq_data ) && ! empty( $faq_data['faqs'] ) ) { 761 $faqs = $faq_data['faqs']; 762 $title = isset( $settings['title'] ) ? $settings['title'] : 'Frequently Asked Questions'; 763 $show_title = isset( $settings['showTitle'] ) ? $settings['showTitle'] : true; 764 765 $primary_color = isset( $settings['primaryColor'] ) ? $settings['primaryColor'] : '#f59e0b'; 766 $text_color = isset( $settings['textColor'] ) ? $settings['textColor'] : '#1f2937'; 767 $bg_color = isset( $settings['backgroundColor'] ) ? $settings['backgroundColor'] : '#ffffff'; 768 $border_color = isset( $settings['borderColor'] ) ? $settings['borderColor'] : '#e5e7eb'; 769 $border_radius = isset( $settings['borderRadius'] ) ? (int) $settings['borderRadius'] : 12; 770 771 // Store for JSON-LD output in wp_footer 772 self::$page_faq_data[ $chatbot_id ] = $faqs; 773 774 $ssr_html .= self::get_ssr_faq_styles( $primary_color, $text_color, $bg_color, $border_color, $border_radius ); 775 776 $ssr_html .= '<div class="cr-faq-container" data-ssr="1">'; 777 778 if ( $show_title ) { 779 $ssr_html .= '<h2 class="cr-faq-title">' . esc_html( $title ) . '</h2>'; 780 } 781 782 // Group FAQs by category for category pill rendering 783 $categories = isset( $faq_data['categories'] ) ? $faq_data['categories'] : array(); 784 if ( ! empty( $categories ) ) { 785 $ssr_html .= '<div class="cr-faq-categories">'; 786 $all_label = isset( $settings['i18n']['all'] ) ? $settings['i18n']['all'] : __( 'All', 'chatreact' ); 787 $ssr_html .= '<button class="cr-faq-category-btn active" data-category="all">' . esc_html( $all_label ) . '</button>'; 788 foreach ( $categories as $cat ) { 789 $ssr_html .= '<button class="cr-faq-category-btn" data-category="' . esc_attr( $cat['id'] ) . '">' . esc_html( $cat['name'] ) . '</button>'; 790 } 791 $ssr_html .= '</div>'; 792 } 793 794 $ssr_html .= '<div class="cr-faq-list">'; 795 foreach ( $faqs as $faq ) { 796 $cat_id = ! empty( $faq['category']['id'] ) ? esc_attr( $faq['category']['id'] ) : ''; 797 $cat_name = ! empty( $faq['category']['name'] ) ? $faq['category']['name'] : ''; 798 799 $ssr_html .= '<div class="cr-faq-item" data-category="' . $cat_id . '">'; 800 $ssr_html .= '<button class="cr-faq-question" onclick="this.parentElement.classList.toggle(\'open\')">'; 801 $ssr_html .= '<span class="cr-faq-question-text">' . esc_html( $faq['question'] ) . '</span>'; 802 if ( $cat_name ) { 803 $ssr_html .= '<span class="cr-faq-category-badge">' . esc_html( $cat_name ) . '</span>'; 804 } 805 $ssr_html .= '<svg class="cr-faq-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>'; 806 $ssr_html .= '</button>'; 807 $ssr_html .= '<div class="cr-faq-answer">' . wp_kses_post( $faq['answer'] ) . '</div>'; 808 $ssr_html .= '</div>'; 809 } 810 $ssr_html .= '</div>'; 811 $ssr_html .= '</div>'; 812 813 // Inline category filtering script (works without external JS, yields to widget when it loads) 814 if ( ! empty( $categories ) ) { 815 $ssr_html .= '<script> 816 (function(){ 817 var c=document.getElementById("' . esc_js( $container_id ) . '"); 818 if(!c)return; 819 c.addEventListener("click",function(e){ 820 if(!c.querySelector("[data-ssr]"))return; 821 var b=e.target.closest(".cr-faq-category-btn"); 822 if(!b)return; 823 var cat=b.getAttribute("data-category"); 824 var btns=c.querySelectorAll(".cr-faq-category-btn"); 825 var items=c.querySelectorAll(".cr-faq-item"); 826 btns.forEach(function(x){x.classList.remove("active")}); 827 b.classList.add("active"); 828 items.forEach(function(item){ 829 if(cat==="all"||item.getAttribute("data-category")===cat){ 830 item.style.display=""; 831 }else{ 832 item.style.display="none"; 328 833 } 834 }); 835 }); 836 })(); 837 </script>'; 838 } 839 } 840 841 return sprintf( 842 '<div id="%s">%s</div>', 843 esc_attr( $container_id ), 844 $ssr_html 845 ); 846 } 847 } -
chatreact/tags/1.1.2/includes/class-rest-api.php
r3468481 r3472173 32 32 'permission_callback' => array( $this, 'verify_api_key' ), 33 33 ) ); 34 35 register_rest_route( 'chatreact/v1', '/purge-faq-cache', array( 36 'methods' => 'POST', 37 'callback' => array( $this, 'purge_faq_cache' ), 38 'permission_callback' => array( $this, 'verify_api_key' ), 39 ) ); 34 40 } 35 41 … … 58 64 59 65 return true; 66 } 67 68 /** 69 * Remotely purge the FAQ cache. 70 * Called by the ChatReact backend when FAQs are updated. 71 * 72 * @return WP_REST_Response 73 */ 74 public function purge_faq_cache() { 75 ChatReact::clear_faq_cache(); 76 77 return rest_ensure_response( array( 78 'success' => true, 79 'message' => 'FAQ cache cleared', 80 ) ); 60 81 } 61 82 -
chatreact/tags/1.1.2/readme.txt
r3469950 r3472173 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1.1. 17 Stable tag: 1.1.2 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 168 168 169 169 == Changelog == 170 171 = 1.1.2 = 172 * Added: Server-side rendering (SSR) for FAQ widgets — FAQs are visible to search engines and LLMs without JavaScript 173 * Added: JSON-LD structured data (FAQPage schema) for improved SEO and rich results 174 * Added: llms.txt endpoint for AI/LLM discoverability 175 * Added: Remote cache purge — FAQ cache is automatically cleared when content changes in the ChatReact Dashboard 176 * Added: Inline styling and category filtering for SSR FAQs (works without external JavaScript) 177 * Fixed: Widget no longer shows "Loading" flash when server-side content is already present 178 * Fixed: Inline filtering script yields to the JavaScript widget to prevent conflicts 179 * Fixed: Plugin Check compliance (removed deprecated load_plugin_textdomain call, removed hidden files) 180 * Improved: FAQ i18n labels (e.g. "All"/"Alle") now use API settings instead of hardcoded English 170 181 171 182 = 1.1.1 = -
chatreact/trunk/admin/class-admin.php
r3468481 r3472173 63 63 add_action( 'wp_ajax_chatreact_save_integration_key', array( $this, 'ajax_save_integration_key' ) ); 64 64 add_action( 'wp_ajax_chatreact_save_sitemap_settings', array( $this, 'ajax_save_sitemap_settings' ) ); 65 add_action( 'wp_ajax_chatreact_clear_faq_cache', array( $this, 'ajax_clear_faq_cache' ) ); 66 add_action( 'wp_ajax_chatreact_save_faq_cache_settings', array( $this, 'ajax_save_faq_cache_settings' ) ); 65 67 // Note: wp_footer hook for render_assigned_widgets is registered in chatreact.php 66 68 // to ensure it works both in admin and frontend contexts … … 599 601 600 602 /** 603 * AJAX: Clear FAQ cache (transients + page caches) 604 */ 605 public function ajax_clear_faq_cache() { 606 check_ajax_referer( 'chatreact_admin_nonce', 'nonce' ); 607 608 if ( ! current_user_can( 'manage_options' ) ) { 609 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'chatreact' ) ) ); 610 } 611 612 ChatReact::clear_faq_cache(); 613 614 wp_send_json_success( array( 615 'message' => __( 'FAQ cache cleared successfully!', 'chatreact' ), 616 ) ); 617 } 618 619 /** 620 * AJAX: Save FAQ cache settings (TTL, llms.txt) 621 */ 622 public function ajax_save_faq_cache_settings() { 623 check_ajax_referer( 'chatreact_admin_nonce', 'nonce' ); 624 625 if ( ! current_user_can( 'manage_options' ) ) { 626 wp_send_json_error( array( 'message' => __( 'Permission denied.', 'chatreact' ) ) ); 627 } 628 629 $ttl = isset( $_POST['cache_ttl'] ) ? intval( $_POST['cache_ttl'] ) : 21600; 630 $llms_enabled = isset( $_POST['llms_enabled'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_enabled'] ) ) : '1'; 631 $llms_chatbot_id = isset( $_POST['llms_chatbot_id'] ) ? sanitize_text_field( wp_unslash( $_POST['llms_chatbot_id'] ) ) : ''; 632 633 update_option( 'chatreact_faq_cache_ttl', $ttl ); 634 update_option( 'chatreact_llms_txt_enabled', $llms_enabled ); 635 update_option( 'chatreact_llms_txt_chatbot_id', $llms_chatbot_id ); 636 637 // Flush rewrite rules when llms.txt setting changes 638 flush_rewrite_rules(); 639 640 // Clear cache so new TTL takes effect 641 ChatReact::clear_faq_cache(); 642 643 wp_send_json_success( array( 644 'message' => __( 'Settings saved!', 'chatreact' ), 645 ) ); 646 } 647 648 /** 601 649 * AJAX: Save sitemap settings (post types and priorities) 602 650 */ -
chatreact/trunk/admin/views/admin-page.php
r3468481 r3472173 414 414 <!-- Tab Content: FAQ --> 415 415 <div id="tab-faq" class="chatreact-tab-content"> 416 <?php 417 $chatreact_faq_cache_ttl = (int) get_option( 'chatreact_faq_cache_ttl', 21600 ); 418 $chatreact_faq_cache_last = get_option( 'chatreact_faq_cache_last', '' ); 419 $chatreact_llms_enabled = get_option( 'chatreact_llms_txt_enabled', '1' ); 420 $chatreact_llms_chatbot_id = get_option( 'chatreact_llms_txt_chatbot_id', '' ); 421 ?> 416 422 <div class="chatreact-alert chatreact-alert-info"> 417 423 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> … … 419 425 <h4 class="chatreact-alert-title"><?php esc_html_e( 'FAQ Widget', 'chatreact' ); ?></h4> 420 426 <p class="chatreact-alert-text"><?php esc_html_e( 'Display your chatbot\'s FAQs as a beautiful accordion widget. FAQs are automatically synced from your ChatReact dashboard.', 'chatreact' ); ?></p> 427 </div> 428 </div> 429 430 <!-- FAQ Cache & SEO Settings --> 431 <div class="chatreact-card"> 432 <div class="chatreact-card-header"> 433 <div> 434 <h2 class="chatreact-card-title"> 435 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg> 436 <?php esc_html_e( 'Cache & SEO Settings', 'chatreact' ); ?> 437 </h2> 438 <p class="chatreact-card-subtitle"><?php esc_html_e( 'FAQ data is cached server-side for fast page loads and SEO visibility. JSON-LD structured data and static HTML are rendered for search engines and LLMs.', 'chatreact' ); ?></p> 439 </div> 440 <div> 441 <button type="button" id="chatreact-clear-faq-cache" class="chatreact-btn chatreact-btn-secondary chatreact-btn-sm"> 442 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg> 443 <?php esc_html_e( 'Clear Cache', 'chatreact' ); ?> 444 </button> 445 </div> 446 </div> 447 448 <?php if ( $chatreact_faq_cache_last ) : ?> 449 <div class="chatreact-endpoint-stats" style="margin-bottom: 16px;"> 450 <span> 451 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg> 452 <?php 453 printf( 454 /* translators: %s: last cache date */ 455 esc_html__( 'Last cached: %s', 'chatreact' ), 456 esc_html( $chatreact_faq_cache_last ) 457 ); 458 ?> 459 </span> 460 </div> 461 <?php endif; ?> 462 463 <div class="chatreact-integration-key-section"> 464 <label class="chatreact-label" for="chatreact-faq-cache-ttl"> 465 <?php esc_html_e( 'Cache Duration', 'chatreact' ); ?> 466 </label> 467 <select id="chatreact-faq-cache-ttl" class="chatreact-sitemap-priority-select" style="max-width: 300px;"> 468 <option value="3600" <?php selected( $chatreact_faq_cache_ttl, 3600 ); ?>><?php esc_html_e( '1 hour', 'chatreact' ); ?></option> 469 <option value="10800" <?php selected( $chatreact_faq_cache_ttl, 10800 ); ?>><?php esc_html_e( '3 hours', 'chatreact' ); ?></option> 470 <option value="21600" <?php selected( $chatreact_faq_cache_ttl, 21600 ); ?>><?php esc_html_e( '6 hours (recommended)', 'chatreact' ); ?></option> 471 <option value="43200" <?php selected( $chatreact_faq_cache_ttl, 43200 ); ?>><?php esc_html_e( '12 hours', 'chatreact' ); ?></option> 472 <option value="86400" <?php selected( $chatreact_faq_cache_ttl, 86400 ); ?>><?php esc_html_e( '24 hours', 'chatreact' ); ?></option> 473 <option value="0" <?php selected( $chatreact_faq_cache_ttl, 0 ); ?>><?php esc_html_e( 'Disabled (not recommended)', 'chatreact' ); ?></option> 474 </select> 475 <p class="chatreact-field-hint"><?php esc_html_e( 'How long FAQ data is cached locally. The cache is also cleared when triggered from the ChatReact dashboard.', 'chatreact' ); ?></p> 476 </div> 477 478 <div class="chatreact-integration-key-section" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e2e8f0;"> 479 <label class="chatreact-label"> 480 <?php esc_html_e( 'llms.txt for AI Crawlers', 'chatreact' ); ?> 481 </label> 482 <div style="margin-bottom: 12px;"> 483 <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;"> 484 <input type="checkbox" id="chatreact-llms-enabled" value="1" <?php checked( $chatreact_llms_enabled, '1' ); ?> /> 485 <?php esc_html_e( 'Serve /llms.txt with FAQ content for LLMs (Claude, Perplexity, etc.)', 'chatreact' ); ?> 486 </label> 487 </div> 488 <div> 489 <label class="chatreact-label" for="chatreact-llms-chatbot-id" style="font-size: 13px;"> 490 <?php esc_html_e( 'Chatbot ID for llms.txt (leave empty to use first assigned chatbot)', 'chatreact' ); ?> 491 </label> 492 <input 493 type="text" 494 id="chatreact-llms-chatbot-id" 495 class="chatreact-input" 496 style="max-width: 400px;" 497 value="<?php echo esc_attr( $chatreact_llms_chatbot_id ); ?>" 498 placeholder="<?php esc_attr_e( 'Auto-detect from assignments', 'chatreact' ); ?>" 499 /> 500 </div> 501 <?php if ( '1' === $chatreact_llms_enabled ) : ?> 502 <div class="chatreact-endpoint-stats" style="margin-top: 8px;"> 503 <span> 504 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> 505 <code><?php echo esc_html( home_url( '/llms.txt' ) ); ?></code> 506 </span> 507 </div> 508 <?php endif; ?> 509 </div> 510 511 <div class="chatreact-sitemap-actions" style="margin-top: 16px;"> 512 <button type="button" id="chatreact-save-faq-cache-settings" class="chatreact-btn chatreact-btn-primary"> 513 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg> 514 <?php esc_html_e( 'Save Settings', 'chatreact' ); ?> 515 </button> 421 516 </div> 422 517 </div> -
chatreact/trunk/assets/js/admin.js
r3468481 r3472173 1119 1119 } 1120 1120 1121 // FAQ Cache & SEO Settings 1122 function initFaqCacheSettings() { 1123 // Clear FAQ cache 1124 $('#chatreact-clear-faq-cache').on('click', function() { 1125 var $btn = $(this); 1126 var originalHtml = $btn.html(); 1127 $btn.prop('disabled', true).html('<span class="chatreact-spinner"></span>'); 1128 1129 $.ajax({ 1130 url: chatreactAdmin.ajaxUrl, 1131 type: 'POST', 1132 data: { 1133 action: 'chatreact_clear_faq_cache', 1134 nonce: chatreactAdmin.nonce 1135 }, 1136 success: function(response) { 1137 if (response.success) { 1138 showNotice('success', response.data.message); 1139 } else { 1140 showNotice('error', response.data.message || 'Error'); 1141 } 1142 }, 1143 error: function() { 1144 showNotice('error', 'Error clearing cache'); 1145 }, 1146 complete: function() { 1147 $btn.prop('disabled', false).html(originalHtml); 1148 } 1149 }); 1150 }); 1151 1152 // Save FAQ cache settings 1153 $('#chatreact-save-faq-cache-settings').on('click', function() { 1154 var $btn = $(this); 1155 var originalHtml = $btn.html(); 1156 $btn.prop('disabled', true).html('<span class="chatreact-spinner"></span>'); 1157 1158 $.ajax({ 1159 url: chatreactAdmin.ajaxUrl, 1160 type: 'POST', 1161 data: { 1162 action: 'chatreact_save_faq_cache_settings', 1163 nonce: chatreactAdmin.nonce, 1164 cache_ttl: $('#chatreact-faq-cache-ttl').val(), 1165 llms_enabled: $('#chatreact-llms-enabled').is(':checked') ? '1' : '0', 1166 llms_chatbot_id: $('#chatreact-llms-chatbot-id').val() 1167 }, 1168 success: function(response) { 1169 if (response.success) { 1170 showNotice('success', response.data.message); 1171 } else { 1172 showNotice('error', response.data.message || 'Error'); 1173 } 1174 }, 1175 error: function() { 1176 showNotice('error', 'Error saving settings'); 1177 }, 1178 complete: function() { 1179 $btn.prop('disabled', false).html(originalHtml); 1180 } 1181 }); 1182 }); 1183 } 1184 1121 1185 // Initialize 1122 1186 $(document).ready(function() { … … 1130 1194 initPostTypesSettings(); 1131 1195 initIntegrationTab(); 1196 initFaqCacheSettings(); 1132 1197 }); 1133 1198 -
chatreact/trunk/chatreact.php
r3469950 r3472173 4 4 * Plugin URI: https://www.chatreact.ai/docs/de/wordpress 5 5 * Description: Embed AI-powered chat widgets, contact forms, and FAQ accordions on your WordPress site. 6 * Version: 1.1. 16 * Version: 1.1.2 7 7 * Requires at least: 5.8 8 8 * Requires PHP: 7.4 … … 21 21 22 22 // Plugin constants 23 define( 'CHATREACT_VERSION', '1.1. 1' );23 define( 'CHATREACT_VERSION', '1.1.2' ); 24 24 define( 'CHATREACT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 25 25 define( 'CHATREACT_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 39 39 */ 40 40 function chatreact_init() { 41 load_plugin_textdomain( 'chatreact', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );42 43 41 // Initialize main plugin class 44 42 ChatReact::get_instance(); -
chatreact/trunk/includes/class-chatreact.php
r3469381 r3472173 38 38 39 39 /** 40 * Cached FAQ data keyed by chatbot_id for JSON-LD output in wp_head 41 * 42 * @var array 43 */ 44 private static $page_faq_data = array(); 45 46 /** 40 47 * Get single instance of the class 41 48 * … … 54 61 private function __construct() { 55 62 $this->register_cache_exclusions(); 63 add_action( 'wp_footer', array( __CLASS__, 'output_faq_jsonld' ) ); 64 add_action( 'init', array( __CLASS__, 'register_llms_rewrite' ) ); 65 add_action( 'template_redirect', array( __CLASS__, 'handle_llms_txt_request' ) ); 56 66 } 57 67 … … 136 146 } 137 147 148 // ========================================================= 149 // Server-side FAQ Caching 150 // ========================================================= 151 152 /** 153 * @return int Cache TTL in seconds (default 6 hours). 154 */ 155 public static function get_cache_ttl() { 156 $ttl = (int) get_option( 'chatreact_faq_cache_ttl', 21600 ); 157 return $ttl > 0 ? $ttl : 0; 158 } 159 160 /** 161 * Fetch FAQ data from the ChatReact API with transient caching. 162 * 163 * @param string $chatbot_id Chatbot ID. 164 * @param string $lang Language code (optional). 165 * @param string $categories Comma-separated category slugs (optional). 166 * @return array|null Decoded API response or null on failure. 167 */ 168 public static function fetch_faqs_cached( $chatbot_id, $lang = '', $categories = '' ) { 169 $ttl = self::get_cache_ttl(); 170 171 $cache_key = 'cr_faqs_' . md5( $chatbot_id . '_' . $lang . '_' . $categories ); 172 173 if ( $ttl > 0 ) { 174 $cached = get_transient( $cache_key ); 175 if ( false !== $cached ) { 176 return $cached; 177 } 178 } 179 180 $api_url = self::get_api_url(); 181 $params = array(); 182 if ( $lang ) { 183 $params['lang'] = $lang; 184 } 185 if ( $categories ) { 186 $params['categories'] = $categories; 187 } 188 189 $url = $api_url . '/api/widget/' . rawurlencode( $chatbot_id ) . '/faqs'; 190 if ( ! empty( $params ) ) { 191 $url .= '?' . http_build_query( $params ); 192 } 193 194 $response = wp_remote_get( $url, array( 'timeout' => 15 ) ); 195 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { 196 return null; 197 } 198 199 $data = json_decode( wp_remote_retrieve_body( $response ), true ); 200 if ( ! is_array( $data ) ) { 201 return null; 202 } 203 204 if ( $ttl > 0 ) { 205 set_transient( $cache_key, $data, $ttl ); 206 update_option( 'chatreact_faq_cache_last', current_time( 'mysql' ) ); 207 } 208 209 return $data; 210 } 211 212 /** 213 * Fetch FAQ widget settings from the ChatReact API with transient caching. 214 * 215 * @param string $chatbot_id Chatbot ID. 216 * @param string $lang Language code (optional). 217 * @return array|null Decoded API response or null on failure. 218 */ 219 public static function fetch_faq_settings_cached( $chatbot_id, $lang = '' ) { 220 $ttl = self::get_cache_ttl(); 221 222 $cache_key = 'cr_faqset_' . md5( $chatbot_id . '_' . $lang ); 223 224 if ( $ttl > 0 ) { 225 $cached = get_transient( $cache_key ); 226 if ( false !== $cached ) { 227 return $cached; 228 } 229 } 230 231 $api_url = self::get_api_url(); 232 $lang_param = $lang ? '?lang=' . rawurlencode( $lang ) : ''; 233 234 $response = wp_remote_get( 235 $api_url . '/api/widget/' . rawurlencode( $chatbot_id ) . '/faqs/settings' . $lang_param, 236 array( 'timeout' => 15 ) 237 ); 238 239 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { 240 return null; 241 } 242 243 $data = json_decode( wp_remote_retrieve_body( $response ), true ); 244 if ( ! is_array( $data ) ) { 245 return null; 246 } 247 248 if ( $ttl > 0 ) { 249 set_transient( $cache_key, $data, $ttl ); 250 } 251 252 return $data; 253 } 254 255 // ========================================================= 256 // Cache Clearing & Page-Cache Compatibility 257 // ========================================================= 258 259 /** 260 * Delete all ChatReact FAQ-related transients. 261 */ 262 public static function clear_faq_cache() { 263 global $wpdb; 264 265 // Delete all transients that match our prefixes. 266 // WordPress stores transients as _transient_{key} and _transient_timeout_{key}. 267 $prefixes = array( 'cr_faqs_', 'cr_faqset_', 'cr_llms_' ); 268 foreach ( $prefixes as $prefix ) { 269 // phpcs:ignore WordPress.DB.DirectDatabaseQuery 270 $wpdb->query( 271 $wpdb->prepare( 272 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", 273 '_transient_' . $prefix . '%', 274 '_transient_timeout_' . $prefix . '%' 275 ) 276 ); 277 } 278 279 delete_option( 'chatreact_faq_cache_last' ); 280 281 self::purge_page_caches(); 282 } 283 284 /** 285 * Purge page caches of popular third-party caching plugins so that 286 * server-rendered FAQ HTML and JSON-LD are refreshed. 287 */ 288 public static function purge_page_caches() { 289 // WP Rocket 290 if ( function_exists( 'rocket_clean_domain' ) ) { 291 rocket_clean_domain(); 292 } 293 // LiteSpeed Cache 294 if ( class_exists( 'LiteSpeed_Cache_API' ) ) { 295 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed's own hook 296 do_action( 'litespeed_purge_all' ); 297 } 298 // W3 Total Cache 299 if ( function_exists( 'w3tc_flush_all' ) ) { 300 w3tc_flush_all(); 301 } 302 // WP Super Cache 303 if ( function_exists( 'wp_cache_clear_cache' ) ) { 304 wp_cache_clear_cache(); 305 } 306 // WP Fastest Cache 307 if ( function_exists( 'wpfc_clear_all_cache' ) ) { 308 wpfc_clear_all_cache(); 309 } 310 // Cache Enabler 311 if ( class_exists( 'Cache_Enabler' ) && method_exists( 'Cache_Enabler', 'clear_total_cache' ) ) { 312 Cache_Enabler::clear_total_cache(); 313 } 314 // Autoptimize page cache 315 if ( function_exists( 'autoptimize_flush_pagecache' ) ) { 316 autoptimize_flush_pagecache(); 317 } 318 // WordPress built-in object cache 319 wp_cache_flush(); 320 } 321 322 // ========================================================= 323 // Server-side JSON-LD Rendering 324 // ========================================================= 325 326 /** 327 * Build FAQPage JSON-LD schema array. 328 * 329 * @param array $faqs Array of FAQ objects with question/answer keys. 330 * @return array Schema.org FAQPage structured data. 331 */ 332 public static function build_faq_jsonld( $faqs ) { 333 $entities = array(); 334 foreach ( $faqs as $faq ) { 335 $entities[] = array( 336 '@type' => 'Question', 337 'name' => $faq['question'], 338 'acceptedAnswer' => array( 339 '@type' => 'Answer', 340 'text' => wp_strip_all_tags( $faq['answer'] ), 341 ), 342 ); 343 } 344 345 return array( 346 '@context' => 'https://schema.org', 347 '@type' => 'FAQPage', 348 'mainEntity' => $entities, 349 ); 350 } 351 352 /** 353 * Output all collected FAQ JSON-LD blocks via wp_footer. 354 * Uses wp_footer because shortcodes run after wp_head, so data is not yet available in wp_head. 355 */ 356 public static function output_faq_jsonld() { 357 if ( empty( self::$page_faq_data ) ) { 358 return; 359 } 360 361 foreach ( self::$page_faq_data as $faqs ) { 362 if ( empty( $faqs ) ) { 363 continue; 364 } 365 $schema = self::build_faq_jsonld( $faqs ); 366 echo '<script type="application/ld+json">' . wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) . '</script>' . "\n"; 367 } 368 } 369 370 // ========================================================= 371 // SSR FAQ Styles (matches the JS widget CSS classes) 372 // ========================================================= 373 374 /** 375 * Generate inline CSS for SSR FAQ rendering that matches the JS widget. 376 * 377 * @param string $primary Primary accent color. 378 * @param string $text Text color. 379 * @param string $bg Background color. 380 * @param string $border Border color. 381 * @param int $radius Border radius in px. 382 * @return string <style> block. 383 */ 384 private static function get_ssr_faq_styles( $primary, $text, $bg, $border, $radius ) { 385 $primary_15 = $primary . '15'; 386 $primary_bg = $primary . '20'; 387 388 return '<style> 389 .cr-faq-container{font-family:inherit;width:100%;margin:0 auto} 390 .cr-faq-title{font-size:1.5rem;font-weight:700;color:' . esc_attr( $text ) . ';margin-bottom:1.5rem;text-align:center} 391 .cr-faq-categories{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1.5rem;justify-content:center} 392 .cr-faq-category-btn{font-family:inherit;padding:.5rem 1rem;border-radius:' . intval( $radius / 2 ) . 'px;border:1px solid ' . esc_attr( $border ) . ';background:' . esc_attr( $bg ) . ';color:' . esc_attr( $text ) . ';font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s ease} 393 .cr-faq-category-btn:hover{border-color:' . esc_attr( $primary ) . '} 394 .cr-faq-category-btn.active{background:' . esc_attr( $primary ) . ';border-color:' . esc_attr( $primary ) . ';color:#fff} 395 .cr-faq-list{display:flex;flex-direction:column;gap:.75rem} 396 .cr-faq-item{background:' . esc_attr( $bg ) . ';border:1px solid ' . esc_attr( $border ) . ';border-radius:' . intval( $radius ) . 'px;overflow:hidden;transition:box-shadow .2s ease} 397 .cr-faq-item:hover{box-shadow:0 4px 12px rgba(0,0,0,.1)} 398 .cr-faq-question{font-family:inherit;display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;cursor:pointer;font-weight:500;color:' . esc_attr( $text ) . ';background:transparent;border:none;width:100%;text-align:left;font-size:1rem;gap:1rem} 399 .cr-faq-question:hover{background:rgba(0,0,0,.02)} 400 .cr-faq-item.open .cr-faq-question{background:' . esc_attr( $primary_15 ) . '} 401 .cr-faq-question-text{flex:1} 402 .cr-faq-category-badge{font-size:.7rem;padding:.2rem .5rem;border-radius:999px;background:' . esc_attr( $primary_bg ) . ';color:' . esc_attr( $primary ) . ';font-weight:500;flex-shrink:0} 403 .cr-faq-icon{flex-shrink:0;width:20px;height:20px;transition:transform .2s ease;color:' . esc_attr( $primary ) . '} 404 .cr-faq-item.open .cr-faq-icon{transform:rotate(180deg)} 405 .cr-faq-answer{font-family:inherit;padding:0 1.25rem;max-height:0;overflow:hidden;transition:max-height .3s ease,padding .3s ease;color:' . esc_attr( $text ) . ';opacity:.8;line-height:1.6;font-size:.95rem;border-top:0 solid ' . esc_attr( $border ) . ';word-wrap:break-word} 406 .cr-faq-answer p{margin:.5rem 0}.cr-faq-answer p:first-child{margin-top:0}.cr-faq-answer p:last-child{margin-bottom:0} 407 .cr-faq-answer ul,.cr-faq-answer ol{margin:.5rem 0;padding-left:1.5rem}.cr-faq-answer li{margin:.25rem 0} 408 .cr-faq-answer a{color:' . esc_attr( $primary ) . ';text-decoration:underline} 409 .cr-faq-item.open .cr-faq-answer{padding:1rem 1.25rem;max-height:none;border-top-width:1px} 410 </style>'; 411 } 412 413 // ========================================================= 414 // llms.txt Support 415 // ========================================================= 416 417 /** 418 * Register rewrite rule for /llms.txt 419 */ 420 public static function register_llms_rewrite() { 421 add_rewrite_rule( '^llms\.txt/?$', 'index.php?chatreact_llms_txt=1', 'top' ); 422 add_filter( 'query_vars', function ( $vars ) { 423 $vars[] = 'chatreact_llms_txt'; 424 return $vars; 425 } ); 426 427 // Prevent WordPress from adding a trailing slash redirect 428 add_filter( 'redirect_canonical', function ( $redirect_url, $requested_url ) { 429 if ( preg_match( '/\/llms\.txt\/?$/i', $requested_url ) ) { 430 return false; 431 } 432 return $redirect_url; 433 }, 10, 2 ); 434 } 435 436 /** 437 * Handle /llms.txt requests by serving cached FAQ content as Markdown. 438 */ 439 public static function handle_llms_txt_request() { 440 if ( ! get_query_var( 'chatreact_llms_txt' ) ) { 441 return; 442 } 443 444 $enabled = get_option( 'chatreact_llms_txt_enabled', '1' ); 445 if ( '1' !== $enabled ) { 446 status_header( 404 ); 447 exit; 448 } 449 450 $chatbot_id = get_option( 'chatreact_llms_txt_chatbot_id', '' ); 451 if ( empty( $chatbot_id ) ) { 452 // Fall back to first chatbot found in widget assignments 453 $admin = ChatReact_Admin::get_instance(); 454 $assignments = $admin->get_assignments(); 455 foreach ( $assignments as $a ) { 456 if ( ! empty( $a['chatbot_id'] ) ) { 457 $chatbot_id = $a['chatbot_id']; 458 break; 459 } 460 } 461 } 462 463 if ( empty( $chatbot_id ) ) { 464 status_header( 404 ); 465 exit; 466 } 467 468 $ttl = self::get_cache_ttl(); 469 $cache_key = 'cr_llms_' . md5( $chatbot_id ); 470 $output = ( $ttl > 0 ) ? get_transient( $cache_key ) : false; 471 472 if ( false === $output ) { 473 $data = self::fetch_faqs_cached( $chatbot_id ); 474 $output = self::generate_llms_txt( $data ); 475 476 if ( $ttl > 0 ) { 477 set_transient( $cache_key, $output, $ttl ); 478 } 479 } 480 481 $max_age = $ttl > 0 ? $ttl : 21600; 482 header( 'Content-Type: text/plain; charset=utf-8' ); 483 header( 'Cache-Control: public, max-age=' . $max_age ); 484 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain text Markdown 485 exit; 486 } 487 488 /** 489 * Generate llms.txt Markdown content from FAQ data. 490 * 491 * @param array|null $data FAQ API response. 492 * @return string Markdown-formatted FAQ content. 493 */ 494 private static function generate_llms_txt( $data ) { 495 $site_name = get_bloginfo( 'name' ); 496 $site_desc = get_bloginfo( 'description' ); 497 498 $lines = array(); 499 $lines[] = '# ' . $site_name; 500 if ( $site_desc ) { 501 $lines[] = ''; 502 $lines[] = '> ' . $site_desc; 503 } 504 $lines[] = ''; 505 $lines[] = '## Frequently Asked Questions'; 506 $lines[] = ''; 507 508 if ( empty( $data ) || empty( $data['faqs'] ) ) { 509 $lines[] = 'No FAQs available.'; 510 return implode( "\n", $lines ); 511 } 512 513 $faqs = $data['faqs']; 514 $categories = isset( $data['categories'] ) ? $data['categories'] : array(); 515 516 // Group FAQs by category 517 $grouped = array(); 518 $uncategorized = array(); 519 520 foreach ( $faqs as $faq ) { 521 if ( ! empty( $faq['category']['id'] ) ) { 522 $cat_id = $faq['category']['id']; 523 if ( ! isset( $grouped[ $cat_id ] ) ) { 524 $grouped[ $cat_id ] = array( 525 'name' => $faq['category']['name'], 526 'faqs' => array(), 527 ); 528 } 529 $grouped[ $cat_id ]['faqs'][] = $faq; 530 } else { 531 $uncategorized[] = $faq; 532 } 533 } 534 535 // Output categorized FAQs 536 foreach ( $grouped as $group ) { 537 $lines[] = '### ' . $group['name']; 538 $lines[] = ''; 539 foreach ( $group['faqs'] as $faq ) { 540 $lines[] = '**Q: ' . $faq['question'] . '**'; 541 $lines[] = 'A: ' . wp_strip_all_tags( $faq['answer'] ); 542 $lines[] = ''; 543 } 544 } 545 546 // Output uncategorized FAQs 547 if ( ! empty( $uncategorized ) ) { 548 if ( ! empty( $grouped ) ) { 549 $lines[] = '### General'; 550 $lines[] = ''; 551 } 552 foreach ( $uncategorized as $faq ) { 553 $lines[] = '**Q: ' . $faq['question'] . '**'; 554 $lines[] = 'A: ' . wp_strip_all_tags( $faq['answer'] ); 555 $lines[] = ''; 556 } 557 } 558 559 return implode( "\n", $lines ); 560 } 561 138 562 /** 139 563 * Ensure the script_loader_tag filter is registered … … 264 688 265 689 /** 266 * Render an FAQ widget 267 * 268 * @param string $chatbot_id The chatbot ID 269 * @param array $options Optional widget options 270 * @return string HTML output (container div) 690 * Render an FAQ widget with server-side rendered HTML for SEO. 691 * 692 * The static HTML inside the container uses native <details>/<summary> 693 * elements so that crawlers and users without JS still see the content. 694 * The client-side faq-widget.js replaces the container contents with the 695 * interactive version once loaded. 696 * 697 * @param string $chatbot_id The chatbot ID. 698 * @param array $options Optional widget options. 699 * @return string HTML output (container div with SSR FAQ content). 271 700 */ 272 701 public static function render_faq_widget( $chatbot_id, $options = array() ) { … … 323 752 self::$script_data[ $handle ] = $data; 324 753 325 // Return only the container div 326 return sprintf( '<div id="%s"></div>', esc_attr( $container_id ) ); 327 } 754 // ------ Server-side rendered FAQ content ------ 755 $faq_data = self::fetch_faqs_cached( $chatbot_id, $options['language'], $options['categories'] ); 756 $settings = self::fetch_faq_settings_cached( $chatbot_id, $options['language'] ); 757 758 $ssr_html = ''; 759 760 if ( ! empty( $faq_data ) && ! empty( $faq_data['faqs'] ) ) { 761 $faqs = $faq_data['faqs']; 762 $title = isset( $settings['title'] ) ? $settings['title'] : 'Frequently Asked Questions'; 763 $show_title = isset( $settings['showTitle'] ) ? $settings['showTitle'] : true; 764 765 $primary_color = isset( $settings['primaryColor'] ) ? $settings['primaryColor'] : '#f59e0b'; 766 $text_color = isset( $settings['textColor'] ) ? $settings['textColor'] : '#1f2937'; 767 $bg_color = isset( $settings['backgroundColor'] ) ? $settings['backgroundColor'] : '#ffffff'; 768 $border_color = isset( $settings['borderColor'] ) ? $settings['borderColor'] : '#e5e7eb'; 769 $border_radius = isset( $settings['borderRadius'] ) ? (int) $settings['borderRadius'] : 12; 770 771 // Store for JSON-LD output in wp_footer 772 self::$page_faq_data[ $chatbot_id ] = $faqs; 773 774 $ssr_html .= self::get_ssr_faq_styles( $primary_color, $text_color, $bg_color, $border_color, $border_radius ); 775 776 $ssr_html .= '<div class="cr-faq-container" data-ssr="1">'; 777 778 if ( $show_title ) { 779 $ssr_html .= '<h2 class="cr-faq-title">' . esc_html( $title ) . '</h2>'; 780 } 781 782 // Group FAQs by category for category pill rendering 783 $categories = isset( $faq_data['categories'] ) ? $faq_data['categories'] : array(); 784 if ( ! empty( $categories ) ) { 785 $ssr_html .= '<div class="cr-faq-categories">'; 786 $all_label = isset( $settings['i18n']['all'] ) ? $settings['i18n']['all'] : __( 'All', 'chatreact' ); 787 $ssr_html .= '<button class="cr-faq-category-btn active" data-category="all">' . esc_html( $all_label ) . '</button>'; 788 foreach ( $categories as $cat ) { 789 $ssr_html .= '<button class="cr-faq-category-btn" data-category="' . esc_attr( $cat['id'] ) . '">' . esc_html( $cat['name'] ) . '</button>'; 790 } 791 $ssr_html .= '</div>'; 792 } 793 794 $ssr_html .= '<div class="cr-faq-list">'; 795 foreach ( $faqs as $faq ) { 796 $cat_id = ! empty( $faq['category']['id'] ) ? esc_attr( $faq['category']['id'] ) : ''; 797 $cat_name = ! empty( $faq['category']['name'] ) ? $faq['category']['name'] : ''; 798 799 $ssr_html .= '<div class="cr-faq-item" data-category="' . $cat_id . '">'; 800 $ssr_html .= '<button class="cr-faq-question" onclick="this.parentElement.classList.toggle(\'open\')">'; 801 $ssr_html .= '<span class="cr-faq-question-text">' . esc_html( $faq['question'] ) . '</span>'; 802 if ( $cat_name ) { 803 $ssr_html .= '<span class="cr-faq-category-badge">' . esc_html( $cat_name ) . '</span>'; 804 } 805 $ssr_html .= '<svg class="cr-faq-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>'; 806 $ssr_html .= '</button>'; 807 $ssr_html .= '<div class="cr-faq-answer">' . wp_kses_post( $faq['answer'] ) . '</div>'; 808 $ssr_html .= '</div>'; 809 } 810 $ssr_html .= '</div>'; 811 $ssr_html .= '</div>'; 812 813 // Inline category filtering script (works without external JS, yields to widget when it loads) 814 if ( ! empty( $categories ) ) { 815 $ssr_html .= '<script> 816 (function(){ 817 var c=document.getElementById("' . esc_js( $container_id ) . '"); 818 if(!c)return; 819 c.addEventListener("click",function(e){ 820 if(!c.querySelector("[data-ssr]"))return; 821 var b=e.target.closest(".cr-faq-category-btn"); 822 if(!b)return; 823 var cat=b.getAttribute("data-category"); 824 var btns=c.querySelectorAll(".cr-faq-category-btn"); 825 var items=c.querySelectorAll(".cr-faq-item"); 826 btns.forEach(function(x){x.classList.remove("active")}); 827 b.classList.add("active"); 828 items.forEach(function(item){ 829 if(cat==="all"||item.getAttribute("data-category")===cat){ 830 item.style.display=""; 831 }else{ 832 item.style.display="none"; 328 833 } 834 }); 835 }); 836 })(); 837 </script>'; 838 } 839 } 840 841 return sprintf( 842 '<div id="%s">%s</div>', 843 esc_attr( $container_id ), 844 $ssr_html 845 ); 846 } 847 } -
chatreact/trunk/includes/class-rest-api.php
r3468481 r3472173 32 32 'permission_callback' => array( $this, 'verify_api_key' ), 33 33 ) ); 34 35 register_rest_route( 'chatreact/v1', '/purge-faq-cache', array( 36 'methods' => 'POST', 37 'callback' => array( $this, 'purge_faq_cache' ), 38 'permission_callback' => array( $this, 'verify_api_key' ), 39 ) ); 34 40 } 35 41 … … 58 64 59 65 return true; 66 } 67 68 /** 69 * Remotely purge the FAQ cache. 70 * Called by the ChatReact backend when FAQs are updated. 71 * 72 * @return WP_REST_Response 73 */ 74 public function purge_faq_cache() { 75 ChatReact::clear_faq_cache(); 76 77 return rest_ensure_response( array( 78 'success' => true, 79 'message' => 'FAQ cache cleared', 80 ) ); 60 81 } 61 82 -
chatreact/trunk/readme.txt
r3469950 r3472173 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1.1. 17 Stable tag: 1.1.2 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 168 168 169 169 == Changelog == 170 171 = 1.1.2 = 172 * Added: Server-side rendering (SSR) for FAQ widgets — FAQs are visible to search engines and LLMs without JavaScript 173 * Added: JSON-LD structured data (FAQPage schema) for improved SEO and rich results 174 * Added: llms.txt endpoint for AI/LLM discoverability 175 * Added: Remote cache purge — FAQ cache is automatically cleared when content changes in the ChatReact Dashboard 176 * Added: Inline styling and category filtering for SSR FAQs (works without external JavaScript) 177 * Fixed: Widget no longer shows "Loading" flash when server-side content is already present 178 * Fixed: Inline filtering script yields to the JavaScript widget to prevent conflicts 179 * Fixed: Plugin Check compliance (removed deprecated load_plugin_textdomain call, removed hidden files) 180 * Improved: FAQ i18n labels (e.g. "All"/"Alle") now use API settings instead of hardcoded English 170 181 171 182 = 1.1.1 =
Note: See TracChangeset
for help on using the changeset viewer.