load_settings(); if ( Rayetun_AG_Modules::is_enabled( 'markdown_agents' ) ) { // Serve early, before the template loads. add_action( 'template_redirect', array( $this, 'maybe_serve_markdown' ), 1 ); // Vary + Link discovery headers on the HTML response. add_action( 'send_headers', array( $this, 'add_response_headers' ) ); // Discovery in
. add_action( 'wp_head', array( $this, 'print_discovery_link' ), 1 ); // Bust the cached conversion when content changes. add_action( 'save_post', array( $this, 'clear_cache' ) ); } add_action( 'wp_ajax_rayetun_ag_save_markdown_settings', array( $this, 'handle_save_settings' ) ); add_action( 'wp_ajax_rayetun_ag_preview_markdown', array( $this, 'handle_preview' ) ); } private function load_settings() { $this->settings = wp_parse_args( get_option( 'rayetun_ag_markdown_settings', array() ), array( 'post_types' => array( 'post', 'page' ), 'frontmatter' => true, 'token_header' => true, 'discovery' => true, ) ); } public function get_settings() { return $this->settings; } // ------------------------------------------------------------------------- // Request handling // ------------------------------------------------------------------------- private function is_enabled_post_type( $post ) { $types = ! empty( $this->settings['post_types'] ) ? (array) $this->settings['post_types'] : array( 'post', 'page' ); return in_array( $post->post_type, $types, true ); } private function is_excluded( $post ) { // Reuse the llms.txt per-post exclude flag — one switch, no duplicate UI. return (bool) get_post_meta( $post->ID, '_rayetun_ag_exclude_llms', true ); } private function wants_markdown() { // Explicit, bookmarkable, cache-friendly discovery URL (?rayetun_ag_md=1). if ( isset( $_GET['rayetun_ag_md'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- public read-only content negotiation, no state change return true; } // Standard content negotiation — agents (incl. Claude Code) send this. $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) : ''; return false !== stripos( $accept, 'text/markdown' ); } private function discovery_url( $post ) { return add_query_arg( 'rayetun_ag_md', '1', get_permalink( $post ) ); } public function maybe_serve_markdown() { if ( is_admin() || ! is_singular() || ! $this->wants_markdown() ) { return; } $post = get_queried_object(); if ( ! ( $post instanceof WP_Post ) || 'publish' !== $post->post_status ) { return; } if ( ! $this->is_enabled_post_type( $post ) || $this->is_excluded( $post ) ) { return; } $markdown = $this->build_markdown( $post ); status_header( 200 ); nocache_headers(); header( 'Content-Type: text/markdown; charset=utf-8' ); header( 'Vary: Accept', false ); header( 'X-Robots-Tag: noindex' ); if ( ! empty( $this->settings['token_header'] ) ) { header( 'X-Markdown-Tokens: ' . (int) ceil( strlen( $markdown ) / 4 ) ); } echo $markdown; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain-text markdown output exit; } public function add_response_headers() { if ( is_admin() ) { return; } $post = get_queried_object(); if ( ! ( $post instanceof WP_Post ) || ! $this->is_enabled_post_type( $post ) || $this->is_excluded( $post ) ) { return; } // Tell caches the response varies by Accept so agents/humans don't get crossed. header( 'Vary: Accept', false ); if ( ! empty( $this->settings['discovery'] ) ) { header( 'Link: <' . esc_url_raw( $this->discovery_url( $post ) ) . '>; rel="alternate"; type="text/markdown"', false ); } } public function print_discovery_link() { if ( empty( $this->settings['discovery'] ) || ! is_singular() ) { return; } $post = get_queried_object(); if ( ! ( $post instanceof WP_Post ) || ! $this->is_enabled_post_type( $post ) || $this->is_excluded( $post ) ) { return; } printf( '' . "\n", esc_url( $this->discovery_url( $post ) ) ); } // ------------------------------------------------------------------------- // Markdown building // ------------------------------------------------------------------------- public function build_markdown( $post ) { $body = $this->get_markdown_body( $post ); if ( empty( $this->settings['frontmatter'] ) ) { return rtrim( $body ) . "\n"; } return $this->build_frontmatter( $post ) . "\n\n" . rtrim( $body ) . "\n"; } private function get_markdown_body( $post ) { $key = 'rayetun_ag_md_' . $post->ID; $cached = get_transient( $key ); if ( false !== $cached ) { return $cached; } // Apply WordPress core's the_content filter so blocks and shortcodes render // exactly as they do on the front end. This is core's filter, not our own. $html = apply_filters( 'the_content', $post->post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound $body = $this->html_to_markdown( $html ); set_transient( $key, $body, DAY_IN_SECONDS ); return $body; } public function clear_cache( $post_id ) { delete_transient( 'rayetun_ag_md_' . absint( $post_id ) ); } private function build_frontmatter( $post ) { $lines = array( '---' ); $lines[] = 'title: ' . $this->yaml_value( wp_strip_all_tags( $post->post_title ) ); $lines[] = 'url: ' . get_permalink( $post ); $lines[] = 'date: ' . get_the_date( 'c', $post ); $lines[] = 'modified: ' . get_the_modified_date( 'c', $post ); $author = get_the_author_meta( 'display_name', $post->post_author ); if ( $author ) { $lines[] = 'author: ' . $this->yaml_value( $author ); } if ( has_post_thumbnail( $post ) ) { $lines[] = 'image: ' . get_the_post_thumbnail_url( $post, 'full' ); } $cats = wp_get_post_terms( $post->ID, 'category', array( 'fields' => 'names' ) ); if ( $cats && ! is_wp_error( $cats ) ) { $lines[] = 'categories: [' . implode( ', ', array_map( array( $this, 'yaml_value' ), $cats ) ) . ']'; } $tags = wp_get_post_terms( $post->ID, 'post_tag', array( 'fields' => 'names' ) ); if ( $tags && ! is_wp_error( $tags ) ) { $lines[] = 'tags: [' . implode( ', ', array_map( array( $this, 'yaml_value' ), $tags ) ) . ']'; } $lines[] = '---'; return implode( "\n", $lines ); } private function yaml_value( $value ) { $value = (string) $value; if ( preg_match( '/[:#\[\]\{\}",\n]/', $value ) ) { return '"' . str_replace( '"', '\"', $value ) . '"'; } return $value; } // ------------------------------------------------------------------------- // Dependency-free HTML → Markdown converter // ------------------------------------------------------------------------- private function html_to_markdown( $html ) { $html = trim( (string) $html ); if ( '' === $html ) { return ''; } if ( ! class_exists( 'DOMDocument' ) ) { return trim( wp_strip_all_tags( $html ) ); // Graceful fallback. } $dom = new DOMDocument( '1.0', 'UTF-8' ); libxml_use_internal_errors( true ); // The XML PI forces UTF-8 parsing; the wrapper div gives us a single root. $dom->loadHTML( '