' . $tagline; $out[] = ''; } $out[] = 'Site: ' . esc_url_raw( home_url( '/' ) ); $out[] = ''; $pages = get_pages( array( 'sort_column' => 'menu_order,post_title', 'number' => 25, 'post_status' => 'publish', ) ); $out = array_merge( $out, self::section( __( 'Pages', 'aeo-tool-connect' ), is_array( $pages ) ? $pages : array() ) ); $posts = get_posts( array( 'numberposts' => 25, 'post_status' => 'publish', ) ); $out = array_merge( $out, self::section( __( 'Posts', 'aeo-tool-connect' ), is_array( $posts ) ? $posts : array() ) ); $out[] = '---'; $out[] = 'Generated locally by Everlange Connect (standalone mode).'; $out[] = 'A full AEO audit across 10 answer engines is available at https://everlange.com/'; return implode( "\n", $out ) . "\n"; } /** * Render one llms.txt section ("## Pages", "## Posts") as Markdown lines. * * @param string $heading Section heading, already translated. * @param WP_Post[] $items Published posts or pages. * @return string[] Lines including the trailing blank line, or an empty * array when nothing in $items is listable. */ private static function section( $heading, $items ) { $lines = array(); foreach ( $items as $item ) { if ( ! $item instanceof WP_Post ) { continue; } $title = wp_strip_all_tags( (string) $item->post_title ); if ( '' === trim( $title ) ) { continue; } if ( self::is_noindexed( $item->ID ) ) { continue; } $line = '- [' . $title . '](' . esc_url_raw( get_permalink( $item ) ) . ')'; $description = self::description( $item ); if ( '' !== $description ) { $line .= ' : ' . $description; } $lines[] = $line; } if ( empty( $lines ) ) { return array(); } array_unshift( $lines, '## ' . $heading ); $lines[] = ''; return $lines; } /** * Best available description for a post: the SEO plugin's meta description * first (that is the one the owner actually wrote for machines), then the * excerpt, then the content itself, trimmed. * * AIOSEO is not read here: it stores its descriptions in its own database * table, not in post meta, and querying another plugin's table directly is * not worth the coupling. Sites running AIOSEO fall back to the excerpt. * * @param WP_Post $item The post or page. * @return string Possibly empty. */ private static function description( $item ) { $meta_keys = array( '_yoast_wpseo_metadesc', // Yoast SEO. 'rank_math_description', // Rank Math. '_seopress_titles_desc', // SEOPress. ); foreach ( $meta_keys as $meta_key ) { $value = get_post_meta( $item->ID, $meta_key, true ); if ( is_string( $value ) && '' !== trim( $value ) ) { return wp_trim_words( wp_strip_all_tags( $value ), 30, '…' ); } } $excerpt = wp_strip_all_tags( (string) $item->post_excerpt ); if ( '' !== trim( $excerpt ) ) { return wp_trim_words( $excerpt, 30, '…' ); } $content = wp_strip_all_tags( strip_shortcodes( (string) $item->post_content ) ); if ( '' !== trim( $content ) ) { return wp_trim_words( $content, 30, '…' ); } return ''; } /** * Has the owner's SEO plugin marked this content noindex? * * Checked per plugin because each stores the flag differently: Yoast uses a * '1' string, Rank Math an array of robots tokens, SEOPress a 'yes' string. * Absence of any flag means indexable, which is WordPress's own default. * * @param int $post_id Post ID. * @return bool */ private static function is_noindexed( $post_id ) { if ( '1' === (string) get_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', true ) ) { return true; } $rank_math = get_post_meta( $post_id, 'rank_math_robots', true ); if ( is_array( $rank_math ) && in_array( 'noindex', $rank_math, true ) ) { return true; } if ( 'yes' === (string) get_post_meta( $post_id, '_seopress_robots_index', true ) ) { return true; } return false; } }