Plain sentences, one per failing check. */ function citolume_llms_attention() { $problems = array(); if ( ! citolume_llms_enabled() ) { $problems[] = __( 'llms.txt is switched off, so /llms.txt answers 404. Turn it on under Settings → Site features.', 'citolume' ); } if ( ! citolume_permalinks_support_llms() ) { $problems[] = __( 'Plain permalinks are active, so WordPress never routes /llms.txt to this plugin. Pick any other structure under Settings → Permalinks.', 'citolume' ); } if ( file_exists( ABSPATH . 'llms.txt' ) ) { $problems[] = __( 'A physical llms.txt file exists in the web root and wins over this generated one — on purpose, so nothing you placed there is silently overridden.', 'citolume' ); } // A missing tagline is NOT here any more. It is a content finding, not a // delivery one, and Review below says the same thing in the standard's // own words ("add a one-line summary as a blockquote"). Two bands saying // it from different angles taught the reader to skim both. return $problems; } /** * The platform's `reviewLlms()`, in PHP, run on the file we generate. * * Two different things used to be conflated and only one of them existed * here. `citolume_llms_attention()` above is about **delivery** — is the file * switched on, does the permalink structure route to it, is a physical file * shadowing it. This is about **content**: does the file say what the * standard asks for, so an AI tool can read it as intended. * * Run **locally**, not through the API. The rules are pure text analysis, the * plugin already holds the file it generated, and llms.txt is a free feature * that must keep working with the service completely absent. Sending the file * away to be read would break that for no gain. * * Kept rule-for-rule with the platform so the two surfaces cannot disagree * about the same file. If a rule changes there, change it here. * * @param string $content The file's content. * @param string $domain The site's host, for the foreign-link check. * @return array */ function citolume_review_llms( $content, $domain ) { $findings = array(); $lines = preg_split( '/\r?\n/', $content ); $first = ''; foreach ( $lines as $line ) { if ( '' !== trim( $line ) ) { $first = $line; break; } } if ( ! preg_match( '/^# \S/', $first ) ) { $findings[] = array( 'level' => 'problem', 'text' => __( 'The file should open with an H1 title ("# Your site name") — it is the first thing an AI reads.', 'citolume' ), ); } $has = static function ( $pattern ) use ( $lines ) { foreach ( $lines as $line ) { if ( preg_match( $pattern, $line ) ) { return true; } } return false; }; if ( ! $has( '/^> \S/' ) ) { $findings[] = array( 'level' => 'suggestion', 'text' => __( 'Add a one-line summary as a blockquote ("> what the site is") right under the title.', 'citolume' ), ); } if ( ! $has( '/^## \S/' ) ) { $findings[] = array( 'level' => 'problem', 'text' => __( 'No "## Section" headings. Group your important pages into sections — the lists are what AI tools actually read.', 'citolume' ), ); } preg_match_all( '/\[[^\]]*\]\(([^)\s]+)\)/', $content, $matches ); $links = (array) ( $matches[1] ?? array() ); if ( ! $links ) { $findings[] = array( 'level' => 'problem', 'text' => __( 'No linked pages. Each section should list pages as "- [Title](URL)".', 'citolume' ), ); } else { $relative = 0; $foreign = 0; // Host comparison, never substrings — the product's own rule. $own = strtolower( (string) preg_replace( '/:.*$/', '', $domain ) ); foreach ( $links as $url ) { if ( ! preg_match( '#^https?://#i', $url ) ) { ++$relative; continue; } $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); if ( '' !== $host && $host !== $own && substr( $host, -strlen( '.' . $own ) ) !== '.' . $own ) { ++$foreign; } } if ( $relative ) { $findings[] = array( 'level' => 'suggestion', 'text' => sprintf( /* translators: %d: number of relative links. */ _n( '%d link is relative — use absolute URLs so the file works wherever it is read from.', '%d links are relative — use absolute URLs so the file works wherever it is read from.', $relative, 'citolume' ), $relative ), ); } if ( $foreign ) { $findings[] = array( 'level' => 'suggestion', 'text' => sprintf( /* translators: %d: number of links pointing at other domains. */ _n( '%d link points at another domain. llms.txt is for your own pages — outbound links dilute it.', '%d links point at other domains. llms.txt is for your own pages — outbound links dilute it.', $foreign, 'citolume' ), $foreign ), ); } } if ( strlen( $content ) > 100 * 1024 ) { $findings[] = array( 'level' => 'suggestion', 'text' => __( 'The file is over 100KB. Shorter files are read whole; trim to the pages that matter.', 'citolume' ), ); } return $findings; } /** * Renders the llms.txt screen. * * @return void */ function citolume_render_llms_screen() { if ( ! current_user_can( 'manage_options' ) ) { return; } $status = '' !== citolume_license_key() ? citolume_site_status() : null; $body = citolume_get_llms_txt(); $generated = (int) get_option( 'citolume_llms_generated_at', 0 ); $context = (string) get_option( 'citolume_llms_context', '' ); $custom = (string) get_option( 'citolume_llms_custom', '' ); $problems = citolume_llms_attention(); $findings = citolume_review_llms( (string) $body, citolume_site_host() ); ?>

✓

✓

✓

true ) ); unset( $types['attachment'] ); add_meta_box( 'citolume-llms', __( 'llms.txt', 'citolume' ), 'citolume_render_llms_meta_box', array_values( $types ), 'side' ); } /** * Renders the meta box. * * @param WP_Post $post The post being edited. * @return void */ function citolume_render_llms_meta_box( $post ) { wp_nonce_field( 'citolume_llms_meta', 'citolume_llms_meta_nonce' ); $description = (string) get_post_meta( $post->ID, '_citolume_description', true ); $excluded = ! empty( get_post_meta( $post->ID, '_citolume_exclude', true ) ); ?>