Changeset 3429715
- Timestamp:
- 12/30/2025 04:02:30 PM (2 months ago)
- Location:
- awesome-footnotes
- Files:
-
- 6 added
- 10 edited
-
tags/3.9.2/classes/controllers/class-post-settings.php (modified) (13 diffs)
-
tags/3.9.2/classes/helpers/class-settings.php (modified) (7 diffs)
-
tags/3.9.2/classes/settings/class-settings-builder.php (modified) (1 diff)
-
tags/3.9.2/classes/settings/settings-options/faq-post.php (added)
-
tags/3.9.2/classes/settings/settings-options/options.php (modified) (1 diff)
-
tags/3.9.2/css/faq-schema.css (added)
-
tags/3.9.2/js/admin/quick-edit.js (added)
-
tags/3.9.2/readme.txt (modified) (1 diff)
-
trunk/classes/controllers/class-post-settings.php (modified) (13 diffs)
-
trunk/classes/helpers/class-settings.php (modified) (7 diffs)
-
trunk/classes/settings/class-settings-builder.php (modified) (1 diff)
-
trunk/classes/settings/settings-options/faq-post.php (added)
-
trunk/classes/settings/settings-options/options.php (modified) (1 diff)
-
trunk/css/faq-schema.css (added)
-
trunk/js/admin/quick-edit.js (added)
-
trunk/readme.txt (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
awesome-footnotes/tags/3.9.2/classes/controllers/class-post-settings.php
r3422097 r3429715 36 36 */ 37 37 public const POST_PRIMARY_CATEGORY = '_awef_primary_category'; 38 39 /** 40 * Meta key for FAQ schema data. 41 */ 42 public const POST_FAQ_SCHEMA = '_awef_post_faq_schema'; 38 43 39 44 /** … … 100 105 101 106 \add_filter( 'TieLabs/meta_title', array( __CLASS__, 'get_meta_title' ) ); 107 // Use the post-level SEO title for the document <title> when available. 108 \add_filter( 'pre_get_document_title', array( __CLASS__, 'get_meta_title' ) ); 109 // Fallback for themes/plugins using the older `wp_title` filter. 110 \add_filter( 'wp_title', array( __CLASS__, 'get_meta_title' ), 10, 1 ); 102 111 \add_filter( 'TieLabs/meta_description', array( __CLASS__, 'get_meta_description' ) ); 103 112 … … 105 114 106 115 \add_filter( 'get_the_excerpt', array( __CLASS__, 'get_meta_description' ), 999, 2 ); 116 117 // Register FAQ shortcode. 118 \add_shortcode( 'awef_faq', array( __CLASS__, 'render_faq_shortcode' ) ); 119 120 // Add FAQ JSON-LD schema to <head>. 121 \add_action( 'wp_head', array( __CLASS__, 'output_faq_schema' ), 10 ); 122 123 // Add quick edit functionality for SEO fields if enabled. 124 if ( Settings::get_current_options()['seo_post_options'] ) { 125 self::init_quick_edit_hooks(); 126 } 127 } 128 129 /** 130 * Initialize quick edit hooks for SEO fields 131 * 132 * @since 3.8.0 133 */ 134 private static function init_quick_edit_hooks() { 135 // Get post types that support footnotes 136 $global_options = Settings::get_current_options(); 137 $post_types = isset( $global_options['post_types'] ) && is_array( $global_options['post_types'] ) 138 ? $global_options['post_types'] 139 : array( 'post', 'page' ); 140 141 // Add quick edit fields for each post type 142 foreach ( $post_types as $post_type ) { 143 \add_action( "quick_edit_custom_box", array( __CLASS__, 'add_quick_edit_fields' ), 10, 2 ); 144 \add_action( "bulk_edit_custom_box", array( __CLASS__, 'add_bulk_edit_fields' ), 10, 2 ); 145 // Add a hidden column to trigger data hooks 146 \add_filter( "manage_{$post_type}_posts_columns", array( __CLASS__, 'add_seo_column' ) ); 147 } 148 149 // Save quick edit data 150 \add_action( 'wp_ajax_save_bulk_edit_awef_seo', array( __CLASS__, 'save_bulk_edit' ) ); 151 \add_action( 'save_post', array( __CLASS__, 'save_quick_edit' ) ); 152 153 // Add admin scripts for quick edit 154 \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_quick_edit_scripts' ) ); 155 156 // Add inline data to posts list 157 \add_action( 'manage_posts_custom_column', array( __CLASS__, 'add_quick_edit_data' ), 10, 2 ); 158 \add_action( 'manage_pages_custom_column', array( __CLASS__, 'add_quick_edit_data' ), 10, 2 ); 159 } 160 161 /** 162 * Add quick edit fields for SEO 163 * 164 * @param string $column_name Column name 165 * @param string $post_type Post type 166 * @since 3.8.0 167 */ 168 public static function add_quick_edit_fields( $column_name, $post_type ) { 169 // Get post types that support footnotes 170 $global_options = Settings::get_current_options(); 171 $supported_post_types = isset( $global_options['post_types'] ) && is_array( $global_options['post_types'] ) 172 ? $global_options['post_types'] 173 : array( 'post', 'page' ); 174 175 if ( ! in_array( $post_type, $supported_post_types, true ) ) { 176 return; 177 } 178 179 // Only add fields once (WordPress calls this multiple times for different columns) 180 static $added = false; 181 if ( $added ) { 182 return; 183 } 184 $added = true; 185 186 ?> 187 <fieldset class="inline-edit-col-right"> 188 <div class="inline-edit-col"> 189 <h4><?php echo esc_html( AWEF_NAME . ' ' . __( 'SEO Settings', 'awesome-footnotes' ) ); ?></h4> 190 <label> 191 <span class="title"><?php esc_html_e( 'SEO Title', 'awesome-footnotes' ); ?></span> 192 <span class="input-text-wrap"> 193 <input type="text" name="awef_seo_title" class="awef-seo-title" value="" /> 194 </span> 195 </label> 196 <label> 197 <span class="title"><?php esc_html_e( 'SEO Description', 'awesome-footnotes' ); ?></span> 198 <span class="input-text-wrap"> 199 <textarea name="awef_seo_description" class="awef-seo-description" rows="3"></textarea> 200 </span> 201 </label> 202 </div> 203 </fieldset> 204 <?php 205 } 206 207 /** 208 * Add bulk edit fields for SEO 209 * 210 * @param string $column_name Column name 211 * @param string $post_type Post type 212 * @since 3.8.0 213 */ 214 public static function add_bulk_edit_fields( $column_name, $post_type ) { 215 // Get post types that support footnotes 216 $global_options = Settings::get_current_options(); 217 $supported_post_types = isset( $global_options['post_types'] ) && is_array( $global_options['post_types'] ) 218 ? $global_options['post_types'] 219 : array( 'post', 'page' ); 220 221 if ( ! in_array( $post_type, $supported_post_types, true ) ) { 222 return; 223 } 224 225 // Only add fields once 226 static $added = false; 227 if ( $added ) { 228 return; 229 } 230 $added = true; 231 232 ?> 233 <fieldset class="inline-edit-col-right"> 234 <div class="inline-edit-col"> 235 <h4><?php echo esc_html( AWEF_NAME . ' ' . __( 'SEO Settings', 'awesome-footnotes' ) ); ?></h4> 236 <label> 237 <span class="title"><?php esc_html_e( 'SEO Title', 'awesome-footnotes' ); ?></span> 238 <span class="input-text-wrap"> 239 <select name="awef_seo_title_bulk_action" class="awef-seo-title-bulk-action"> 240 <option value=""><?php esc_html_e( '— No Change —', 'awesome-footnotes' ); ?></option> 241 <option value="set"><?php esc_html_e( 'Set to:', 'awesome-footnotes' ); ?></option> 242 <option value="clear"><?php esc_html_e( 'Clear', 'awesome-footnotes' ); ?></option> 243 </select> 244 <input type="text" name="awef_seo_title" class="awef-seo-title" value="" style="display:none;" /> 245 </span> 246 </label> 247 <label> 248 <span class="title"><?php esc_html_e( 'SEO Description', 'awesome-footnotes' ); ?></span> 249 <span class="input-text-wrap"> 250 <select name="awef_seo_description_bulk_action" class="awef-seo-description-bulk-action"> 251 <option value=""><?php esc_html_e( '— No Change —', 'awesome-footnotes' ); ?></option> 252 <option value="set"><?php esc_html_e( 'Set to:', 'awesome-footnotes' ); ?></option> 253 <option value="clear"><?php esc_html_e( 'Clear', 'awesome-footnotes' ); ?></option> 254 </select> 255 <textarea name="awef_seo_description" class="awef-seo-description" rows="3" style="display:none;"></textarea> 256 </span> 257 </label> 258 </div> 259 </fieldset> 260 <?php 261 } 262 263 /** 264 * Save quick edit data 265 * 266 * @param int $post_id Post ID 267 * @since 3.8.0 268 */ 269 public static function save_quick_edit( $post_id ) { 270 // Check if this is a quick edit request 271 if ( ! isset( $_POST['_inline_edit'] ) ) { 272 return; 273 } 274 275 // Verify nonce 276 if ( ! isset( $_POST['_inline_edit'] ) || ! \wp_verify_nonce( $_POST['_inline_edit'], 'inlineeditnonce' ) ) { 277 return; 278 } 279 280 // Check permissions 281 if ( ! \current_user_can( 'edit_post', $post_id ) ) { 282 return; 283 } 284 285 // Handle SEO title 286 if ( isset( $_POST['awef_seo_title'] ) ) { 287 $seo_title = \sanitize_text_field( \wp_unslash( $_POST['awef_seo_title'] ) ); 288 $post = \get_post( $post_id ); 289 290 if ( ! empty( $seo_title ) && $seo_title !== \get_the_title( $post_id ) ) { 291 \update_post_meta( $post_id, self::POST_SEO_TITLE, $seo_title ); 292 } else { 293 \delete_post_meta( $post_id, self::POST_SEO_TITLE ); 294 } 295 } 296 297 // Handle SEO description 298 if ( isset( $_POST['awef_seo_description'] ) ) { 299 $seo_description = \sanitize_textarea_field( \wp_unslash( $_POST['awef_seo_description'] ) ); 300 301 if ( ! empty( $seo_description ) ) { 302 $post = \get_post( $post_id ); 303 $post->post_excerpt = self::the_short_content( 160, $seo_description ); 304 305 \remove_all_actions( 'save_post' ); 306 \wp_update_post( $post ); 307 \add_action( 'save_post', array( __CLASS__, 'save_quick_edit' ) ); 308 \add_action( 'save_post', array( __CLASS__, 'save' ) ); 309 } 310 } 311 } 312 313 /** 314 * Save bulk edit data 315 * 316 * @since 3.8.0 317 */ 318 public static function save_bulk_edit() { 319 // Verify nonce 320 if ( ! isset( $_POST['_wpnonce'] ) || ! \wp_verify_nonce( $_POST['_wpnonce'], 'bulk-posts' ) ) { 321 \wp_die( 'Security check failed' ); 322 } 323 324 // Check permissions 325 if ( ! \current_user_can( 'edit_posts' ) ) { 326 \wp_die( 'Insufficient permissions' ); 327 } 328 329 $post_ids = array(); 330 if ( isset( $_POST['post_ids'] ) && is_array( $_POST['post_ids'] ) ) { 331 $post_ids = array_map( 'intval', $_POST['post_ids'] ); 332 } 333 334 if ( empty( $post_ids ) ) { 335 \wp_die( 'No posts selected' ); 336 } 337 338 foreach ( $post_ids as $post_id ) { 339 if ( ! \current_user_can( 'edit_post', $post_id ) ) { 340 continue; 341 } 342 343 // Handle SEO title bulk action 344 if ( isset( $_POST['awef_seo_title_bulk_action'] ) ) { 345 $title_action = \sanitize_text_field( $_POST['awef_seo_title_bulk_action'] ); 346 347 if ( 'set' === $title_action && isset( $_POST['awef_seo_title'] ) ) { 348 $seo_title = \sanitize_text_field( \wp_unslash( $_POST['awef_seo_title'] ) ); 349 if ( ! empty( $seo_title ) ) { 350 \update_post_meta( $post_id, self::POST_SEO_TITLE, $seo_title ); 351 } 352 } elseif ( 'clear' === $title_action ) { 353 \delete_post_meta( $post_id, self::POST_SEO_TITLE ); 354 } 355 } 356 357 // Handle SEO description bulk action 358 if ( isset( $_POST['awef_seo_description_bulk_action'] ) ) { 359 $desc_action = \sanitize_text_field( $_POST['awef_seo_description_bulk_action'] ); 360 361 if ( 'set' === $desc_action && isset( $_POST['awef_seo_description'] ) ) { 362 $seo_description = \sanitize_textarea_field( \wp_unslash( $_POST['awef_seo_description'] ) ); 363 if ( ! empty( $seo_description ) ) { 364 $post = \get_post( $post_id ); 365 $post->post_excerpt = self::the_short_content( 160, $seo_description ); 366 \wp_update_post( $post ); 367 } 368 } elseif ( 'clear' === $desc_action ) { 369 $post = \get_post( $post_id ); 370 $post->post_excerpt = ''; 371 \wp_update_post( $post ); 372 } 373 } 374 } 375 376 \wp_die(); // This is required to terminate immediately and return a proper response 377 } 378 379 /** 380 * Enqueue scripts for quick edit functionality 381 * 382 * @param string $hook_suffix Current admin page hook suffix 383 * @since 3.8.0 384 */ 385 public static function enqueue_quick_edit_scripts( $hook_suffix ) { 386 if ( 'edit.php' !== $hook_suffix ) { 387 return; 388 } 389 390 // Get current screen 391 $screen = \get_current_screen(); 392 if ( ! $screen ) { 393 return; 394 } 395 396 // Check if this is a supported post type 397 $global_options = Settings::get_current_options(); 398 $supported_post_types = isset( $global_options['post_types'] ) && is_array( $global_options['post_types'] ) 399 ? $global_options['post_types'] 400 : array( 'post', 'page' ); 401 402 if ( ! in_array( $screen->post_type, $supported_post_types, true ) ) { 403 return; 404 } 405 406 \wp_enqueue_script( 407 'awef-quick-edit', 408 \AWEF_PLUGIN_ROOT_URL . 'js/admin/quick-edit.js', 409 array( 'jquery', 'inline-edit-post' ), 410 \AWEF_VERSION, 411 true 412 ); 413 414 \wp_localize_script( 415 'awef-quick-edit', 416 'awefQuickEdit', 417 array( 418 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), 419 'nonce' => \wp_create_nonce( 'awef_quick_edit' ), 420 ) 421 ); 422 423 // Register and enqueue style for hiding the SEO data column 424 \wp_register_style( 'awef-admin-quick-edit', false ); 425 \wp_enqueue_style( 'awef-admin-quick-edit' ); 426 \wp_add_inline_style( 427 'awef-admin-quick-edit', 428 '.column-awef_seo_data { display: none !important; width: 0 !important; }' 429 ); 430 } 431 432 /** 433 * Add a hidden SEO column to posts list (to trigger data hooks) 434 * 435 * @param array $columns Existing columns 436 * @return array Modified columns 437 * @since 3.8.0 438 */ 439 public static function add_seo_column( $columns ) { 440 $columns['awef_seo_data'] = ''; 441 return $columns; 442 } 443 444 /** 445 * Add inline data for quick edit 446 * 447 * @param string $column Column name 448 * @param int $post_id Post ID 449 * @since 3.8.0 450 */ 451 public static function add_quick_edit_data( $column, $post_id ) { 452 // Only process our hidden column 453 if ( 'awef_seo_data' !== $column ) { 454 return; 455 } 456 457 $seo_title = \get_post_meta( $post_id, self::POST_SEO_TITLE, true ); 458 $post = \get_post( $post_id ); 459 $seo_description = ! empty( $post->post_excerpt ) ? $post->post_excerpt : ''; 460 461 ?> 462 <div class="awef-quick-edit-data" data-post-id="<?php echo esc_attr( $post_id ); ?>" style="display:none;"> 463 <div class="seo-title"><?php echo esc_attr( $seo_title ); ?></div> 464 <div class="seo-description"><?php echo esc_attr( $seo_description ); ?></div> 465 </div> 466 <?php 107 467 } 108 468 … … 128 488 129 489 // Security: verify nonce and user capability. 130 if ( ! isset( $_POST['awef_post_settings_nonce'] ) || ! \wp_verify_nonce( $_POST['awef_post_settings_nonce'], 'awef_post_settings' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 131 return; 132 } 133 134 if ( ! \current_user_can( ' edit_post', $post_id ) ) {490 if ( ! isset( $_POST['awef_post_settings_nonce'] ) || ! \wp_verify_nonce( $_POST['awef_post_settings_nonce'], 'awef_post_settings' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 491 return; 492 } 493 494 if ( ! \current_user_can( 'manage_options', $post_id ) ) { 135 495 return; 136 496 } … … 139 499 140 500 // Collect and sanitize full submitted settings once. 141 $raw_settings = isset( $_POST[ \AWEF_SETTINGS_NAME ] ) ? \stripslashes_deep( $_POST[ \AWEF_SETTINGS_NAME ] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing 501 $raw_settings = isset( $_POST[ \AWEF_SETTINGS_NAME ] ) ? \stripslashes_deep( $_POST[ \AWEF_SETTINGS_NAME ] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 142 502 $all_settings_collected = Settings::collect_and_sanitize_options( $raw_settings ); 143 503 … … 154 514 ARRAY_FILTER_USE_KEY 155 515 ); 516 517 // Handle FAQ schema data. 518 self::save_faq_schema( $post_id ); 156 519 157 520 // TOC subset for overrides. … … 200 563 $primary_taxonomy = self::get_primary_category_taxonomy( \get_post_type( $post_id ) ); 201 564 if ( $primary_taxonomy ) { 202 $primary_raw = isset( $_POST[ \AWEF_SETTINGS_NAME ]['seo_primary_category'] ) ? $_POST[ \AWEF_SETTINGS_NAME ]['seo_primary_category'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing 565 $primary_raw = isset( $_POST[ \AWEF_SETTINGS_NAME ]['seo_primary_category'] ) ? $_POST[ \AWEF_SETTINGS_NAME ]['seo_primary_category'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 203 566 $primary_id = (int) $primary_raw; 204 567 … … 268 631 } else { 269 632 self::save_toc_overrides( $post_id, $toc_settings_collected ); 633 } 634 } 635 636 /** 637 * Save FAQ schema data for a post. 638 * 639 * @param int $post_id Post ID. 640 * 641 * @return void 642 */ 643 public static function save_faq_schema( int $post_id ): void { 644 645 // Check if FAQ data is submitted. 646 if ( ! isset( $_POST[ \AWEF_SETTINGS_NAME ]['faq_items'] ) || ! is_array( $_POST[ \AWEF_SETTINGS_NAME ]['faq_items'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 647 // If nothing submitted, delete any existing FAQ data. 648 \delete_post_meta( $post_id, self::POST_FAQ_SCHEMA ); 649 return; 650 } 651 652 $raw_faq_items = \wp_unslash( $_POST[ \AWEF_SETTINGS_NAME ]['faq_items'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing 653 654 $faq_items = array(); 655 656 foreach ( $raw_faq_items as $item ) { 657 // Sanitize each FAQ item. 658 $question = isset( $item['question'] ) ? \sanitize_text_field( $item['question'] ) : ''; 659 $answer = isset( $item['answer'] ) ? \wp_kses_post( $item['answer'] ) : ''; 660 661 // Only save items with both question and answer. 662 if ( ! empty( trim( $question ) ) && ! empty( trim( $answer ) ) ) { 663 $faq_items[] = array( 664 'question' => $question, 665 'answer' => $answer, 666 ); 667 } 668 } 669 670 if ( empty( $faq_items ) ) { 671 \delete_post_meta( $post_id, self::POST_FAQ_SCHEMA ); 672 } else { 673 $result = \update_post_meta( $post_id, self::POST_FAQ_SCHEMA, $faq_items ); 270 674 } 271 675 } … … 429 833 430 834 /** 835 * Retrieve FAQ schema data for a post. 836 * 837 * @param int $post_id Post ID. 838 * @return array FAQ items array. 839 */ 840 public static function get_faq_schema( int $post_id ): array { 841 $raw = \get_post_meta( $post_id, self::POST_FAQ_SCHEMA, true ); 842 return is_array( $raw ) ? $raw : array(); 843 } 844 845 /** 431 846 * Register The Meta Boxes 432 847 * … … 471 886 \wp_enqueue_script( 'awef-admin-scripts', \AWEF_PLUGIN_ROOT_URL . 'js/admin/awef-settings.js', array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-draggable', 'wp-color-picker', 'jquery-ui-autocomplete' ), \AWEF_VERSION, false ); 472 887 888 // Enqueue editor scripts for WYSIWYG editors in FAQ section 889 \wp_enqueue_editor(); 890 473 891 $settings_tabs = array( 474 892 … … 505 923 ); 506 924 } 925 926 // FAQ Schema tab - available for all post types. 927 $settings_tabs['faq-post'] = array( 928 'icon' => 'format-chat', 929 'title' => esc_html__( 'FAQ Schema', 'awesome-footnotes' ), 930 ); 507 931 508 932 ?> … … 677 1101 // $excerpt = \get_the_excerpt( $post ); 678 1102 // if ( ! empty( $excerpt ) ) { 679 // return $excerpt;1103 // return $excerpt; 680 1104 // } else { 681 1105 return self::the_short_content( 160, $raw ); … … 707 1131 return $title; 708 1132 } 1133 1134 /** 1135 * Render FAQ shortcode to display FAQ items. 1136 * 1137 * @param array $atts Shortcode attributes. 1138 * @return string Rendered HTML output. 1139 * 1140 * @since 3.8.0 1141 */ 1142 public static function render_faq_shortcode( $atts ) { 1143 // Parse shortcode attributes. 1144 $atts = \shortcode_atts( 1145 array( 1146 'post_id' => \get_the_ID(), 1147 'class' => '', 1148 ), 1149 $atts, 1150 'awef_faq' 1151 ); 1152 1153 $post_id = absint( $atts['post_id'] ); 1154 if ( ! $post_id ) { 1155 return ''; 1156 } 1157 1158 $faq_items = self::get_faq_schema( $post_id ); 1159 if ( empty( $faq_items ) ) { 1160 return ''; 1161 } 1162 1163 // // Enqueue FAQ styles. 1164 // \wp_enqueue_style( 1165 // 'awef-faq-schema', 1166 // \AWEF_PLUGIN_ROOT_URL . 'css/faq-schema.css', 1167 // array(), 1168 // \AWEF_VERSION 1169 // ); 1170 1171 // Mark that shortcode was used (for schema output). 1172 self::set_faq_shortcode_used( $post_id ); 1173 1174 $extra_class = ! empty( $atts['class'] ) ? ' ' . \esc_attr( $atts['class'] ) : ''; 1175 1176 \ob_start(); 1177 ?> 1178 <section class="awef-faq-schema<?php echo esc_attr( $extra_class ); ?>"> 1179 <?php foreach ( $faq_items as $index => $item ) : ?> 1180 <div class="awef-faq-item"> 1181 <h3 class="awef-faq-question"><?php echo \esc_html( $item['question'] ); ?></h3> 1182 <p class="awef-faq-answer"> 1183 <p><?php echo \wp_kses_post( $item['answer'] ); ?></p> 1184 </p> 1185 </div> 1186 <?php endforeach; ?> 1187 </section> 1188 <?php 1189 return \ob_get_clean(); 1190 } 1191 1192 /** 1193 * Track whether FAQ shortcode was used on current page/post. 1194 * 1195 * @param int $post_id Post ID. 1196 * @return void 1197 */ 1198 private static function set_faq_shortcode_used( int $post_id ): void { 1199 if ( ! isset( $GLOBALS['awef_faq_shortcode_used'] ) ) { 1200 $GLOBALS['awef_faq_shortcode_used'] = array(); 1201 } 1202 $GLOBALS['awef_faq_shortcode_used'][ $post_id ] = true; 1203 } 1204 1205 /** 1206 * Check if FAQ shortcode was used for a post. 1207 * 1208 * @param int $post_id Post ID. 1209 * @return bool 1210 */ 1211 private static function is_faq_shortcode_used( int $post_id ): bool { 1212 return isset( $GLOBALS['awef_faq_shortcode_used'][ $post_id ] ); 1213 } 1214 1215 /** 1216 * Output FAQ JSON-LD schema in the document head. 1217 * Only outputs if FAQ shortcode is used on the current post. 1218 * 1219 * @return void 1220 * 1221 * @since 3.8.0 1222 */ 1223 public static function output_faq_schema(): void { 1224 // Only output on singular posts/pages. 1225 if ( ! \is_singular() ) { 1226 return; 1227 } 1228 1229 global $post; 1230 if ( ! $post instanceof \WP_Post ) { 1231 return; 1232 } 1233 1234 $post_id = $post->ID; 1235 1236 // Check if content has the shortcode. 1237 if ( ! \has_shortcode( $post->post_content, 'awef_faq' ) ) { 1238 return; 1239 } 1240 1241 $faq_items = self::get_faq_schema( $post_id ); 1242 if ( empty( $faq_items ) ) { 1243 return; 1244 } 1245 1246 // Build JSON-LD schema. 1247 $schema = array( 1248 '@context' => 'https://schema.org', 1249 '@type' => 'FAQPage', 1250 'mainEntity' => array(), 1251 ); 1252 1253 foreach ( $faq_items as $item ) { 1254 $schema['mainEntity'][] = array( 1255 '@type' => 'Question', 1256 'name' => $item['question'], 1257 'acceptedAnswer' => array( 1258 '@type' => 'Answer', 1259 'text' => \wp_strip_all_tags( $item['answer'] ), 1260 ), 1261 ); 1262 } 1263 1264 // Output JSON-LD. 1265 ?> 1266 <script type="application/ld+json"> 1267 <?php echo \wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ); ?> 1268 </script> 1269 <?php 1270 } 709 1271 } 710 1272 } -
awesome-footnotes/tags/3.9.2/classes/helpers/class-settings.php
r3422097 r3429715 110 110 */ 111 111 \add_action( 'awef_settings_save_button', array( __CLASS__, 'save_button' ) ); 112 113 // After options are updated, ensure llms.txt is written to site root when present. 114 \add_action( 'updated_option', array( __CLASS__, 'maybe_write_llms_on_update' ), 10, 3 ); 115 } 116 117 /** 118 * Attempt to write llms.txt after plugin options are updated. 119 * 120 * @param string $option Option name updated. 121 * @param mixed $old_value Previous value. 122 * @param mixed $value New value. 123 * 124 * @return void 125 */ 126 public static function maybe_write_llms_on_update( $option, $old_value, $value ) { 127 if ( \AWEF_SETTINGS_NAME !== $option ) { 128 return; 129 } 130 if ( ! is_array( $value ) ) { 131 return; 132 } 133 $llms = isset( $value['llms_txt_content'] ) ? trim( (string) $value['llms_txt_content'] ) : ''; 134 if ( $llms === '' ) { 135 // Nothing to write; remove file if exists? keep existing. Just return. 136 return; 137 } 138 139 global $wp_filesystem; 140 if ( null === $wp_filesystem ) { 141 \WP_Filesystem(); 142 } 143 $content_path = \trailingslashit( ABSPATH ) . 'llms.txt'; 144 $written = false; 145 if ( isset( $wp_filesystem ) && is_object( $wp_filesystem ) && method_exists( $wp_filesystem, 'put_contents' ) ) { 146 $written = (bool) $wp_filesystem->put_contents( $content_path, $llms, FS_CHMOD_FILE ); 147 } else { 148 $written = (bool) @file_put_contents( $content_path, $llms ); 149 } 150 151 // Avoid recursion: transients call update_option which would re-trigger this handler. 152 \remove_action( 'updated_option', array( __CLASS__, 'maybe_write_llms_on_update' ), 10 ); 153 if ( $written ) { 154 \set_transient( 'awef_llms_written', 1, 30 ); 155 } else { 156 \set_transient( 'awef_llms_write_failed', 1, 30 ); 157 } 158 // Re-attach the handler. 159 \add_action( 'updated_option', array( __CLASS__, 'maybe_write_llms_on_update' ), 10, 3 ); 112 160 } 113 161 … … 157 205 158 206 $footnotes_options['seo_post_options'] = ( array_key_exists( 'seo_post_options', $post_array ) ) ? filter_var( $post_array['seo_post_options'], FILTER_VALIDATE_BOOLEAN ) : false; 207 208 // Optional llms.txt preset content (plain text). Stored as textarea/editor in settings. 209 // If the field is present in POST we save its (possibly empty) value. If it's absent 210 // (for example hidden by JS), preserve the currently stored value instead of 211 // falling back to the default. 212 if ( array_key_exists( 'llms_txt_content', $post_array ) ) { 213 $footnotes_options['llms_txt_content'] = \sanitize_textarea_field( $post_array['llms_txt_content'] ); 214 } else { 215 $footnotes_options['llms_txt_content'] = ''; 216 } 159 217 160 218 $footnotes_options['combine_identical_notes'] = ( array_key_exists( 'combine_identical_notes', $post_array ) ) ? filter_var( $post_array['combine_identical_notes'], FILTER_VALIDATE_BOOLEAN ) : false; … … 252 310 self::$current_options = $footnotes_options; 253 311 312 // If a llms.txt preset exists, attempt to write it to site root (ABSPATH/llms.txt). 313 if ( isset( $footnotes_options['llms_txt_content'] ) && trim( $footnotes_options['llms_txt_content'] ) !== '' ) { 314 global $wp_filesystem; 315 if ( null === $wp_filesystem ) { 316 \WP_Filesystem(); 317 } 318 $content_path = \trailingslashit( ABSPATH ) . 'llms.txt'; 319 if ( isset( $wp_filesystem ) && is_object( $wp_filesystem ) ) { 320 $wp_filesystem->put_contents( $content_path, $footnotes_options['llms_txt_content'], FS_CHMOD_FILE ); 321 } else { 322 @file_put_contents( $content_path, $footnotes_options['llms_txt_content'] ); 323 } 324 } 325 254 326 // Purge TOC transients so changes reflect immediately and queue admin notice. 255 327 self::clear_toc_transients(); … … 307 379 self::$global_options = self::get_default_options(); 308 380 \update_option( AWEF_SETTINGS_NAME, self::$global_options ); 309 } elseif ( ! isset( self::$global_options['version'] ) || self::OPTIONS_VERSION !== self::$global_options['version'] ) { 310 311 // Set any unset options. 312 foreach ( self::get_default_options() as $key => $value ) { 381 } else { 382 // Ensure any missing default options are set. Persist only when we added keys. 383 $defaults = self::get_default_options(); 384 $added = false; 385 foreach ( $defaults as $key => $value ) { 313 386 if ( ! isset( self::$global_options[ $key ] ) ) { 314 387 self::$global_options[ $key ] = $value; 388 $added = true; 315 389 } 316 390 } 317 391 318 \update_option( AWEF_SETTINGS_NAME, self::$global_options ); 392 if ( $added ) { 393 \update_option( AWEF_SETTINGS_NAME, self::$global_options ); 394 } 319 395 } 320 396 … … 346 422 self::$current_options = self::get_default_options(); 347 423 \update_option( AWEF_SETTINGS_NAME, self::$current_options ); 348 } elseif ( ! isset( self::$current_options['version'] ) || self::OPTIONS_VERSION !== self::$current_options['version'] ) { 349 350 // Set any unset options. 351 foreach ( self::get_default_options() as $key => $value ) { 424 } else { 425 // Ensure any missing default options are set. Persist only when we added keys. 426 $defaults = self::get_default_options(); 427 $added = false; 428 foreach ( $defaults as $key => $value ) { 352 429 if ( ! isset( self::$current_options[ $key ] ) ) { 353 430 self::$current_options[ $key ] = $value; 431 $added = true; 354 432 } 355 433 } 356 434 357 \update_option( AWEF_SETTINGS_NAME, self::$current_options ); 435 if ( $added ) { 436 \update_option( AWEF_SETTINGS_NAME, self::$current_options ); 437 } 358 438 } 359 439 … … 494 574 'toc_post_types' => array( 'post', 'page' ), 495 575 'toc_include_archives' => false, 576 // Default llms.txt preset: allow AI agents broadly but block access to admin. 577 'llms_txt_content' => "User-agent: AI\nAllow: /\n\nUser-agent: *\nDisallow: /wp-admin/\n", 496 578 ); 497 579 } … … 632 714 delete_transient( 'awef_toc_cache_cleared' ); 633 715 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'TOC cache cleared. Your changes are now active.', 'awesome-footnotes' ) . '</p></div>'; 716 } 717 718 // Show notice if llms.txt was created or writing failed. 719 if ( get_transient( 'awef_llms_written' ) ) { 720 delete_transient( 'awef_llms_written' ); 721 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'llms.txt was written to site root.', 'awesome-footnotes' ) . '</p></div>'; 722 } 723 if ( get_transient( 'awef_llms_write_failed' ) ) { 724 delete_transient( 'awef_llms_write_failed' ); 725 $path = esc_html( ABSPATH . 'llms.txt' ); 726 echo '<div class="notice notice-error is-dismissible"><p>' . sprintf( esc_html__( 'Could not write %s. Make the site root writable or create the file manually.', 'awesome-footnotes' ), $path ) . '</p></div>'; 634 727 } 635 728 -
awesome-footnotes/tags/3.9.2/classes/settings/class-settings-builder.php
r3422097 r3429715 1350 1350 self::$placeholder_attr = ! empty( $placeholder ) ? 'placeholder="' . $placeholder . '"' : ''; 1351 1351 1352 // Get the option stored data. 1353 if ( ! empty( $data ) ) { 1352 // Get the option stored data. Use provided data even if it's an empty 1353 // value (''), otherwise fall back to the configured default. The 1354 // builder previously used `empty()` which caused explicitly-saved 1355 // empty strings to be ignored and the default to be shown instead. 1356 if ( $data !== false ) { 1354 1357 self::$current_value = $data; 1355 1358 } elseif ( ! empty( $default ) ) { -
awesome-footnotes/tags/3.9.2/classes/settings/settings-options/options.php
r3366051 r3429715 179 179 'type' => 'checkbox', 180 180 'default' => Settings::get_current_options()['seo_post_options'], 181 'hint' => \esc_html__( 'Enable this if you want to use plugin SEO options in posts. Keep in mid that the plugin uses Posts excerpt field to store the Meta description!', 'awesome-footnotes' ), 182 ) 183 ); 181 'toggle' => '#llms_txt_content-item', 182 'hint' => \esc_html__( 'Enable this if you want to use plugin SEO options in posts. Keep in mind that the plugin uses Posts excerpt field to store the Meta description!', 'awesome-footnotes' ), 183 ) 184 ); 185 186 // llms.txt content editor - visible only when `seo_post_options` is enabled (toggle). 187 $writable = is_writable( ABSPATH ); 188 $hint = $writable 189 ? \esc_html__( 'Optional preset content for llms.txt to be written to site root when non-empty.', 'awesome-footnotes' ) 190 : sprintf( /* translators: %s: path */ \esc_html__( 'Plugin cannot write to site root (%s). Create llms.txt there or make it writable for automatic generation.', 'awesome-footnotes' ), esc_html( ABSPATH . 'llms.txt' ) ); 191 192 Settings::build_option( 193 array( 194 'name' => \esc_html__( 'llms.txt content', 'awesome-footnotes' ), 195 'id' => 'llms_txt_content', 196 'type' => Settings::get_current_options()['no_editor_header_footer'] ? 'textarea' : 'editor', 197 'hint' => $hint, 198 // Use the stored option value directly here to avoid merged defaults overwriting 199 // an explicitly-saved empty string when rendering the form after save. 200 'default' => ( \get_option( \AWEF_SETTINGS_NAME )['llms_txt_content'] ?? '' ), 201 ) 202 ); -
awesome-footnotes/tags/3.9.2/readme.txt
r3422097 r3429715 96 96 Even though it's a little more typing, using the exact text method is much more robust. The number referencing will not work across multiple pages in a paged post (but will work within the page). Also, if you use the number referencing system you risk them identifying the incorrect footnote if you go back and insert a new footnote and forget to change the referenced number. 97 97 98 ## FAQ Schema Usage Guide 99 100 ### For Administrators 101 102 1. **Edit any post or page** 103 2. **Scroll to the "Awesome Footnotes - Settings" meta box** 104 3. **Click the "FAQ Schema" tab** 105 4. **Click "Add FAQ Item"** 106 5. **Fill in the Question and Answer fields** 107 6. **Repeat for additional FAQs** 108 7. **Use move up/down buttons to reorder** 109 8. **Click Remove to delete items** 110 9. **Save/Update the post** 111 112 ### For Content Display 113 114 1. **In the post content editor, add:** 115 ``` 116 [awef_faq] 117 ``` 118 2. **Publish/Update the post** 119 3. **View the post on the frontend** 120 4. **FAQs will display at the shortcode location** 121 5. **JSON-LD schema automatically added to `<head>`** 122 123 ### For Developers 124 125 **Get FAQ data programmatically:** 126 ```php 127 use AWEF\Controllers\Post_Settings; 128 129 $post_id = get_the_ID(); 130 $faqs = Post_Settings::get_faq_schema( $post_id ); 131 132 foreach ( $faqs as $faq ) { 133 echo $faq['question']; 134 echo $faq['answer']; 135 } 136 ``` 137 138 **Check if FAQ shortcode is used:** 139 ```php 140 $content = get_post_field( 'post_content', $post_id ); 141 if ( has_shortcode( $content, 'awef_faq' ) ) { 142 // FAQ shortcode is present 143 } 144 ``` 145 98 146 == Installation == 99 147 -
awesome-footnotes/trunk/classes/controllers/class-post-settings.php
r3422097 r3429715 36 36 */ 37 37 public const POST_PRIMARY_CATEGORY = '_awef_primary_category'; 38 39 /** 40 * Meta key for FAQ schema data. 41 */ 42 public const POST_FAQ_SCHEMA = '_awef_post_faq_schema'; 38 43 39 44 /** … … 100 105 101 106 \add_filter( 'TieLabs/meta_title', array( __CLASS__, 'get_meta_title' ) ); 107 // Use the post-level SEO title for the document <title> when available. 108 \add_filter( 'pre_get_document_title', array( __CLASS__, 'get_meta_title' ) ); 109 // Fallback for themes/plugins using the older `wp_title` filter. 110 \add_filter( 'wp_title', array( __CLASS__, 'get_meta_title' ), 10, 1 ); 102 111 \add_filter( 'TieLabs/meta_description', array( __CLASS__, 'get_meta_description' ) ); 103 112 … … 105 114 106 115 \add_filter( 'get_the_excerpt', array( __CLASS__, 'get_meta_description' ), 999, 2 ); 116 117 // Register FAQ shortcode. 118 \add_shortcode( 'awef_faq', array( __CLASS__, 'render_faq_shortcode' ) ); 119 120 // Add FAQ JSON-LD schema to <head>. 121 \add_action( 'wp_head', array( __CLASS__, 'output_faq_schema' ), 10 ); 122 123 // Add quick edit functionality for SEO fields if enabled. 124 if ( Settings::get_current_options()['seo_post_options'] ) { 125 self::init_quick_edit_hooks(); 126 } 127 } 128 129 /** 130 * Initialize quick edit hooks for SEO fields 131 * 132 * @since 3.8.0 133 */ 134 private static function init_quick_edit_hooks() { 135 // Get post types that support footnotes 136 $global_options = Settings::get_current_options(); 137 $post_types = isset( $global_options['post_types'] ) && is_array( $global_options['post_types'] ) 138 ? $global_options['post_types'] 139 : array( 'post', 'page' ); 140 141 // Add quick edit fields for each post type 142 foreach ( $post_types as $post_type ) { 143 \add_action( "quick_edit_custom_box", array( __CLASS__, 'add_quick_edit_fields' ), 10, 2 ); 144 \add_action( "bulk_edit_custom_box", array( __CLASS__, 'add_bulk_edit_fields' ), 10, 2 ); 145 // Add a hidden column to trigger data hooks 146 \add_filter( "manage_{$post_type}_posts_columns", array( __CLASS__, 'add_seo_column' ) ); 147 } 148 149 // Save quick edit data 150 \add_action( 'wp_ajax_save_bulk_edit_awef_seo', array( __CLASS__, 'save_bulk_edit' ) ); 151 \add_action( 'save_post', array( __CLASS__, 'save_quick_edit' ) ); 152 153 // Add admin scripts for quick edit 154 \add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_quick_edit_scripts' ) ); 155 156 // Add inline data to posts list 157 \add_action( 'manage_posts_custom_column', array( __CLASS__, 'add_quick_edit_data' ), 10, 2 ); 158 \add_action( 'manage_pages_custom_column', array( __CLASS__, 'add_quick_edit_data' ), 10, 2 ); 159 } 160 161 /** 162 * Add quick edit fields for SEO 163 * 164 * @param string $column_name Column name 165 * @param string $post_type Post type 166 * @since 3.8.0 167 */ 168 public static function add_quick_edit_fields( $column_name, $post_type ) { 169 // Get post types that support footnotes 170 $global_options = Settings::get_current_options(); 171 $supported_post_types = isset( $global_options['post_types'] ) && is_array( $global_options['post_types'] ) 172 ? $global_options['post_types'] 173 : array( 'post', 'page' ); 174 175 if ( ! in_array( $post_type, $supported_post_types, true ) ) { 176 return; 177 } 178 179 // Only add fields once (WordPress calls this multiple times for different columns) 180 static $added = false; 181 if ( $added ) { 182 return; 183 } 184 $added = true; 185 186 ?> 187 <fieldset class="inline-edit-col-right"> 188 <div class="inline-edit-col"> 189 <h4><?php echo esc_html( AWEF_NAME . ' ' . __( 'SEO Settings', 'awesome-footnotes' ) ); ?></h4> 190 <label> 191 <span class="title"><?php esc_html_e( 'SEO Title', 'awesome-footnotes' ); ?></span> 192 <span class="input-text-wrap"> 193 <input type="text" name="awef_seo_title" class="awef-seo-title" value="" /> 194 </span> 195 </label> 196 <label> 197 <span class="title"><?php esc_html_e( 'SEO Description', 'awesome-footnotes' ); ?></span> 198 <span class="input-text-wrap"> 199 <textarea name="awef_seo_description" class="awef-seo-description" rows="3"></textarea> 200 </span> 201 </label> 202 </div> 203 </fieldset> 204 <?php 205 } 206 207 /** 208 * Add bulk edit fields for SEO 209 * 210 * @param string $column_name Column name 211 * @param string $post_type Post type 212 * @since 3.8.0 213 */ 214 public static function add_bulk_edit_fields( $column_name, $post_type ) { 215 // Get post types that support footnotes 216 $global_options = Settings::get_current_options(); 217 $supported_post_types = isset( $global_options['post_types'] ) && is_array( $global_options['post_types'] ) 218 ? $global_options['post_types'] 219 : array( 'post', 'page' ); 220 221 if ( ! in_array( $post_type, $supported_post_types, true ) ) { 222 return; 223 } 224 225 // Only add fields once 226 static $added = false; 227 if ( $added ) { 228 return; 229 } 230 $added = true; 231 232 ?> 233 <fieldset class="inline-edit-col-right"> 234 <div class="inline-edit-col"> 235 <h4><?php echo esc_html( AWEF_NAME . ' ' . __( 'SEO Settings', 'awesome-footnotes' ) ); ?></h4> 236 <label> 237 <span class="title"><?php esc_html_e( 'SEO Title', 'awesome-footnotes' ); ?></span> 238 <span class="input-text-wrap"> 239 <select name="awef_seo_title_bulk_action" class="awef-seo-title-bulk-action"> 240 <option value=""><?php esc_html_e( '— No Change —', 'awesome-footnotes' ); ?></option> 241 <option value="set"><?php esc_html_e( 'Set to:', 'awesome-footnotes' ); ?></option> 242 <option value="clear"><?php esc_html_e( 'Clear', 'awesome-footnotes' ); ?></option> 243 </select> 244 <input type="text" name="awef_seo_title" class="awef-seo-title" value="" style="display:none;" /> 245 </span> 246 </label> 247 <label> 248 <span class="title"><?php esc_html_e( 'SEO Description', 'awesome-footnotes' ); ?></span> 249 <span class="input-text-wrap"> 250 <select name="awef_seo_description_bulk_action" class="awef-seo-description-bulk-action"> 251 <option value=""><?php esc_html_e( '— No Change —', 'awesome-footnotes' ); ?></option> 252 <option value="set"><?php esc_html_e( 'Set to:', 'awesome-footnotes' ); ?></option> 253 <option value="clear"><?php esc_html_e( 'Clear', 'awesome-footnotes' ); ?></option> 254 </select> 255 <textarea name="awef_seo_description" class="awef-seo-description" rows="3" style="display:none;"></textarea> 256 </span> 257 </label> 258 </div> 259 </fieldset> 260 <?php 261 } 262 263 /** 264 * Save quick edit data 265 * 266 * @param int $post_id Post ID 267 * @since 3.8.0 268 */ 269 public static function save_quick_edit( $post_id ) { 270 // Check if this is a quick edit request 271 if ( ! isset( $_POST['_inline_edit'] ) ) { 272 return; 273 } 274 275 // Verify nonce 276 if ( ! isset( $_POST['_inline_edit'] ) || ! \wp_verify_nonce( $_POST['_inline_edit'], 'inlineeditnonce' ) ) { 277 return; 278 } 279 280 // Check permissions 281 if ( ! \current_user_can( 'edit_post', $post_id ) ) { 282 return; 283 } 284 285 // Handle SEO title 286 if ( isset( $_POST['awef_seo_title'] ) ) { 287 $seo_title = \sanitize_text_field( \wp_unslash( $_POST['awef_seo_title'] ) ); 288 $post = \get_post( $post_id ); 289 290 if ( ! empty( $seo_title ) && $seo_title !== \get_the_title( $post_id ) ) { 291 \update_post_meta( $post_id, self::POST_SEO_TITLE, $seo_title ); 292 } else { 293 \delete_post_meta( $post_id, self::POST_SEO_TITLE ); 294 } 295 } 296 297 // Handle SEO description 298 if ( isset( $_POST['awef_seo_description'] ) ) { 299 $seo_description = \sanitize_textarea_field( \wp_unslash( $_POST['awef_seo_description'] ) ); 300 301 if ( ! empty( $seo_description ) ) { 302 $post = \get_post( $post_id ); 303 $post->post_excerpt = self::the_short_content( 160, $seo_description ); 304 305 \remove_all_actions( 'save_post' ); 306 \wp_update_post( $post ); 307 \add_action( 'save_post', array( __CLASS__, 'save_quick_edit' ) ); 308 \add_action( 'save_post', array( __CLASS__, 'save' ) ); 309 } 310 } 311 } 312 313 /** 314 * Save bulk edit data 315 * 316 * @since 3.8.0 317 */ 318 public static function save_bulk_edit() { 319 // Verify nonce 320 if ( ! isset( $_POST['_wpnonce'] ) || ! \wp_verify_nonce( $_POST['_wpnonce'], 'bulk-posts' ) ) { 321 \wp_die( 'Security check failed' ); 322 } 323 324 // Check permissions 325 if ( ! \current_user_can( 'edit_posts' ) ) { 326 \wp_die( 'Insufficient permissions' ); 327 } 328 329 $post_ids = array(); 330 if ( isset( $_POST['post_ids'] ) && is_array( $_POST['post_ids'] ) ) { 331 $post_ids = array_map( 'intval', $_POST['post_ids'] ); 332 } 333 334 if ( empty( $post_ids ) ) { 335 \wp_die( 'No posts selected' ); 336 } 337 338 foreach ( $post_ids as $post_id ) { 339 if ( ! \current_user_can( 'edit_post', $post_id ) ) { 340 continue; 341 } 342 343 // Handle SEO title bulk action 344 if ( isset( $_POST['awef_seo_title_bulk_action'] ) ) { 345 $title_action = \sanitize_text_field( $_POST['awef_seo_title_bulk_action'] ); 346 347 if ( 'set' === $title_action && isset( $_POST['awef_seo_title'] ) ) { 348 $seo_title = \sanitize_text_field( \wp_unslash( $_POST['awef_seo_title'] ) ); 349 if ( ! empty( $seo_title ) ) { 350 \update_post_meta( $post_id, self::POST_SEO_TITLE, $seo_title ); 351 } 352 } elseif ( 'clear' === $title_action ) { 353 \delete_post_meta( $post_id, self::POST_SEO_TITLE ); 354 } 355 } 356 357 // Handle SEO description bulk action 358 if ( isset( $_POST['awef_seo_description_bulk_action'] ) ) { 359 $desc_action = \sanitize_text_field( $_POST['awef_seo_description_bulk_action'] ); 360 361 if ( 'set' === $desc_action && isset( $_POST['awef_seo_description'] ) ) { 362 $seo_description = \sanitize_textarea_field( \wp_unslash( $_POST['awef_seo_description'] ) ); 363 if ( ! empty( $seo_description ) ) { 364 $post = \get_post( $post_id ); 365 $post->post_excerpt = self::the_short_content( 160, $seo_description ); 366 \wp_update_post( $post ); 367 } 368 } elseif ( 'clear' === $desc_action ) { 369 $post = \get_post( $post_id ); 370 $post->post_excerpt = ''; 371 \wp_update_post( $post ); 372 } 373 } 374 } 375 376 \wp_die(); // This is required to terminate immediately and return a proper response 377 } 378 379 /** 380 * Enqueue scripts for quick edit functionality 381 * 382 * @param string $hook_suffix Current admin page hook suffix 383 * @since 3.8.0 384 */ 385 public static function enqueue_quick_edit_scripts( $hook_suffix ) { 386 if ( 'edit.php' !== $hook_suffix ) { 387 return; 388 } 389 390 // Get current screen 391 $screen = \get_current_screen(); 392 if ( ! $screen ) { 393 return; 394 } 395 396 // Check if this is a supported post type 397 $global_options = Settings::get_current_options(); 398 $supported_post_types = isset( $global_options['post_types'] ) && is_array( $global_options['post_types'] ) 399 ? $global_options['post_types'] 400 : array( 'post', 'page' ); 401 402 if ( ! in_array( $screen->post_type, $supported_post_types, true ) ) { 403 return; 404 } 405 406 \wp_enqueue_script( 407 'awef-quick-edit', 408 \AWEF_PLUGIN_ROOT_URL . 'js/admin/quick-edit.js', 409 array( 'jquery', 'inline-edit-post' ), 410 \AWEF_VERSION, 411 true 412 ); 413 414 \wp_localize_script( 415 'awef-quick-edit', 416 'awefQuickEdit', 417 array( 418 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), 419 'nonce' => \wp_create_nonce( 'awef_quick_edit' ), 420 ) 421 ); 422 423 // Register and enqueue style for hiding the SEO data column 424 \wp_register_style( 'awef-admin-quick-edit', false ); 425 \wp_enqueue_style( 'awef-admin-quick-edit' ); 426 \wp_add_inline_style( 427 'awef-admin-quick-edit', 428 '.column-awef_seo_data { display: none !important; width: 0 !important; }' 429 ); 430 } 431 432 /** 433 * Add a hidden SEO column to posts list (to trigger data hooks) 434 * 435 * @param array $columns Existing columns 436 * @return array Modified columns 437 * @since 3.8.0 438 */ 439 public static function add_seo_column( $columns ) { 440 $columns['awef_seo_data'] = ''; 441 return $columns; 442 } 443 444 /** 445 * Add inline data for quick edit 446 * 447 * @param string $column Column name 448 * @param int $post_id Post ID 449 * @since 3.8.0 450 */ 451 public static function add_quick_edit_data( $column, $post_id ) { 452 // Only process our hidden column 453 if ( 'awef_seo_data' !== $column ) { 454 return; 455 } 456 457 $seo_title = \get_post_meta( $post_id, self::POST_SEO_TITLE, true ); 458 $post = \get_post( $post_id ); 459 $seo_description = ! empty( $post->post_excerpt ) ? $post->post_excerpt : ''; 460 461 ?> 462 <div class="awef-quick-edit-data" data-post-id="<?php echo esc_attr( $post_id ); ?>" style="display:none;"> 463 <div class="seo-title"><?php echo esc_attr( $seo_title ); ?></div> 464 <div class="seo-description"><?php echo esc_attr( $seo_description ); ?></div> 465 </div> 466 <?php 107 467 } 108 468 … … 128 488 129 489 // Security: verify nonce and user capability. 130 if ( ! isset( $_POST['awef_post_settings_nonce'] ) || ! \wp_verify_nonce( $_POST['awef_post_settings_nonce'], 'awef_post_settings' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 131 return; 132 } 133 134 if ( ! \current_user_can( ' edit_post', $post_id ) ) {490 if ( ! isset( $_POST['awef_post_settings_nonce'] ) || ! \wp_verify_nonce( $_POST['awef_post_settings_nonce'], 'awef_post_settings' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 491 return; 492 } 493 494 if ( ! \current_user_can( 'manage_options', $post_id ) ) { 135 495 return; 136 496 } … … 139 499 140 500 // Collect and sanitize full submitted settings once. 141 $raw_settings = isset( $_POST[ \AWEF_SETTINGS_NAME ] ) ? \stripslashes_deep( $_POST[ \AWEF_SETTINGS_NAME ] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing 501 $raw_settings = isset( $_POST[ \AWEF_SETTINGS_NAME ] ) ? \stripslashes_deep( $_POST[ \AWEF_SETTINGS_NAME ] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 142 502 $all_settings_collected = Settings::collect_and_sanitize_options( $raw_settings ); 143 503 … … 154 514 ARRAY_FILTER_USE_KEY 155 515 ); 516 517 // Handle FAQ schema data. 518 self::save_faq_schema( $post_id ); 156 519 157 520 // TOC subset for overrides. … … 200 563 $primary_taxonomy = self::get_primary_category_taxonomy( \get_post_type( $post_id ) ); 201 564 if ( $primary_taxonomy ) { 202 $primary_raw = isset( $_POST[ \AWEF_SETTINGS_NAME ]['seo_primary_category'] ) ? $_POST[ \AWEF_SETTINGS_NAME ]['seo_primary_category'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing 565 $primary_raw = isset( $_POST[ \AWEF_SETTINGS_NAME ]['seo_primary_category'] ) ? $_POST[ \AWEF_SETTINGS_NAME ]['seo_primary_category'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 203 566 $primary_id = (int) $primary_raw; 204 567 … … 268 631 } else { 269 632 self::save_toc_overrides( $post_id, $toc_settings_collected ); 633 } 634 } 635 636 /** 637 * Save FAQ schema data for a post. 638 * 639 * @param int $post_id Post ID. 640 * 641 * @return void 642 */ 643 public static function save_faq_schema( int $post_id ): void { 644 645 // Check if FAQ data is submitted. 646 if ( ! isset( $_POST[ \AWEF_SETTINGS_NAME ]['faq_items'] ) || ! is_array( $_POST[ \AWEF_SETTINGS_NAME ]['faq_items'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing 647 // If nothing submitted, delete any existing FAQ data. 648 \delete_post_meta( $post_id, self::POST_FAQ_SCHEMA ); 649 return; 650 } 651 652 $raw_faq_items = \wp_unslash( $_POST[ \AWEF_SETTINGS_NAME ]['faq_items'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing 653 654 $faq_items = array(); 655 656 foreach ( $raw_faq_items as $item ) { 657 // Sanitize each FAQ item. 658 $question = isset( $item['question'] ) ? \sanitize_text_field( $item['question'] ) : ''; 659 $answer = isset( $item['answer'] ) ? \wp_kses_post( $item['answer'] ) : ''; 660 661 // Only save items with both question and answer. 662 if ( ! empty( trim( $question ) ) && ! empty( trim( $answer ) ) ) { 663 $faq_items[] = array( 664 'question' => $question, 665 'answer' => $answer, 666 ); 667 } 668 } 669 670 if ( empty( $faq_items ) ) { 671 \delete_post_meta( $post_id, self::POST_FAQ_SCHEMA ); 672 } else { 673 $result = \update_post_meta( $post_id, self::POST_FAQ_SCHEMA, $faq_items ); 270 674 } 271 675 } … … 429 833 430 834 /** 835 * Retrieve FAQ schema data for a post. 836 * 837 * @param int $post_id Post ID. 838 * @return array FAQ items array. 839 */ 840 public static function get_faq_schema( int $post_id ): array { 841 $raw = \get_post_meta( $post_id, self::POST_FAQ_SCHEMA, true ); 842 return is_array( $raw ) ? $raw : array(); 843 } 844 845 /** 431 846 * Register The Meta Boxes 432 847 * … … 471 886 \wp_enqueue_script( 'awef-admin-scripts', \AWEF_PLUGIN_ROOT_URL . 'js/admin/awef-settings.js', array( 'jquery', 'jquery-ui-sortable', 'jquery-ui-draggable', 'wp-color-picker', 'jquery-ui-autocomplete' ), \AWEF_VERSION, false ); 472 887 888 // Enqueue editor scripts for WYSIWYG editors in FAQ section 889 \wp_enqueue_editor(); 890 473 891 $settings_tabs = array( 474 892 … … 505 923 ); 506 924 } 925 926 // FAQ Schema tab - available for all post types. 927 $settings_tabs['faq-post'] = array( 928 'icon' => 'format-chat', 929 'title' => esc_html__( 'FAQ Schema', 'awesome-footnotes' ), 930 ); 507 931 508 932 ?> … … 677 1101 // $excerpt = \get_the_excerpt( $post ); 678 1102 // if ( ! empty( $excerpt ) ) { 679 // return $excerpt;1103 // return $excerpt; 680 1104 // } else { 681 1105 return self::the_short_content( 160, $raw ); … … 707 1131 return $title; 708 1132 } 1133 1134 /** 1135 * Render FAQ shortcode to display FAQ items. 1136 * 1137 * @param array $atts Shortcode attributes. 1138 * @return string Rendered HTML output. 1139 * 1140 * @since 3.8.0 1141 */ 1142 public static function render_faq_shortcode( $atts ) { 1143 // Parse shortcode attributes. 1144 $atts = \shortcode_atts( 1145 array( 1146 'post_id' => \get_the_ID(), 1147 'class' => '', 1148 ), 1149 $atts, 1150 'awef_faq' 1151 ); 1152 1153 $post_id = absint( $atts['post_id'] ); 1154 if ( ! $post_id ) { 1155 return ''; 1156 } 1157 1158 $faq_items = self::get_faq_schema( $post_id ); 1159 if ( empty( $faq_items ) ) { 1160 return ''; 1161 } 1162 1163 // // Enqueue FAQ styles. 1164 // \wp_enqueue_style( 1165 // 'awef-faq-schema', 1166 // \AWEF_PLUGIN_ROOT_URL . 'css/faq-schema.css', 1167 // array(), 1168 // \AWEF_VERSION 1169 // ); 1170 1171 // Mark that shortcode was used (for schema output). 1172 self::set_faq_shortcode_used( $post_id ); 1173 1174 $extra_class = ! empty( $atts['class'] ) ? ' ' . \esc_attr( $atts['class'] ) : ''; 1175 1176 \ob_start(); 1177 ?> 1178 <section class="awef-faq-schema<?php echo esc_attr( $extra_class ); ?>"> 1179 <?php foreach ( $faq_items as $index => $item ) : ?> 1180 <div class="awef-faq-item"> 1181 <h3 class="awef-faq-question"><?php echo \esc_html( $item['question'] ); ?></h3> 1182 <p class="awef-faq-answer"> 1183 <p><?php echo \wp_kses_post( $item['answer'] ); ?></p> 1184 </p> 1185 </div> 1186 <?php endforeach; ?> 1187 </section> 1188 <?php 1189 return \ob_get_clean(); 1190 } 1191 1192 /** 1193 * Track whether FAQ shortcode was used on current page/post. 1194 * 1195 * @param int $post_id Post ID. 1196 * @return void 1197 */ 1198 private static function set_faq_shortcode_used( int $post_id ): void { 1199 if ( ! isset( $GLOBALS['awef_faq_shortcode_used'] ) ) { 1200 $GLOBALS['awef_faq_shortcode_used'] = array(); 1201 } 1202 $GLOBALS['awef_faq_shortcode_used'][ $post_id ] = true; 1203 } 1204 1205 /** 1206 * Check if FAQ shortcode was used for a post. 1207 * 1208 * @param int $post_id Post ID. 1209 * @return bool 1210 */ 1211 private static function is_faq_shortcode_used( int $post_id ): bool { 1212 return isset( $GLOBALS['awef_faq_shortcode_used'][ $post_id ] ); 1213 } 1214 1215 /** 1216 * Output FAQ JSON-LD schema in the document head. 1217 * Only outputs if FAQ shortcode is used on the current post. 1218 * 1219 * @return void 1220 * 1221 * @since 3.8.0 1222 */ 1223 public static function output_faq_schema(): void { 1224 // Only output on singular posts/pages. 1225 if ( ! \is_singular() ) { 1226 return; 1227 } 1228 1229 global $post; 1230 if ( ! $post instanceof \WP_Post ) { 1231 return; 1232 } 1233 1234 $post_id = $post->ID; 1235 1236 // Check if content has the shortcode. 1237 if ( ! \has_shortcode( $post->post_content, 'awef_faq' ) ) { 1238 return; 1239 } 1240 1241 $faq_items = self::get_faq_schema( $post_id ); 1242 if ( empty( $faq_items ) ) { 1243 return; 1244 } 1245 1246 // Build JSON-LD schema. 1247 $schema = array( 1248 '@context' => 'https://schema.org', 1249 '@type' => 'FAQPage', 1250 'mainEntity' => array(), 1251 ); 1252 1253 foreach ( $faq_items as $item ) { 1254 $schema['mainEntity'][] = array( 1255 '@type' => 'Question', 1256 'name' => $item['question'], 1257 'acceptedAnswer' => array( 1258 '@type' => 'Answer', 1259 'text' => \wp_strip_all_tags( $item['answer'] ), 1260 ), 1261 ); 1262 } 1263 1264 // Output JSON-LD. 1265 ?> 1266 <script type="application/ld+json"> 1267 <?php echo \wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ); ?> 1268 </script> 1269 <?php 1270 } 709 1271 } 710 1272 } -
awesome-footnotes/trunk/classes/helpers/class-settings.php
r3422097 r3429715 110 110 */ 111 111 \add_action( 'awef_settings_save_button', array( __CLASS__, 'save_button' ) ); 112 113 // After options are updated, ensure llms.txt is written to site root when present. 114 \add_action( 'updated_option', array( __CLASS__, 'maybe_write_llms_on_update' ), 10, 3 ); 115 } 116 117 /** 118 * Attempt to write llms.txt after plugin options are updated. 119 * 120 * @param string $option Option name updated. 121 * @param mixed $old_value Previous value. 122 * @param mixed $value New value. 123 * 124 * @return void 125 */ 126 public static function maybe_write_llms_on_update( $option, $old_value, $value ) { 127 if ( \AWEF_SETTINGS_NAME !== $option ) { 128 return; 129 } 130 if ( ! is_array( $value ) ) { 131 return; 132 } 133 $llms = isset( $value['llms_txt_content'] ) ? trim( (string) $value['llms_txt_content'] ) : ''; 134 if ( $llms === '' ) { 135 // Nothing to write; remove file if exists? keep existing. Just return. 136 return; 137 } 138 139 global $wp_filesystem; 140 if ( null === $wp_filesystem ) { 141 \WP_Filesystem(); 142 } 143 $content_path = \trailingslashit( ABSPATH ) . 'llms.txt'; 144 $written = false; 145 if ( isset( $wp_filesystem ) && is_object( $wp_filesystem ) && method_exists( $wp_filesystem, 'put_contents' ) ) { 146 $written = (bool) $wp_filesystem->put_contents( $content_path, $llms, FS_CHMOD_FILE ); 147 } else { 148 $written = (bool) @file_put_contents( $content_path, $llms ); 149 } 150 151 // Avoid recursion: transients call update_option which would re-trigger this handler. 152 \remove_action( 'updated_option', array( __CLASS__, 'maybe_write_llms_on_update' ), 10 ); 153 if ( $written ) { 154 \set_transient( 'awef_llms_written', 1, 30 ); 155 } else { 156 \set_transient( 'awef_llms_write_failed', 1, 30 ); 157 } 158 // Re-attach the handler. 159 \add_action( 'updated_option', array( __CLASS__, 'maybe_write_llms_on_update' ), 10, 3 ); 112 160 } 113 161 … … 157 205 158 206 $footnotes_options['seo_post_options'] = ( array_key_exists( 'seo_post_options', $post_array ) ) ? filter_var( $post_array['seo_post_options'], FILTER_VALIDATE_BOOLEAN ) : false; 207 208 // Optional llms.txt preset content (plain text). Stored as textarea/editor in settings. 209 // If the field is present in POST we save its (possibly empty) value. If it's absent 210 // (for example hidden by JS), preserve the currently stored value instead of 211 // falling back to the default. 212 if ( array_key_exists( 'llms_txt_content', $post_array ) ) { 213 $footnotes_options['llms_txt_content'] = \sanitize_textarea_field( $post_array['llms_txt_content'] ); 214 } else { 215 $footnotes_options['llms_txt_content'] = ''; 216 } 159 217 160 218 $footnotes_options['combine_identical_notes'] = ( array_key_exists( 'combine_identical_notes', $post_array ) ) ? filter_var( $post_array['combine_identical_notes'], FILTER_VALIDATE_BOOLEAN ) : false; … … 252 310 self::$current_options = $footnotes_options; 253 311 312 // If a llms.txt preset exists, attempt to write it to site root (ABSPATH/llms.txt). 313 if ( isset( $footnotes_options['llms_txt_content'] ) && trim( $footnotes_options['llms_txt_content'] ) !== '' ) { 314 global $wp_filesystem; 315 if ( null === $wp_filesystem ) { 316 \WP_Filesystem(); 317 } 318 $content_path = \trailingslashit( ABSPATH ) . 'llms.txt'; 319 if ( isset( $wp_filesystem ) && is_object( $wp_filesystem ) ) { 320 $wp_filesystem->put_contents( $content_path, $footnotes_options['llms_txt_content'], FS_CHMOD_FILE ); 321 } else { 322 @file_put_contents( $content_path, $footnotes_options['llms_txt_content'] ); 323 } 324 } 325 254 326 // Purge TOC transients so changes reflect immediately and queue admin notice. 255 327 self::clear_toc_transients(); … … 307 379 self::$global_options = self::get_default_options(); 308 380 \update_option( AWEF_SETTINGS_NAME, self::$global_options ); 309 } elseif ( ! isset( self::$global_options['version'] ) || self::OPTIONS_VERSION !== self::$global_options['version'] ) { 310 311 // Set any unset options. 312 foreach ( self::get_default_options() as $key => $value ) { 381 } else { 382 // Ensure any missing default options are set. Persist only when we added keys. 383 $defaults = self::get_default_options(); 384 $added = false; 385 foreach ( $defaults as $key => $value ) { 313 386 if ( ! isset( self::$global_options[ $key ] ) ) { 314 387 self::$global_options[ $key ] = $value; 388 $added = true; 315 389 } 316 390 } 317 391 318 \update_option( AWEF_SETTINGS_NAME, self::$global_options ); 392 if ( $added ) { 393 \update_option( AWEF_SETTINGS_NAME, self::$global_options ); 394 } 319 395 } 320 396 … … 346 422 self::$current_options = self::get_default_options(); 347 423 \update_option( AWEF_SETTINGS_NAME, self::$current_options ); 348 } elseif ( ! isset( self::$current_options['version'] ) || self::OPTIONS_VERSION !== self::$current_options['version'] ) { 349 350 // Set any unset options. 351 foreach ( self::get_default_options() as $key => $value ) { 424 } else { 425 // Ensure any missing default options are set. Persist only when we added keys. 426 $defaults = self::get_default_options(); 427 $added = false; 428 foreach ( $defaults as $key => $value ) { 352 429 if ( ! isset( self::$current_options[ $key ] ) ) { 353 430 self::$current_options[ $key ] = $value; 431 $added = true; 354 432 } 355 433 } 356 434 357 \update_option( AWEF_SETTINGS_NAME, self::$current_options ); 435 if ( $added ) { 436 \update_option( AWEF_SETTINGS_NAME, self::$current_options ); 437 } 358 438 } 359 439 … … 494 574 'toc_post_types' => array( 'post', 'page' ), 495 575 'toc_include_archives' => false, 576 // Default llms.txt preset: allow AI agents broadly but block access to admin. 577 'llms_txt_content' => "User-agent: AI\nAllow: /\n\nUser-agent: *\nDisallow: /wp-admin/\n", 496 578 ); 497 579 } … … 632 714 delete_transient( 'awef_toc_cache_cleared' ); 633 715 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'TOC cache cleared. Your changes are now active.', 'awesome-footnotes' ) . '</p></div>'; 716 } 717 718 // Show notice if llms.txt was created or writing failed. 719 if ( get_transient( 'awef_llms_written' ) ) { 720 delete_transient( 'awef_llms_written' ); 721 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'llms.txt was written to site root.', 'awesome-footnotes' ) . '</p></div>'; 722 } 723 if ( get_transient( 'awef_llms_write_failed' ) ) { 724 delete_transient( 'awef_llms_write_failed' ); 725 $path = esc_html( ABSPATH . 'llms.txt' ); 726 echo '<div class="notice notice-error is-dismissible"><p>' . sprintf( esc_html__( 'Could not write %s. Make the site root writable or create the file manually.', 'awesome-footnotes' ), $path ) . '</p></div>'; 634 727 } 635 728 -
awesome-footnotes/trunk/classes/settings/class-settings-builder.php
r3422097 r3429715 1350 1350 self::$placeholder_attr = ! empty( $placeholder ) ? 'placeholder="' . $placeholder . '"' : ''; 1351 1351 1352 // Get the option stored data. 1353 if ( ! empty( $data ) ) { 1352 // Get the option stored data. Use provided data even if it's an empty 1353 // value (''), otherwise fall back to the configured default. The 1354 // builder previously used `empty()` which caused explicitly-saved 1355 // empty strings to be ignored and the default to be shown instead. 1356 if ( $data !== false ) { 1354 1357 self::$current_value = $data; 1355 1358 } elseif ( ! empty( $default ) ) { -
awesome-footnotes/trunk/classes/settings/settings-options/options.php
r3366051 r3429715 179 179 'type' => 'checkbox', 180 180 'default' => Settings::get_current_options()['seo_post_options'], 181 'hint' => \esc_html__( 'Enable this if you want to use plugin SEO options in posts. Keep in mid that the plugin uses Posts excerpt field to store the Meta description!', 'awesome-footnotes' ), 182 ) 183 ); 181 'toggle' => '#llms_txt_content-item', 182 'hint' => \esc_html__( 'Enable this if you want to use plugin SEO options in posts. Keep in mind that the plugin uses Posts excerpt field to store the Meta description!', 'awesome-footnotes' ), 183 ) 184 ); 185 186 // llms.txt content editor - visible only when `seo_post_options` is enabled (toggle). 187 $writable = is_writable( ABSPATH ); 188 $hint = $writable 189 ? \esc_html__( 'Optional preset content for llms.txt to be written to site root when non-empty.', 'awesome-footnotes' ) 190 : sprintf( /* translators: %s: path */ \esc_html__( 'Plugin cannot write to site root (%s). Create llms.txt there or make it writable for automatic generation.', 'awesome-footnotes' ), esc_html( ABSPATH . 'llms.txt' ) ); 191 192 Settings::build_option( 193 array( 194 'name' => \esc_html__( 'llms.txt content', 'awesome-footnotes' ), 195 'id' => 'llms_txt_content', 196 'type' => Settings::get_current_options()['no_editor_header_footer'] ? 'textarea' : 'editor', 197 'hint' => $hint, 198 // Use the stored option value directly here to avoid merged defaults overwriting 199 // an explicitly-saved empty string when rendering the form after save. 200 'default' => ( \get_option( \AWEF_SETTINGS_NAME )['llms_txt_content'] ?? '' ), 201 ) 202 ); -
awesome-footnotes/trunk/readme.txt
r3422097 r3429715 96 96 Even though it's a little more typing, using the exact text method is much more robust. The number referencing will not work across multiple pages in a paged post (but will work within the page). Also, if you use the number referencing system you risk them identifying the incorrect footnote if you go back and insert a new footnote and forget to change the referenced number. 97 97 98 ## FAQ Schema Usage Guide 99 100 ### For Administrators 101 102 1. **Edit any post or page** 103 2. **Scroll to the "Awesome Footnotes - Settings" meta box** 104 3. **Click the "FAQ Schema" tab** 105 4. **Click "Add FAQ Item"** 106 5. **Fill in the Question and Answer fields** 107 6. **Repeat for additional FAQs** 108 7. **Use move up/down buttons to reorder** 109 8. **Click Remove to delete items** 110 9. **Save/Update the post** 111 112 ### For Content Display 113 114 1. **In the post content editor, add:** 115 ``` 116 [awef_faq] 117 ``` 118 2. **Publish/Update the post** 119 3. **View the post on the frontend** 120 4. **FAQs will display at the shortcode location** 121 5. **JSON-LD schema automatically added to `<head>`** 122 123 ### For Developers 124 125 **Get FAQ data programmatically:** 126 ```php 127 use AWEF\Controllers\Post_Settings; 128 129 $post_id = get_the_ID(); 130 $faqs = Post_Settings::get_faq_schema( $post_id ); 131 132 foreach ( $faqs as $faq ) { 133 echo $faq['question']; 134 echo $faq['answer']; 135 } 136 ``` 137 138 **Check if FAQ shortcode is used:** 139 ```php 140 $content = get_post_field( 'post_content', $post_id ); 141 if ( has_shortcode( $content, 'awef_faq' ) ) { 142 // FAQ shortcode is present 143 } 144 ``` 145 98 146 == Installation == 99 147
Note: See TracChangeset
for help on using the changeset viewer.