*/ private const DEFAULTS = [ 'llms_index_enabled' => '1', 'token_diet_enabled' => '1', 'strip_layout' => '1', 'strip_styles' => '1', 'strip_svg' => '1', 'strip_iframes' => '1', 'strip_forms' => '1', 'strip_scripts' => '1', ]; // ------------------------------------------------------------------------- // Singleton // ------------------------------------------------------------------------- private static ?self $instance = null; public static function get_instance(): self { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } private function __construct() { // Admin-only hooks: menu, settings registration, assets. add_action( 'admin_menu', [ $this, 'register_menu' ] ); add_action( 'admin_init', [ $this, 'register_settings' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_settings_styles' ] ); // Front-end filter hooks — alter core plugin behaviour from saved options. // These must run on every request, not just admin requests. add_filter( 'aionly_should_clean_output', [ $this, 'filter_token_diet_enabled' ], 5, 2 ); add_filter( 'aionly_strip_token_bloat_tags', [ $this, 'filter_token_bloat_tags' ], 5 ); add_filter( 'aionly_strip_selectors', [ $this, 'filter_strip_selectors' ], 5 ); // LLM Index disable: intercept template_redirect at priority 0, // before the core plugin fires at priority 1. add_action( 'template_redirect', [ $this, 'maybe_block_llms_index' ], 0 ); } // ------------------------------------------------------------------------- // Options Helper // ------------------------------------------------------------------------- /** * Retrieve all settings, merged with defaults so callers always get a full * array even if the option has never been saved. * * @return array */ public static function get_settings(): array { $saved = get_option( self::OPTION_KEY, [] ); return array_merge( self::DEFAULTS, is_array( $saved ) ? $saved : [] ); } /** * Helper: return '1' or '' for a single setting key. */ public static function get( string $key ): string { $settings = self::get_settings(); return isset( $settings[ $key ] ) ? (string) $settings[ $key ] : ''; } // ------------------------------------------------------------------------- // Admin Menu // ------------------------------------------------------------------------- public function register_menu(): void { add_menu_page( esc_html__( 'AI-Only Pages', 'ai-only-pages' ), // esc_html__( 'AI-Only Pages', 'ai-only-pages' ), // menu label 'manage_options', // capability self::MENU_SLUG, // menu slug [ $this, 'render_settings_page' ], // callback 'dashicons-superhero', // icon 80 // position (after Settings) ); } // ------------------------------------------------------------------------- // Settings Registration (WordPress Settings API) // ------------------------------------------------------------------------- public function register_settings(): void { // register_setting() is all that is required for options.php to validate, // sanitize, and securely save submitted form data. The page uses a fully // custom card-based HTML layout in render_settings_page(), so // do_settings_sections() is never called. add_settings_section() and // add_settings_field() are therefore omitted — they would be dead code that // creates a maintenance trap without contributing anything to the UI or save flow. register_setting( self::OPTION_GROUP, self::OPTION_KEY, [ 'type' => 'array', 'sanitize_callback' => [ $this, 'sanitize_settings' ], 'default' => self::DEFAULTS, ] ); } // ------------------------------------------------------------------------- // Sanitization // ------------------------------------------------------------------------- /** * Sanitize all submitted settings values. * Each field is a binary toggle: accept only '1' or ''. * * @param mixed $raw * @return array<string, string> */ public function sanitize_settings( $raw ): array { $sanitized = []; $raw = is_array( $raw ) ? $raw : []; foreach ( self::DEFAULTS as $key => $_default ) { $sanitized[ $key ] = isset( $raw[ $key ] ) && '1' === (string) $raw[ $key ] ? '1' : ''; } return $sanitized; } // ------------------------------------------------------------------------- // Field Renderers // ------------------------------------------------------------------------- /** * Render a single on/off toggle checkbox with label and description. * * @param array<string, string> $args */ public function render_toggle_field( array $args ): void { $key = $args['key'] ?? ''; $label = $args['label'] ?? ''; $description = $args['description'] ?? ''; if ( '' === $key ) { return; } $settings = self::get_settings(); $checked = ! empty( $settings[ $key ] ) && '1' === $settings[ $key ]; $field_id = 'aionly_field_' . esc_attr( $key ); $name = esc_attr( self::OPTION_KEY . '[' . $key . ']' ); ?> <div class="aionly-toggle-row"> <label class="aionly-toggle-label" for="<?php echo esc_attr( $field_id ); ?>"> <div class="aionly-toggle-switch"> <input type="checkbox" id="<?php echo esc_attr( $field_id ); ?>" name="<?php echo esc_attr( $name ); ?>" value="1" <?php checked( $checked ); ?> /> <span class="aionly-toggle-slider"></span> </div> <span class="aionly-toggle-text"> <?php // $label may contain <code> tags — allow only that. echo wp_kses( $label, [ 'code' => [] ] ); ?> </span> </label> <?php if ( $description ) : ?> <p class="aionly-field-description"> <?php echo wp_kses( $description, [ 'code' => [], 'strong' => [ 'style' => [] ] ] ); ?> </p> <?php endif; ?> </div> <?php } // ------------------------------------------------------------------------- // Settings Page Renderer // ------------------------------------------------------------------------- public function render_settings_page(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'ai-only-pages' ) ); } $settings = self::get_settings(); $llms_url = esc_url( home_url( '/llms-index.txt' ) ); $diet_enabled = '1' === ( $settings['token_diet_enabled'] ?? '1' ); $index_enabled = '1' === ( $settings['llms_index_enabled'] ?? '1' ); ?> <div class="wrap aionly-settings-wrap"> <div class="aionly-page-header"> <div class="aionly-page-title"> <span class="dashicons dashicons-superhero" aria-hidden="true"></span> <h1><?php esc_html_e( 'AI-Only Pages', 'ai-only-pages' ); ?></h1> <span class="aionly-version-badge">v<?php echo esc_html( Plugin::VERSION ); ?></span> </div> <p class="aionly-page-subtitle"> <?php esc_html_e( 'Configure global behaviour for Token Diet output cleaning and the LLM discovery index. Per-page blocking is still managed via the meta box on each individual post or page.', 'ai-only-pages' ); ?> </p> </div> <?php settings_errors( self::OPTION_KEY ); ?> <form method="post" action="options.php"> <?php settings_fields( self::OPTION_GROUP ); ?> <?php /* ============================================================ CARD 1 — Instructions & Status ============================================================ */ ?> <div class="aionly-card aionly-card-status"> <div class="aionly-card-header"> <span class="dashicons dashicons-info-outline" aria-hidden="true"></span> <h2><?php esc_html_e( 'How It Works', 'ai-only-pages' ); ?></h2> </div> <div class="aionly-card-body"> <div class="aionly-how-it-works-grid"> <div class="aionly-how-step"> <div class="aionly-how-step-icon"> <span class="dashicons dashicons-edit-page" aria-hidden="true"></span> </div> <div class="aionly-how-step-content"> <strong><?php esc_html_e( '1. Mark Pages', 'ai-only-pages' ); ?></strong> <p><?php esc_html_e( 'Open any post or page in the editor and find the "AI-Only Pages" meta box in the sidebar. Check the bots you want to block — or use the master toggle to block all 10 search engine bots at once.', 'ai-only-pages' ); ?></p> </div> </div> <div class="aionly-how-step"> <div class="aionly-how-step-icon"> <span class="dashicons dashicons-filter" aria-hidden="true"></span> </div> <div class="aionly-how-step-content"> <strong><?php esc_html_e( '2. Token Diet Cleans Output', 'ai-only-pages' ); ?></strong> <p><?php esc_html_e( 'When an AI crawler (GPTBot, ClaudeBot, PerplexityBot, etc.) visits a marked page, the plugin rewrites the HTML on the fly — stripping navigation, sidebars, styles, SVGs, and other token-heavy noise so AI models receive pure, high-signal content.', 'ai-only-pages' ); ?></p> </div> </div> <div class="aionly-how-step"> <div class="aionly-how-step-icon"> <span class="dashicons dashicons-list-view" aria-hidden="true"></span> </div> <div class="aionly-how-step-content"> <strong><?php esc_html_e( '3. LLM Index Lists Your Pages', 'ai-only-pages' ); ?></strong> <p><?php esc_html_e( 'All marked pages are automatically listed in the /llms-index.txt discovery file. AI crawlers can bookmark this URL to continuously find your AI-optimised content without waiting for a sitemap crawl.', 'ai-only-pages' ); ?></p> </div> </div> </div> <div class="aionly-status-strip <?php echo esc_attr( $index_enabled ? 'aionly-status-active' : 'aionly-status-inactive' ); ?>"> <span class="dashicons <?php echo esc_attr( $index_enabled ? 'dashicons-yes-alt' : 'dashicons-dismiss' ); ?>" aria-hidden="true"></span> <div> <strong> <?php echo $index_enabled ? esc_html__( 'LLM Index is live', 'ai-only-pages' ) : esc_html__( 'LLM Index is disabled', 'ai-only-pages' ); ?> </strong> <?php if ( $index_enabled ) : ?> <br /> <span class="aionly-status-url-label"> <?php esc_html_e( 'Your AI discovery file is publicly accessible at:', 'ai-only-pages' ); ?> </span> <a href="<?php echo esc_url( $llms_url ); ?>" target="_blank" rel="noopener noreferrer" class="aionly-llms-url" ><?php echo esc_html( $llms_url ); ?></a> <?php else : ?> <br /> <span class="aionly-status-url-label"> <?php esc_html_e( 'Enable the LLM Index in the section below to restore this endpoint.', 'ai-only-pages' ); ?> </span> <?php endif; ?> </div> </div> </div> </div> <?php /* ============================================================ CARD 2 — LLM Index Settings ============================================================ */ ?> <div class="aionly-card"> <div class="aionly-card-header"> <span class="dashicons dashicons-list-view" aria-hidden="true"></span> <h2><?php esc_html_e( 'LLM Index Settings', 'ai-only-pages' ); ?></h2> </div> <div class="aionly-card-body"> <p class="aionly-section-description"> <?php esc_html_e( 'The /llms-index.txt file is a plain-text discovery manifest listing all your AI-Only pages with their titles and last-modified dates. It follows the emerging llms.txt convention. AI crawlers, ChatGPT plugins, and RAG pipelines can use this URL to continuously discover your AI-optimised content.', 'ai-only-pages' ); ?> </p> <table class="form-table aionly-form-table" role="presentation"> <tbody> <tr> <th scope="row"> <?php esc_html_e( 'LLM Index', 'ai-only-pages' ); ?> </th> <td> <?php $this->render_toggle_field( [ 'key' => 'llms_index_enabled', 'label' => esc_html__( 'Serve the /llms-index.txt discovery file', 'ai-only-pages' ), 'description' => esc_html__( 'When disabled, the /llms-index.txt endpoint returns a 404. AI crawlers that have bookmarked the URL will stop receiving updates.', 'ai-only-pages' ), ] ); ?> </td> </tr> </tbody> </table> </div> </div> <?php /* ============================================================ CARD 3 — Token Diet Master Control ============================================================ */ ?> <div class="aionly-card"> <div class="aionly-card-header"> <span class="dashicons dashicons-performance" aria-hidden="true"></span> <h2><?php esc_html_e( 'Token Diet — Master Control', 'ai-only-pages' ); ?></h2> </div> <div class="aionly-card-body"> <p class="aionly-section-description"> <?php echo wp_kses( sprintf( /* translators: %1$s: opening <a> tag, %2$s: closing </a> tag */ __( 'Token Diet rewrites the HTML served to AI crawlers in real-time, removing chrome and noise so AI models receive clean, high-signal content. The three-pass process strips structural layout, token-bloating tags, and non-semantic HTML attributes. %1$sSee the developer reference%2$s for caching plugin caveats.', 'ai-only-pages' ), '<a href="https://wordpress.org/plugins/ai-only-pages/#developers" target="_blank" rel="noopener noreferrer">', '</a>' ), [ 'a' => [ 'href' => [], 'target' => [], 'rel' => [] ] ] ); ?> </p> <table class="form-table aionly-form-table" role="presentation"> <tbody> <tr> <th scope="row"> <?php esc_html_e( 'Token Diet', 'ai-only-pages' ); ?> </th> <td> <?php $this->render_toggle_field( [ 'key' => 'token_diet_enabled', 'label' => esc_html__( 'Clean HTML output for AI crawlers', 'ai-only-pages' ), 'description' => esc_html__( 'When disabled, AI bots receive the raw, full HTML of the page — identical to what human visitors see. All granular stripping settings below are ignored while this master toggle is off.', 'ai-only-pages' ), ] ); ?> </td> </tr> </tbody> </table> </div> </div> <?php /* ============================================================ CARD 4 — Granular Token Diet Tag Stripping ============================================================ */ ?> <div class="aionly-card <?php echo esc_attr( ! $diet_enabled ? 'aionly-card-dimmed' : '' ); ?>"> <div class="aionly-card-header"> <span class="dashicons dashicons-editor-code" aria-hidden="true"></span> <h2><?php esc_html_e( 'Token Diet — Granular Stripping', 'ai-only-pages' ); ?></h2> <?php if ( ! $diet_enabled ) : ?> <span class="aionly-badge-disabled"> <?php esc_html_e( 'Token Diet is off — these settings have no effect', 'ai-only-pages' ); ?> </span> <?php endif; ?> </div> <div class="aionly-card-body"> <p class="aionly-section-description"> <?php esc_html_e( 'Choose exactly which HTML elements are stripped from the output served to AI crawlers. These controls run during Token Diet Pass 1 (structural layout) and Pass 2 (tag removal). Pass 3 attribute stripping always runs when Token Diet is enabled and cannot be disabled here.', 'ai-only-pages' ); ?> </p> <table class="form-table aionly-form-table" role="presentation"> <tbody> <tr> <th scope="row"><?php esc_html_e( 'Layout Chrome', 'ai-only-pages' ); ?></th> <td> <?php $this->render_toggle_field( [ 'key' => 'strip_layout', 'label' => esc_html__( 'Strip structural layout (Headers, Footers, Sidebars, Navigations)', 'ai-only-pages' ), 'description' => esc_html__( 'Removes <header>, <footer>, <nav>, <aside>, sidebars, cookie banners, admin bars, and common WordPress/Elementor/Divi layout wrappers. Highly recommended — these elements add zero informational value for LLMs.', 'ai-only-pages' ), ] ); ?> </td> </tr> <tr> <th scope="row"><?php esc_html_e( 'CSS', 'ai-only-pages' ); ?></th> <td> <?php $this->render_toggle_field( [ 'key' => 'strip_styles', 'label' => esc_html__( 'Strip <style> tags and embedded CSS', 'ai-only-pages' ), 'description' => esc_html__( 'Removes all embedded CSS blocks. AI crawlers have no use for visual styling information, and large theme style blocks can consume thousands of tokens.', 'ai-only-pages' ), ] ); ?> </td> </tr> <tr> <th scope="row"><?php esc_html_e( 'SVG', 'ai-only-pages' ); ?></th> <td> <?php $this->render_toggle_field( [ 'key' => 'strip_svg', 'label' => esc_html__( 'Strip <svg> elements', 'ai-only-pages' ), 'description' => esc_html__( 'SVG markup is one of the heaviest token bloaters in modern WordPress themes. Inline SVGs for icons, logos, and illustrations can add thousands of tokens of noise with zero semantic value for LLMs.', 'ai-only-pages' ), ] ); ?> </td> </tr> <tr> <th scope="row"><?php esc_html_e( 'iFrames', 'ai-only-pages' ); ?></th> <td> <?php $this->render_toggle_field( [ 'key' => 'strip_iframes', 'label' => esc_html__( 'Strip <iframe> elements', 'ai-only-pages' ), 'description' => esc_html__( 'Removes embedded maps, social media widgets, video embeds, and third-party iframes. These are non-indexable UI elements that LLMs cannot use.', 'ai-only-pages' ), ] ); ?> </td> </tr> <tr> <th scope="row"><?php esc_html_e( 'Forms', 'ai-only-pages' ); ?></th> <td> <?php $this->render_toggle_field( [ 'key' => 'strip_forms', 'label' => esc_html__( 'Strip <form> elements', 'ai-only-pages' ), 'description' => wp_kses( __( '<strong style="color:#d63638;">Warning:</strong> Stripping forms removes WooCommerce "Add to Cart" buttons, checkout flows, contact forms, and search boxes from AI view. Disable this toggle if you need AI crawlers to see your product purchase UI or if you use forms for structured data capture.', 'ai-only-pages' ), [ 'strong' => [ 'style' => [] ] ] ), ] ); ?> </td> </tr> <tr> <th scope="row"><?php esc_html_e( 'Scripts', 'ai-only-pages' ); ?></th> <td> <?php $this->render_toggle_field( [ 'key' => 'strip_scripts', 'label' => esc_html__( 'Strip <script> tags', 'ai-only-pages' ), 'description' => wp_kses( __( '<strong style="color:#2271b1;">Important:</strong> Schema markup (<code>application/ld+json</code>) is incredibly valuable to AI — it encodes structured product data, breadcrumbs, FAQs, article metadata, and more in a format LLMs can parse directly. It is <strong>always preserved</strong> regardless of this setting. Only non-schema JavaScript is stripped.', 'ai-only-pages' ), [ 'strong' => [ 'style' => [] ], 'code' => [] ] ), ] ); ?> </td> </tr> </tbody> </table> </div> </div> <div class="aionly-submit-row"> <?php submit_button( esc_html__( 'Save Settings', 'ai-only-pages' ), 'primary aionly-save-button', 'submit', false ); ?> <span class="aionly-submit-note"> <?php esc_html_e( 'Changes take effect immediately on the next AI crawler visit.', 'ai-only-pages' ); ?> </span> </div> </form> </div> <?php } // ------------------------------------------------------------------------- // Admin CSS (Settings Page Only) // ------------------------------------------------------------------------- public function enqueue_settings_styles( string $hook ): void { // Only load on our own settings page. if ( 'toplevel_page_' . self::MENU_SLUG !== $hook ) { return; } // wp_add_inline_style() requires a previously registered handle. // Register a handle with no src (false), enqueue it, then attach the // inline CSS to it — the only reliable cross-setup approach. wp_register_style( 'aionly-settings-ui', false, [], Plugin::VERSION ); wp_enqueue_style( 'aionly-settings-ui' ); wp_add_inline_style( 'aionly-settings-ui', $this->get_settings_css() ); } private function get_settings_css(): string { return ' /* ===== AI-Only Pages Settings Page ===== */ .aionly-settings-wrap { max-width: 900px; margin-top: 24px; } /* ---- Page Header ---- */ .aionly-page-header { margin-bottom: 28px; } .aionly-page-title { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; } .aionly-page-title .dashicons { font-size: 28px; width: 28px; height: 28px; color: #2271b1; } .aionly-page-title h1 { margin: 0; font-size: 24px; font-weight: 700; color: #1d2327; line-height: 1; } .aionly-version-badge { display: inline-flex; align-items: center; background: #e8f0fc; color: #2271b1; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; letter-spacing: 0.03em; } .aionly-page-subtitle { color: #50575e; font-size: 13px; margin: 0; max-width: 680px; line-height: 1.6; } /* ---- Cards ---- */ .aionly-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,.05); overflow: hidden; transition: opacity .2s ease; } .aionly-card-dimmed { opacity: 0.65; pointer-events: none; } .aionly-card-status { border-top: 3px solid #2271b1; } .aionly-card-header { display: flex; align-items: center; gap: 10px; padding: 16px 24px; border-bottom: 1px solid #f0f0f0; background: #fafafa; } .aionly-card-header .dashicons { font-size: 18px; width: 18px; height: 18px; color: #2271b1; flex-shrink: 0; } .aionly-card-header h2 { margin: 0; font-size: 14px; font-weight: 600; color: #1d2327; } .aionly-badge-disabled { margin-left: auto; font-size: 11px; font-weight: 500; color: #8c8f94; background: #f0f0f1; padding: 3px 10px; border-radius: 10px; } .aionly-card-body { padding: 20px 24px; } /* ---- How It Works Grid ---- */ .aionly-how-it-works-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin-bottom: 20px; } .aionly-how-step { display: flex; gap: 12px; align-items: flex-start; } .aionly-how-step-icon { flex-shrink: 0; width: 36px; height: 36px; background: #e8f0fc; border-radius: 8px; display: flex; align-items: center; justify-content: center; } .aionly-how-step-icon .dashicons { color: #2271b1; font-size: 16px; width: 16px; height: 16px; } .aionly-how-step-content strong { display: block; font-size: 13px; color: #1d2327; margin-bottom: 4px; } .aionly-how-step-content p { margin: 0; font-size: 12px; color: #50575e; line-height: 1.6; } /* ---- Status Strip ---- */ .aionly-status-strip { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border-radius: 6px; font-size: 13px; line-height: 1.5; } .aionly-status-strip .dashicons { font-size: 20px; width: 20px; height: 20px; flex-shrink: 0; margin-top: 1px; } .aionly-status-active { background: #edfdf4; border: 1px solid #00a06a; color: #007a4d; } .aionly-status-active .dashicons { color: #00a06a; } .aionly-status-inactive { background: #fcf0f1; border: 1px solid #d63638; color: #d63638; } .aionly-status-inactive .dashicons { color: #d63638; } .aionly-status-url-label { font-size: 12px; opacity: 0.85; } .aionly-llms-url { display: inline-block; font-family: monospace; font-size: 13px; font-weight: 600; word-break: break-all; color: #007a4d; } /* ---- Section Description ---- */ .aionly-section-description { color: #50575e; font-size: 13px; margin: 0 0 16px; line-height: 1.6; max-width: 680px; } /* ---- Form Table ---- */ .aionly-form-table { border-collapse: collapse; width: 100%; } .aionly-form-table th { width: 180px; padding: 14px 16px 14px 0; vertical-align: top; font-size: 13px; font-weight: 600; color: #1d2327; } .aionly-form-table td { padding: 12px 0; vertical-align: top; } .aionly-form-table tr + tr { border-top: 1px solid #f0f0f1; } /* ---- Toggle Switch ---- */ .aionly-toggle-row { display: flex; flex-direction: column; gap: 6px; } .aionly-toggle-label { display: flex; align-items: center; gap: 10px; cursor: pointer; } .aionly-toggle-switch { position: relative; display: inline-block; width: 40px; height: 22px; flex-shrink: 0; } .aionly-toggle-switch input { opacity: 0; width: 0; height: 0; position: absolute; } .aionly-toggle-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #c3c4c7; border-radius: 22px; transition: background .2s; cursor: pointer; } .aionly-toggle-slider:before { content: ""; position: absolute; width: 16px; height: 16px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: transform .2s; box-shadow: 0 1px 3px rgba(0,0,0,.2); } .aionly-toggle-switch input:checked + .aionly-toggle-slider { background: #2271b1; } .aionly-toggle-switch input:checked + .aionly-toggle-slider:before { transform: translateX(18px); } .aionly-toggle-switch input:focus + .aionly-toggle-slider { box-shadow: 0 0 0 2px #2271b1; } .aionly-toggle-text { font-size: 13px; color: #1d2327; font-weight: 500; } .aionly-field-description { margin: 0 0 0 50px; font-size: 12px; color: #50575e; line-height: 1.6; max-width: 580px; } /* ---- Submit Row ---- */ .aionly-submit-row { display: flex; align-items: center; gap: 14px; margin-top: 8px; padding-bottom: 40px; } .aionly-save-button.button-primary { height: 38px; padding: 0 22px; font-size: 14px; border-radius: 5px; } .aionly-submit-note { font-size: 12px; color: #8c8f94; } '; } // ------------------------------------------------------------------------- // Front-End Filter Hooks // ------------------------------------------------------------------------- /** * Hook: aionly_should_clean_output (priority 5, before developer filters). * * If the Token Diet master toggle is OFF, return false to disable all * cleaning for this request. The core plugin will serve raw HTML. * * @param bool $enabled Passed value (default true from core plugin). * @param \WP_Post $post The current post. * @return bool */ public function filter_token_diet_enabled( bool $enabled, $post ): bool { if ( ! $enabled ) { // Already disabled by another filter — honour that. return false; } return '1' === self::get( 'token_diet_enabled' ); } /** * Hook: aionly_strip_token_bloat_tags (priority 5, before developer filters). * * Dynamically builds the XPath query array that Pass 2 of Token Diet uses. * Each granular toggle maps to one XPath query. Queries for disabled toggles * are simply omitted from the array. * * The //script query is special: the core default preserves * application/ld+json scripts. We keep that nuance here. * * @param string[] $queries Existing array from core plugin. * @return string[] */ public function filter_token_bloat_tags( array $queries ): array { // If Token Diet is globally disabled this filter should not run — // but guard here defensively. if ( '1' !== self::get( 'token_diet_enabled' ) ) { return $queries; } // Build a fresh list based solely on the saved settings. // We replace (not merge with) the defaults so user choices are authoritative. $new_queries = []; if ( '1' === self::get( 'strip_styles' ) ) { $new_queries[] = '//style'; } if ( '1' === self::get( 'strip_svg' ) ) { $new_queries[] = '//svg'; } if ( '1' === self::get( 'strip_iframes' ) ) { $new_queries[] = '//iframe'; } if ( '1' === self::get( 'strip_forms' ) ) { $new_queries[] = '//form'; } if ( '1' === self::get( 'strip_scripts' ) ) { // Always preserve application/ld+json schema scripts. $new_queries[] = '//script[not(translate(@type,"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz")="application/ld+json")]'; } return $new_queries; } /** * Hook: aionly_strip_selectors (priority 5, before developer filters). * * If the "strip layout" toggle is OFF, return an empty array so Pass 1 * of Token Diet skips structural element removal entirely. * * If it is ON, return the defaults unchanged — the core Plugin constant * STRIP_ELEMENTS is already the passed-in value from the core filter. * * @param string[] $selectors Existing array from core plugin. * @return string[] */ public function filter_strip_selectors( array $selectors ): array { if ( '1' !== self::get( 'token_diet_enabled' ) ) { return []; } if ( '1' !== self::get( 'strip_layout' ) ) { return []; } return $selectors; } /** * Hook: template_redirect (priority 0, fires BEFORE core plugin at priority 1). * * When the LLM Index is disabled, intercept the request before the core plugin * can serve it and return a 404 instead. */ public function maybe_block_llms_index(): void { if ( '1' === self::get( 'llms_index_enabled' ) ) { return; // Index is enabled — do nothing, let the core plugin handle it. } if ( ! get_query_var( 'aionly_llms_index' ) ) { return; // Not an llms-index.txt request. } // Serve a clean 404 and stop further processing. status_header( 404 ); nocache_headers(); header( 'Content-Type: text/plain; charset=UTF-8' ); echo '404 Not Found'; exit; } } // Bootstrap on plugins_loaded — same strategy as the core plugin. add_action( 'plugins_loaded', [ '\AIOnly\Pages\Settings', 'get_instance' ] );