output_llms_txt(); exit; } $this->output_llms_full_txt(); exit; } /** * Outputs the llms.txt file. * * @since 1.0.0 */ private function output_llms_txt() { status_header( 200 ); header( 'Content-Type: text/plain; charset=utf-8' ); header( 'Cache-Control: public, max-age=3600' ); header( 'X-Generated-By: Aidentity GEO ' . AIDENTITY_GEO_VERSION ); $content = get_transient( 'aidentity_geo_llms_txt' ); if ( false === $content ) { $content = $this->generate_llms_txt(); set_transient( 'aidentity_geo_llms_txt', $content, HOUR_IN_SECONDS ); } echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain text output. } /** * Outputs the llms-full.txt file. * * @since 1.0.0 */ private function output_llms_full_txt() { status_header( 200 ); header( 'Content-Type: text/plain; charset=utf-8' ); header( 'Cache-Control: public, max-age=3600' ); header( 'X-Generated-By: Aidentity GEO ' . AIDENTITY_GEO_VERSION ); $content = get_transient( 'aidentity_geo_llms_full_txt' ); if ( false === $content ) { $content = $this->generate_llms_full_txt(); set_transient( 'aidentity_geo_llms_full_txt', $content, HOUR_IN_SECONDS ); } echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain text output. } /** * Generates the files and stores them in cache. * * @since 1.0.0 */ public function generate_file() { // Clear the cache. delete_transient( 'aidentity_geo_llms_txt' ); delete_transient( 'aidentity_geo_llms_full_txt' ); // Pre-generate the files. $llms_txt = $this->generate_llms_txt(); $llms_full = $this->generate_llms_full_txt(); set_transient( 'aidentity_geo_llms_txt', $llms_txt, HOUR_IN_SECONDS ); set_transient( 'aidentity_geo_llms_full_txt', $llms_full, HOUR_IN_SECONDS ); } /** * Generates the llms.txt content. * * @since 1.0.0 * @return string File content. */ private function generate_llms_txt() { $site_name = get_bloginfo( 'name' ); $site_description = get_bloginfo( 'description' ); // Header with entity information. $output = "# {$site_name}\n\n"; $output .= "> {$site_description}\n\n"; $output .= '**' . esc_html__( 'Site:', 'aidentity-generative-engine-optimizer' ) . '** ' . home_url() . "\n\n"; // Links to AI resources. $output .= '## ' . esc_html__( 'Resources for AI', 'aidentity-generative-engine-optimizer' ) . "\n\n"; $output .= '- [' . esc_html__( 'Full content', 'aidentity-generative-engine-optimizer' ) . '](' . home_url( '/llms-full.txt' ) . ")\n"; $output .= '- [Sitemap XML](' . home_url( '/wp-sitemap.xml' ) . ")\n\n"; // Main pages. $pages = get_posts( array( 'post_type' => 'page', 'posts_per_page' => 20, 'post_status' => 'publish', 'orderby' => 'menu_order', 'order' => 'ASC', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', array( 'key' => '_geo_exclude', 'compare' => 'NOT EXISTS', ), array( 'key' => '_geo_exclude', 'value' => '1', 'compare' => '!=', ), ), ) ); if ( ! empty( $pages ) ) { $output .= '## ' . esc_html__( 'Main Pages', 'aidentity-generative-engine-optimizer' ) . "\n\n"; foreach ( $pages as $page ) { $description = $this->get_post_description( $page ); $url = get_permalink( $page ); $output .= "- [{$page->post_title}]({$url})"; if ( $description ) { $output .= ": {$description}"; } $output .= "\n"; } $output .= "\n"; } // Posts grouped by category. $categories = get_categories( array( 'orderby' => 'count', 'order' => 'DESC', 'number' => 10, 'hide_empty' => true, ) ); foreach ( $categories as $category ) { $posts = get_posts( array( 'post_type' => 'post', 'posts_per_page' => 10, 'post_status' => 'publish', 'category' => $category->term_id, 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', array( 'key' => '_geo_exclude', 'compare' => 'NOT EXISTS', ), array( 'key' => '_geo_exclude', 'value' => '1', 'compare' => '!=', ), ), ) ); if ( ! empty( $posts ) ) { $output .= "## {$category->name}\n\n"; foreach ( $posts as $post ) { $description = $this->get_post_description( $post ); $url = get_permalink( $post ); $output .= "- [{$post->post_title}]({$url})"; if ( $description ) { $output .= ": {$description}"; } $output .= "\n"; } $output .= "\n"; } } // Footer. $output .= "---\n"; $output .= sprintf( /* translators: %1$s: plugin version, %2$s: current date and time */ esc_html__( 'Generated by Aidentity GEO v%1$s | %2$s', 'aidentity-generative-engine-optimizer' ), AIDENTITY_GEO_VERSION, current_time( 'Y-m-d H:i' ) ) . "\n"; return $output; } /** * Generates the llms-full.txt content. * * @since 1.0.0 * @return string File content. */ private function generate_llms_full_txt() { $site_name = get_bloginfo( 'name' ); $site_description = get_bloginfo( 'description' ); $output = "# {$site_name}\n\n"; $output .= "> {$site_description}\n\n"; $output .= "---\n\n"; // All published content. $posts = get_posts( array( 'post_type' => array( 'post', 'page' ), 'posts_per_page' => 100, 'post_status' => 'publish', 'orderby' => 'date', 'order' => 'DESC', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', array( 'key' => '_geo_exclude', 'compare' => 'NOT EXISTS', ), array( 'key' => '_geo_exclude', 'value' => '1', 'compare' => '!=', ), ), ) ); foreach ( $posts as $post ) { $output .= "## {$post->post_title}\n\n"; $output .= '**URL:** ' . get_permalink( $post ) . "\n"; $output .= '**' . esc_html__( 'Date:', 'aidentity-generative-engine-optimizer' ) . '** ' . get_the_date( 'Y-m-d', $post ) . "\n\n"; // Convert content to simplified markdown. $content = $this->html_to_markdown( $post->post_content ); $output .= $content . "\n\n"; $output .= "---\n\n"; } return $output; } /** * Gets the description of a post. * * @since 1.0.0 * @param WP_Post $post Post object. * @return string Post description. */ private function get_post_description( $post ) { // Try the excerpt first. if ( ! empty( $post->post_excerpt ) ) { return wp_trim_words( $post->post_excerpt, 20, '...' ); } // Try Yoast meta description. $yoast_desc = get_post_meta( $post->ID, '_yoast_wpseo_metadesc', true ); if ( ! empty( $yoast_desc ) ) { return wp_trim_words( $yoast_desc, 20, '...' ); } // Try RankMath meta description. $rank_desc = get_post_meta( $post->ID, 'rank_math_description', true ); if ( ! empty( $rank_desc ) ) { return wp_trim_words( $rank_desc, 20, '...' ); } // Fallback: first words of the content. $content = wp_strip_all_tags( $post->post_content ); if ( ! empty( $content ) ) { return wp_trim_words( $content, 20, '...' ); } return ''; } /** * Converts HTML to Markdown. * * @since 1.0.0 * @param string $html HTML content. * @return string Markdown content. */ private function html_to_markdown( $html ) { // Remove shortcodes. $content = strip_shortcodes( $html ); // Process Gutenberg blocks. $content = do_blocks( $content ); // Convert headings. $content = preg_replace( '/
]*>(.*?)<\/p>/is', '$1' . "\n\n", $content ); // Convert lists. $content = preg_replace( '/