.md
* - Whole site: /llms-full.txt (every page's markdown concatenated)
*
* The markdown is built from the captured analysis (llm_context_block,
* summaries, content_sections, approved entities with sameAs, FAQs) — a view
* purpose-built for LLM retrieval, not a naive HTML→MD dump. Falls back to the
* raw post content when a page hasn't been analysed.
*
* Also advertises each page's .md via .
*/
class Semantic_Markdown {
public static function init() {
add_action( 'init', [ __CLASS__, 'rewrite_rules' ] );
add_filter( 'query_vars', [ __CLASS__, 'query_vars' ] );
add_action( 'template_redirect', [ __CLASS__, 'serve' ] );
add_action( 'wp_head', [ __CLASS__, 'alternate_link' ], 6 );
}
public static function rewrite_rules() {
// .md → markdown for that page
add_rewrite_rule( '^(.+)\.md$', 'index.php?semantic_md_path=$matches[1]', 'top' );
// /llms-full.txt → full-corpus markdown
add_rewrite_rule( '^llms-full\.txt$', 'index.php?semantic_llms_full=1', 'top' );
}
public static function query_vars( $vars ) {
$vars[] = 'semantic_md_path';
$vars[] = 'semantic_llms_full';
return $vars;
}
public static function serve() {
if ( ! Semantic_Settings::markdown_enabled() ) return;
// Whole-site corpus
if ( get_query_var( 'semantic_llms_full' ) ) {
header( 'Content-Type: text/markdown; charset=utf-8' );
header( 'X-Robots-Tag: noindex' );
header( 'X-Content-Type-Options: nosniff' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text markdown response (Content-Type: text/markdown sent above); HTML escaping would corrupt the output.
echo self::full_corpus();
exit;
}
// Per-page markdown
$path = get_query_var( 'semantic_md_path' );
if ( $path === '' || $path === null ) return;
$path = trim( $path, '/' );
$post_id = url_to_postid( home_url( '/' . $path . '/' ) );
if ( ! $post_id ) $post_id = url_to_postid( home_url( '/' . $path ) );
if ( ! $post_id ) {
status_header( 404 );
header( 'Content-Type: text/markdown; charset=utf-8' );
echo "# Not found\n\nNo page matches this URL.\n";
exit;
}
$post = get_post( $post_id );
// Same public-content gate as every other public surface: published, public,
// password-free, non-excluded, publicly-queryable type. Anything else 404s.
if ( ! $post || ! Semantic_Settings::is_publicly_servable( $post ) ) {
status_header( 404 );
header( 'Content-Type: text/markdown; charset=utf-8' );
echo "# Not found\n";
exit;
}
header( 'Content-Type: text/markdown; charset=utf-8' );
header( 'X-Robots-Tag: noindex' );
header( 'X-Content-Type-Options: nosniff' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text markdown response (Content-Type: text/markdown sent above); HTML escaping would corrupt the output.
echo self::render_post( $post );
exit;
}
/** Advertise the .md alternate on singular pages so agents can discover it. */
public static function alternate_link() {
if ( ! Semantic_Settings::markdown_enabled() ) return;
if ( ! is_singular() ) return;
$post = get_queried_object();
if ( ! $post instanceof WP_Post ) return;
// Only advertise .md where we'd actually serve it (skips password-protected,
// excluded and non-public posts, matching serve()).
if ( ! Semantic_Settings::is_publicly_servable( $post ) ) return;
$md = self::md_url( $post );
if ( $md ) {
echo '' . "\n";
}
}
/** Build the .md URL for a post from its permalink. */
public static function md_url( WP_Post $post ) {
$url = get_permalink( $post );
if ( ! $url ) return '';
// Insert .md before any trailing slash / query.
$parts = wp_parse_url( $url );
$path = isset( $parts['path'] ) ? rtrim( $parts['path'], '/' ) : '';
if ( $path === '' ) return '';
$base = ( $parts['scheme'] ?? 'https' ) . '://' . ( $parts['host'] ?? '' );
return $base . $path . '.md';
}
// -------------------------------------------------------------------------
// Rendering
// -------------------------------------------------------------------------
/** Render one post as AI-optimised markdown. */
public static function render_post( WP_Post $post ) {
global $wpdb;
$url = get_permalink( $post );
$analysis = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}semantic_analyses
WHERE post_id = %d AND status = 'completed'
ORDER BY finished_at DESC LIMIT 1",
$post->ID
), ARRAY_A );
$lines = [];
$lines[] = '# ' . wp_strip_all_tags( get_the_title( $post ) );
$lines[] = '';
$desc = $analysis['llm_description'] ?? '';
if ( $desc ) { $lines[] = '> ' . $desc; $lines[] = ''; }
$lines[] = '**Source:** ' . $url;
$lines[] = '**Updated:** ' . get_post_modified_time( 'Y-m-d', true, $post );
$lines[] = '';
// No analysis yet → fall back to a clean text rendering of the content.
if ( ! $analysis ) {
$lines[] = self::content_to_markdown( $post );
return implode( "\n", $lines ) . "\n";
}
// Summary
$summary = $analysis['summary'] ?: '';
if ( $summary ) {
$lines[] = '## Summary';
$lines[] = '';
$lines[] = $summary;
$lines[] = '';
}
// LLM context block — the self-contained retrieval paragraph.
if ( ! empty( $analysis['llm_context_block'] ) ) {
$lines[] = '## Overview';
$lines[] = '';
$lines[] = $analysis['llm_context_block'];
$lines[] = '';
}
// Key entities (approved, with grounding links)
$entities = $wpdb->get_results( $wpdb->prepare(
"SELECT name, type, definition, canonical_entity_id, wikidata_qid, same_as
FROM {$wpdb->prefix}semantic_suggested_entities
WHERE analysis_id = %d AND approval_status = 'approved'
ORDER BY relevance_score DESC LIMIT 25",
$analysis['id']
), ARRAY_A );
if ( $entities ) {
$lines[] = '## Key entities';
$lines[] = '';
foreach ( $entities as $e ) {
$same = json_decode( $e['same_as'] ?? '[]', true ) ?: [];
$link = $same[0] ?? ( $e['canonical_entity_id'] ? home_url( '/entities/' . $e['canonical_entity_id'] ) : '' );
$name = $link ? '[' . $e['name'] . '](' . $link . ')' : $e['name'];
$row = '- ' . $name;
if ( ! empty( $e['type'] ) ) $row .= ' _(' . str_replace( '_', ' ', $e['type'] ) . ')_';
if ( ! empty( $e['definition'] ) ) $row .= ' — ' . $e['definition'];
if ( ! empty( $e['wikidata_qid'] ) ) $row .= ' (Wikidata: ' . $e['wikidata_qid'] . ')';
$lines[] = $row;
}
$lines[] = '';
}
// Section outline
$sections = json_decode( $analysis['content_sections'] ?? '[]', true ) ?: [];
if ( $sections ) {
$lines[] = '## Contents';
$lines[] = '';
foreach ( $sections as $sec ) {
if ( empty( $sec['heading'] ) ) continue;
$lines[] = '### ' . wp_strip_all_tags( $sec['heading'] );
if ( ! empty( $sec['summary'] ) ) { $lines[] = ''; $lines[] = $sec['summary']; }
$lines[] = '';
}
}
// FAQs
$faqs = $wpdb->get_results( $wpdb->prepare(
"SELECT question, answer FROM {$wpdb->prefix}semantic_suggested_faqs
WHERE analysis_id = %d AND approval_status = 'approved'
ORDER BY ai_confidence DESC",
$analysis['id']
), ARRAY_A );
if ( $faqs ) {
$lines[] = '## FAQ';
$lines[] = '';
foreach ( $faqs as $f ) {
$lines[] = '**' . wp_strip_all_tags( $f['question'] ) . '**';
$lines[] = '';
$lines[] = wp_strip_all_tags( $f['answer'] );
$lines[] = '';
}
}
// Citable snippet
if ( ! empty( $analysis['citable_snippet'] ) ) {
$lines[] = '---';
$lines[] = '';
$lines[] = $analysis['citable_snippet'];
$lines[] = '';
}
return implode( "\n", $lines ) . "\n";
}
/** Whole-site corpus: every published page's markdown, concatenated. */
public static function full_corpus() {
$name = Semantic_Settings::org_name();
$out = [ '# ' . $name . ' — Full Content Corpus', '' ];
$desc = Semantic_Settings::org_description();
if ( $desc ) { $out[] = '> ' . $desc; $out[] = ''; }
$types = Semantic_Settings::public_post_types();
$posts = empty( $types ) ? [] : get_posts( [
'post_type' => $types,
'post_status' => 'publish',
'has_password' => false,
'posts_per_page' => 500,
'orderby' => 'menu_order title',
'order' => 'ASC',
'meta_query' => [ [ 'key' => Semantic_Settings::EXCLUDE_META, 'compare' => 'NOT EXISTS' ] ],
] );
foreach ( $posts as $post ) {
$out[] = self::render_post( $post );
$out[] = "\n---\n";
}
return implode( "\n", $out ) . "\n";
}
/** Lightweight HTML→Markdown for un-analysed pages (headings, links, lists). */
private static function content_to_markdown( WP_Post $post ) {
$html = apply_filters( 'the_content', $post->post_content );
$html = preg_replace( '/]*>(.*?)<\/h\1>/is', "\n" . '### $2' . "\n", $html );
$html = preg_replace( '/]*>(.*?)<\/li>/is', '- $1' . "\n", $html );
$html = preg_replace( '/]*href="([^"]*)"[^>]*>(.*?)<\/a>/is', '[$2]($1)', $html );
$text = wp_strip_all_tags( $html );
$text = preg_replace( "/\n{3,}/", "\n\n", $text );
return trim( $text );
}
}