Plugin Directory

Changeset 3429715


Ignore:
Timestamp:
12/30/2025 04:02:30 PM (2 months ago)
Author:
awesomefootnotes
Message:

Adding the first version of my plugin

Location:
awesome-footnotes
Files:
6 added
10 edited

Legend:

Unmodified
Added
Removed
  • awesome-footnotes/tags/3.9.2/classes/controllers/class-post-settings.php

    r3422097 r3429715  
    3636         */
    3737        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';
    3843
    3944        /**
     
    100105
    101106            \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 );
    102111            \add_filter( 'TieLabs/meta_description', array( __CLASS__, 'get_meta_description' ) );
    103112
     
    105114
    106115            \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
    107467        }
    108468
     
    128488
    129489            // 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 ) ) {
    135495                return;
    136496            }
     
    139499
    140500            // 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
    142502            $all_settings_collected = Settings::collect_and_sanitize_options( $raw_settings );
    143503
     
    154514                ARRAY_FILTER_USE_KEY
    155515            );
     516
     517            // Handle FAQ schema data.
     518            self::save_faq_schema( $post_id );
    156519
    157520            // TOC subset for overrides.
     
    200563            $primary_taxonomy = self::get_primary_category_taxonomy( \get_post_type( $post_id ) );
    201564            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
    203566                $primary_id  = (int) $primary_raw;
    204567
     
    268631            } else {
    269632                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 );
    270674            }
    271675        }
     
    429833
    430834        /**
     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        /**
    431846         * Register The Meta Boxes
    432847         *
     
    471886            \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 );
    472887
     888            // Enqueue editor scripts for WYSIWYG editors in FAQ section
     889            \wp_enqueue_editor();
     890
    473891            $settings_tabs = array(
    474892
     
    505923                );
    506924            }
     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            );
    507931
    508932            ?>
     
    6771101                // $excerpt = \get_the_excerpt( $post );
    6781102                // if ( ! empty( $excerpt ) ) {
    679                 //  return $excerpt;
     1103                // return $excerpt;
    6801104                // } else {
    6811105                return self::the_short_content( 160, $raw );
     
    7071131            return $title;
    7081132        }
     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        }
    7091271    }
    7101272}
  • awesome-footnotes/tags/3.9.2/classes/helpers/class-settings.php

    r3422097 r3429715  
    110110             */
    111111            \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 );
    112160        }
    113161
     
    157205
    158206            $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            }
    159217
    160218            $footnotes_options['combine_identical_notes'] = ( array_key_exists( 'combine_identical_notes', $post_array ) ) ? filter_var( $post_array['combine_identical_notes'], FILTER_VALIDATE_BOOLEAN ) : false;
     
    252310            self::$current_options = $footnotes_options;
    253311
     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
    254326            // Purge TOC transients so changes reflect immediately and queue admin notice.
    255327            self::clear_toc_transients();
     
    307379                    self::$global_options = self::get_default_options();
    308380                    \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 ) {
    313386                        if ( ! isset( self::$global_options[ $key ] ) ) {
    314387                            self::$global_options[ $key ] = $value;
     388                            $added = true;
    315389                        }
    316390                    }
    317391
    318                     \update_option( AWEF_SETTINGS_NAME, self::$global_options );
     392                    if ( $added ) {
     393                        \update_option( AWEF_SETTINGS_NAME, self::$global_options );
     394                    }
    319395                }
    320396
     
    346422                    self::$current_options = self::get_default_options();
    347423                    \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 ) {
    352429                        if ( ! isset( self::$current_options[ $key ] ) ) {
    353430                            self::$current_options[ $key ] = $value;
     431                            $added = true;
    354432                        }
    355433                    }
    356434
    357                     \update_option( AWEF_SETTINGS_NAME, self::$current_options );
     435                    if ( $added ) {
     436                        \update_option( AWEF_SETTINGS_NAME, self::$current_options );
     437                    }
    358438                }
    359439
     
    494574                    'toc_post_types'           => array( 'post', 'page' ),
    495575                    '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",
    496578                );
    497579            }
     
    632714                delete_transient( 'awef_toc_cache_cleared' );
    633715                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>';
    634727            }
    635728
  • awesome-footnotes/tags/3.9.2/classes/settings/class-settings-builder.php

    r3422097 r3429715  
    13501350            self::$placeholder_attr = ! empty( $placeholder ) ? 'placeholder="' . $placeholder . '"' : '';
    13511351
    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 ) {
    13541357                self::$current_value = $data;
    13551358            } elseif ( ! empty( $default ) ) {
  • awesome-footnotes/tags/3.9.2/classes/settings/settings-options/options.php

    r3366051 r3429715  
    179179            'type'    => 'checkbox',
    180180            '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
     192Settings::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  
    9696Even 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.
    9797
     98## FAQ Schema Usage Guide
     99
     100### For Administrators
     101
     1021. **Edit any post or page**
     1032. **Scroll to the "Awesome Footnotes - Settings" meta box**
     1043. **Click the "FAQ Schema" tab**
     1054. **Click "Add FAQ Item"**
     1065. **Fill in the Question and Answer fields**
     1076. **Repeat for additional FAQs**
     1087. **Use move up/down buttons to reorder**
     1098. **Click Remove to delete items**
     1109. **Save/Update the post**
     111
     112### For Content Display
     113
     1141. **In the post content editor, add:**
     115   ```
     116   [awef_faq]
     117   ```
     1182. **Publish/Update the post**
     1193. **View the post on the frontend**
     1204. **FAQs will display at the shortcode location**
     1215. **JSON-LD schema automatically added to `<head>`**
     122
     123### For Developers
     124
     125**Get FAQ data programmatically:**
     126```php
     127use AWEF\Controllers\Post_Settings;
     128
     129$post_id = get_the_ID();
     130$faqs = Post_Settings::get_faq_schema( $post_id );
     131
     132foreach ( $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 );
     141if ( has_shortcode( $content, 'awef_faq' ) ) {
     142    // FAQ shortcode is present
     143}
     144```
     145
    98146== Installation ==
    99147
  • awesome-footnotes/trunk/classes/controllers/class-post-settings.php

    r3422097 r3429715  
    3636         */
    3737        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';
    3843
    3944        /**
     
    100105
    101106            \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 );
    102111            \add_filter( 'TieLabs/meta_description', array( __CLASS__, 'get_meta_description' ) );
    103112
     
    105114
    106115            \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
    107467        }
    108468
     
    128488
    129489            // 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 ) ) {
    135495                return;
    136496            }
     
    139499
    140500            // 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
    142502            $all_settings_collected = Settings::collect_and_sanitize_options( $raw_settings );
    143503
     
    154514                ARRAY_FILTER_USE_KEY
    155515            );
     516
     517            // Handle FAQ schema data.
     518            self::save_faq_schema( $post_id );
    156519
    157520            // TOC subset for overrides.
     
    200563            $primary_taxonomy = self::get_primary_category_taxonomy( \get_post_type( $post_id ) );
    201564            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
    203566                $primary_id  = (int) $primary_raw;
    204567
     
    268631            } else {
    269632                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 );
    270674            }
    271675        }
     
    429833
    430834        /**
     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        /**
    431846         * Register The Meta Boxes
    432847         *
     
    471886            \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 );
    472887
     888            // Enqueue editor scripts for WYSIWYG editors in FAQ section
     889            \wp_enqueue_editor();
     890
    473891            $settings_tabs = array(
    474892
     
    505923                );
    506924            }
     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            );
    507931
    508932            ?>
     
    6771101                // $excerpt = \get_the_excerpt( $post );
    6781102                // if ( ! empty( $excerpt ) ) {
    679                 //  return $excerpt;
     1103                // return $excerpt;
    6801104                // } else {
    6811105                return self::the_short_content( 160, $raw );
     
    7071131            return $title;
    7081132        }
     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        }
    7091271    }
    7101272}
  • awesome-footnotes/trunk/classes/helpers/class-settings.php

    r3422097 r3429715  
    110110             */
    111111            \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 );
    112160        }
    113161
     
    157205
    158206            $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            }
    159217
    160218            $footnotes_options['combine_identical_notes'] = ( array_key_exists( 'combine_identical_notes', $post_array ) ) ? filter_var( $post_array['combine_identical_notes'], FILTER_VALIDATE_BOOLEAN ) : false;
     
    252310            self::$current_options = $footnotes_options;
    253311
     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
    254326            // Purge TOC transients so changes reflect immediately and queue admin notice.
    255327            self::clear_toc_transients();
     
    307379                    self::$global_options = self::get_default_options();
    308380                    \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 ) {
    313386                        if ( ! isset( self::$global_options[ $key ] ) ) {
    314387                            self::$global_options[ $key ] = $value;
     388                            $added = true;
    315389                        }
    316390                    }
    317391
    318                     \update_option( AWEF_SETTINGS_NAME, self::$global_options );
     392                    if ( $added ) {
     393                        \update_option( AWEF_SETTINGS_NAME, self::$global_options );
     394                    }
    319395                }
    320396
     
    346422                    self::$current_options = self::get_default_options();
    347423                    \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 ) {
    352429                        if ( ! isset( self::$current_options[ $key ] ) ) {
    353430                            self::$current_options[ $key ] = $value;
     431                            $added = true;
    354432                        }
    355433                    }
    356434
    357                     \update_option( AWEF_SETTINGS_NAME, self::$current_options );
     435                    if ( $added ) {
     436                        \update_option( AWEF_SETTINGS_NAME, self::$current_options );
     437                    }
    358438                }
    359439
     
    494574                    'toc_post_types'           => array( 'post', 'page' ),
    495575                    '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",
    496578                );
    497579            }
     
    632714                delete_transient( 'awef_toc_cache_cleared' );
    633715                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>';
    634727            }
    635728
  • awesome-footnotes/trunk/classes/settings/class-settings-builder.php

    r3422097 r3429715  
    13501350            self::$placeholder_attr = ! empty( $placeholder ) ? 'placeholder="' . $placeholder . '"' : '';
    13511351
    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 ) {
    13541357                self::$current_value = $data;
    13551358            } elseif ( ! empty( $default ) ) {
  • awesome-footnotes/trunk/classes/settings/settings-options/options.php

    r3366051 r3429715  
    179179            'type'    => 'checkbox',
    180180            '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
     192Settings::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  
    9696Even 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.
    9797
     98## FAQ Schema Usage Guide
     99
     100### For Administrators
     101
     1021. **Edit any post or page**
     1032. **Scroll to the "Awesome Footnotes - Settings" meta box**
     1043. **Click the "FAQ Schema" tab**
     1054. **Click "Add FAQ Item"**
     1065. **Fill in the Question and Answer fields**
     1076. **Repeat for additional FAQs**
     1087. **Use move up/down buttons to reorder**
     1098. **Click Remove to delete items**
     1109. **Save/Update the post**
     111
     112### For Content Display
     113
     1141. **In the post content editor, add:**
     115   ```
     116   [awef_faq]
     117   ```
     1182. **Publish/Update the post**
     1193. **View the post on the frontend**
     1204. **FAQs will display at the shortcode location**
     1215. **JSON-LD schema automatically added to `<head>`**
     122
     123### For Developers
     124
     125**Get FAQ data programmatically:**
     126```php
     127use AWEF\Controllers\Post_Settings;
     128
     129$post_id = get_the_ID();
     130$faqs = Post_Settings::get_faq_schema( $post_id );
     131
     132foreach ( $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 );
     141if ( has_shortcode( $content, 'awef_faq' ) ) {
     142    // FAQ shortcode is present
     143}
     144```
     145
    98146== Installation ==
    99147
Note: See TracChangeset for help on using the changeset viewer.