generate_and_save(); } // ── Rewrite rules: serve /llms.txt and /llms-full.txt via WP ───────────────── add_action( 'init', 'llmstxt_rewrite_rules' ); /** * Register custom rewrite rules. */ function llmstxt_rewrite_rules(): void { add_rewrite_rule( '^llms\\.txt$', 'index.php?llmstxt=1', 'top' ); add_rewrite_rule( '^llms-full\\.txt$', 'index.php?llmstxt=full', 'top' ); } add_filter( 'query_vars', 'llmstxt_query_vars' ); /** * Register custom query variables. * * @param array $vars Existing query vars. * @return array */ function llmstxt_query_vars( array $vars ): array { $vars[] = 'llmstxt'; return $vars; } add_action( 'template_redirect', 'llmstxt_template_redirect' ); /** * Serve llms.txt / llms-full.txt when the custom query var is present. */ function llmstxt_template_redirect(): void { $val = get_query_var( 'llmstxt' ); if ( ! $val ) { return; } $type = ( 'full' === $val ) ? 'full' : 'standard'; $content = LLMSTXT_File_Writer::read( $type ); if ( ! $content ) { // Generate on the fly if no cached file exists yet. $generator = new LLMSTXT_Generator(); $content = $generator->build_content( $type ); } // Strip site-name suffixes and typographic separator chars from all lines before output. $site_name = get_bloginfo( 'name' ); $content = llmstxt_sanitize_output( $content, $site_name ); header( 'Content-Type: text/plain; charset=UTF-8' ); header( 'Cache-Control: public, max-age=3600' ); header( 'X-Robots-Tag: noindex' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain text file, escaping would corrupt it. echo $content; exit; } /** * Sanitize llms.txt output: strip site-name suffixes and typographic separators * that SEO plugins may have injected into WordPress titles or the tagline. * * Applied as a final pass before serving the file, regardless of which * generator produced the content. * * @param string $content Raw llms.txt content. * @param string $site_name WordPress site name (get_bloginfo('name')). * @return string Cleaned content. */ function llmstxt_sanitize_output( string $content, string $site_name ): string { if ( ! $site_name ) { return $content; } $lines = explode( "\n", $content ); $escaped = preg_quote( $site_name, '/' ); foreach ( $lines as &$line ) { // Strip " – Site Name", " — Site Name", " | Site Name", " - Site Name" // from inside Markdown link labels: [Title – Site Name](url) $line = preg_replace( '/(\[)([^\]]+?)\s*[\x{2013}\x{2014}\x{007C}\-]\s*' . $escaped . '\s*(\]\()/u', '$1$2$3', $line ) ?? $line; // Also strip from plain text lines (e.g. tagline, section values). $line = preg_replace( '/\s*[\x{2013}\x{2014}\x{007C}]\s*' . $escaped . '\s*$/u', '', $line ) ?? $line; // Strip middle dots used as separators (· bullet) — replace with space, trim. $line = preg_replace( '/\s*\x{00B7}\s*/u', ' ', $line ) ?? $line; $line = trim( $line ); } unset( $line ); return implode( "\n", $lines ); } // ── Activation / Deactivation ───────────────────────────────────────────────── register_activation_hook( __FILE__, 'llmstxt_activate' ); /** * Plugin activation: set defaults, generate initial file, flush rewrite rules. */ function llmstxt_activate(): void { if ( ! get_option( 'llmstxt_settings' ) ) { update_option( 'llmstxt_settings', array( 'mode' => 'free', 'pro_key' => '', 'auto_update' => true, 'include_products' => true, 'max_products' => 50, 'include_posts' => true, 'max_posts' => 20, 'include_pages' => true, 'custom_sections' => '', 'webhook_secret' => '', ) ); } LLMSTXT_Htaccess::insert(); llmstxt_do_regen(); llmstxt_rewrite_rules(); flush_rewrite_rules(); } register_deactivation_hook( __FILE__, 'llmstxt_deactivate' ); /** * Plugin deactivation: unschedule cron and flush rewrite rules. */ function llmstxt_deactivate(): void { LLMSTXT_Htaccess::remove(); wp_unschedule_hook( 'llmstxt_async_regen' ); flush_rewrite_rules(); } // ── REST API: external regeneration trigger ─────────────────────────────────── add_action( 'rest_api_init', 'llmstxt_rest_routes' ); /** * Register the REST API route. */ function llmstxt_rest_routes(): void { register_rest_route( 'llmstxt/v1', '/regenerate', array( 'methods' => 'POST', 'callback' => 'llmstxt_rest_regen', 'permission_callback' => 'llmstxt_rest_permission', ) ); } /** * Permission callback for the REST regeneration endpoint. * * @param WP_REST_Request $request The request object. */ function llmstxt_rest_permission( WP_REST_Request $request ): bool { $key = $request->get_header( 'X-LLMSTXT-Key' ); $opts = get_option( 'llmstxt_settings', array() ); return ! empty( $opts['webhook_secret'] ) && ! empty( $key ) && hash_equals( $opts['webhook_secret'], $key ); } /** * REST callback: regenerate and return result. * * @param WP_REST_Request $request The request object. */ function llmstxt_rest_regen( WP_REST_Request $request ): WP_REST_Response { $generator = new LLMSTXT_Generator(); $result = $generator->generate_and_save(); return new WP_REST_Response( array( 'success' => $result, 'time' => current_time( 'mysql' ), ), 200 ); }