set_404();
status_header( 404 );
nocache_headers();
include get_404_template();
exit;
}
private static function send( $body, $type ) {
status_header( 200 );
header( 'Content-Type: ' . $type );
header( 'X-Robots-Tag: noindex, follow', true );
header( 'Cache-Control: public, max-age=900' );
echo $body; // phpcs:ignore WordPress.Security.EscapeOutput
exit;
}
private static function cached( $key, $builder ) {
$k = 'sxseo_' . substr( md5( $key . '|' . get_option( 'sxseo_cache_ver', '0' ) ), 0, 20 );
$v = get_transient( $k );
if ( false === $v ) {
$v = call_user_func( $builder );
set_transient( $k, $v, 6 * HOUR_IN_SECONDS );
}
return $v;
}
// ------------------------------------------------------------------- queries
private static function base_args( $type ) {
return array(
'post_type' => $type,
'post_status' => 'publish',
'has_password' => false,
'no_found_rows' => true,
'update_post_term_cache' => false,
'meta_query' => array(
'relation' => 'AND',
array(
'relation' => 'OR',
array( 'key' => '_sxseo_noindex', 'compare' => 'NOT EXISTS' ),
array( 'key' => '_sxseo_noindex', 'value' => '1', 'compare' => '!=' ),
),
array(
'relation' => 'OR',
array( 'key' => '_sxseo_exclude_sitemap', 'compare' => 'NOT EXISTS' ),
array( 'key' => '_sxseo_exclude_sitemap', 'value' => '1', 'compare' => '!=' ),
),
),
);
}
private static function count_posts( $type ) {
$q = new WP_Query( array_merge( self::base_args( $type ), array( 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => false ) ) );
return (int) $q->found_posts;
}
private static function terms_for( $tax ) {
$terms = get_terms( array( 'taxonomy' => $tax, 'hide_empty' => true, 'number' => 0 ) );
if ( is_wp_error( $terms ) ) {
return array();
}
return array_values( array_filter( $terms, function ( $t ) {
return sxseo_term_indexable( $t ) && 'uncategorized' !== $t->slug;
} ) );
}
// -------------------------------------------------------------------- builders
public static function build_index() {
$x = '' . "\n" . '' . "\n";
$latest = gmdate( 'c' );
foreach ( sxseo_post_types() as $type ) {
$n = self::count_posts( $type );
if ( 0 === $n && ! ( 'page' === $type ) ) {
continue;
}
$pages = max( 1, (int) ceil( $n / self::PER_PAGE ) );
for ( $p = 1; $p <= $pages; $p++ ) {
$x .= '' . sxseo_x( home_url( "/sitemap-p-{$type}-{$p}.xml" ) ) . '' . $latest . '' . "\n";
}
}
foreach ( sxseo_taxonomies() as $tax ) {
if ( self::terms_for( $tax ) ) {
$x .= '' . sxseo_x( home_url( "/sitemap-t-{$tax}-1.xml" ) ) . '' . $latest . '' . "\n";
}
}
return $x . '';
}
public static function build_map( $kind, $name, $page ) {
$head = '' . "\n" . '' . "\n";
$rows = '';
if ( 'p' === $kind && in_array( $name, sxseo_post_types(), true ) ) {
if ( 1 === $page && ( 'page' === $name ) && 'page' !== get_option( 'show_on_front' ) ) {
$rows .= '' . sxseo_x( home_url( '/' ) ) . 'daily1.0' . "\n";
}
$q = new WP_Query( array_merge( self::base_args( $name ), array( 'posts_per_page' => self::PER_PAGE, 'paged' => $page, 'orderby' => 'modified', 'order' => 'DESC' ) ) );
$front = (int) get_option( 'page_on_front' );
foreach ( $q->posts as $post ) {
$loc = get_permalink( $post );
$prio = $post->ID === $front ? '1.0' : ( 'page' === $post->post_type ? '0.8' : '0.6' );
$rows .= '' . sxseo_x( $loc ) . '' . sxseo_w3c( $post->post_modified_gmt ) . '' . ( $post->ID === $front ? 'daily' : 'weekly' ) . '' . $prio . '';
if ( sxseo_opt( 'sitemap_images' ) ) {
foreach ( self::images_for( $post ) as $img ) {
$rows .= '' . sxseo_x( $img['url'] ) . '' . ( $img['title'] ? '' . sxseo_x( $img['title'] ) . '' : '' ) . '';
}
}
$rows .= "\n";
}
if ( ! $q->posts && 1 !== $page ) {
return '';
}
} elseif ( 't' === $kind && in_array( $name, sxseo_taxonomies(), true ) ) {
foreach ( self::terms_for( $name ) as $t ) {
$link = get_term_link( $t );
if ( ! is_wp_error( $link ) ) {
$rows .= '' . sxseo_x( $link ) . 'weekly0.4' . "\n";
}
}
} else {
return '';
}
return $head . $rows . '';
}
private static function images_for( WP_Post $post ) {
$imgs = array();
$thumb = get_post_thumbnail_id( $post );
if ( $thumb ) {
$u = wp_get_attachment_image_url( $thumb, 'full' );
if ( $u ) {
$imgs[ $u ] = array( 'url' => $u, 'title' => sxseo_title_text( $thumb ) );
}
}
if ( preg_match_all( '/
]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $m ) ) {
foreach ( array_slice( $m[1], 0, 8 ) as $src ) {
if ( 0 === strpos( $src, '//' ) ) {
$src = 'https:' . $src;
}
if ( 0 === strpos( $src, '/' ) ) {
$src = home_url( $src );
}
if ( 0 === strpos( $src, 'http' ) && ! isset( $imgs[ $src ] ) ) {
$imgs[ $src ] = array( 'url' => $src, 'title' => '' );
}
}
}
return array_slice( array_values( $imgs ), 0, 10 );
}
/** llms.txt: a plain-language map of the site for AI assistants and answer engines. */
public static function build_llms() {
$name = get_bloginfo( 'name' );
$out = "# {$name}\n\n";
$desc = (string) sxseo_opt( 'home_description' ) ?: get_bloginfo( 'description' );
if ( $desc ) {
$out .= '> ' . $desc . "\n\n";
}
$o = SXSEO_Settings::all();
$bits = array_filter( array( $o['org_phone'], $o['org_email'], trim( implode( ', ', array_filter( array( $o['org_street'], $o['org_city'], $o['org_postcode'] ) ) ) ) ) );
if ( $bits ) {
$out .= 'Contact: ' . implode( ' | ', $bits ) . "\n\n";
}
$pages = get_posts( array( 'post_type' => 'page', 'post_status' => 'publish', 'numberposts' => 50, 'orderby' => 'menu_order title', 'order' => 'ASC', 'has_password' => false ) );
if ( $pages ) {
$out .= "## Pages\n\n";
foreach ( $pages as $p ) {
if ( '1' === get_post_meta( $p->ID, '_sxseo_noindex', true ) ) {
continue;
}
$d = SXSEO_Context::for_post( $p );
$out .= '- [' . sxseo_title_text( $p ) . '](' . get_permalink( $p ) . '): ' . $d['desc'] . "\n";
}
$out .= "\n";
}
foreach ( array_diff( sxseo_post_types(), array( 'page' ) ) as $type ) {
$posts = get_posts( array( 'post_type' => $type, 'post_status' => 'publish', 'numberposts' => 40, 'has_password' => false ) );
if ( ! $posts ) {
continue;
}
$obj = get_post_type_object( $type );
$out .= '## ' . ( $obj ? $obj->labels->name : $type ) . "\n\n";
foreach ( $posts as $p ) {
if ( '1' === get_post_meta( $p->ID, '_sxseo_noindex', true ) ) {
continue;
}
$d = SXSEO_Context::for_post( $p );
$out .= '- [' . sxseo_title_text( $p ) . '](' . get_permalink( $p ) . '): ' . $d['desc'] . "\n";
}
$out .= "\n";
}
$out .= "## Sitemap\n\n- " . home_url( '/sitemap.xml' ) . "\n";
return $out;
}
// --------------------------------------------------------------------- robots
public static function robots_txt( $output, $public ) {
if ( ! $public ) {
return $output;
}
if ( sxseo_opt( 'robots_txt_extras' ) ) {
$output = rtrim( $output ) . "\n";
$output .= "Disallow: /?s=\nDisallow: /search/\nDisallow: /*?replytocom=\nDisallow: /*?add-to-cart=\n";
}
if ( 'block_training' === sxseo_opt( 'ai_policy' ) ) {
foreach ( array( 'GPTBot', 'CCBot', 'Google-Extended', 'anthropic-ai', 'ClaudeBot', 'Bytespider', 'Applebot-Extended', 'meta-externalagent' ) as $bot ) {
$output .= "\nUser-agent: {$bot}\nDisallow: /\n";
}
}
if ( sxseo_opt( 'sitemap' ) ) {
$output .= "\nSitemap: " . home_url( '/sitemap.xml' ) . "\n";
}
return $output;
}
// ------------------------------------------------------------------- IndexNow
public static function on_transition( $new, $old, $post ) {
if ( ! sxseo_opt( 'indexnow' ) || ! get_option( 'blog_public' ) || wp_is_post_revision( $post ) ) {
return;
}
if ( ! in_array( $post->post_type, sxseo_post_types(), true ) ) {
return;
}
if ( 'publish' !== $new && 'publish' !== $old ) {
return;
}
if ( 'publish' === $new && '1' === get_post_meta( $post->ID, '_sxseo_noindex', true ) ) {
return;
}
self::submit( array( get_permalink( $post ) ) );
}
public static function submit( array $urls ) {
$host = sxseo_home_host();
$key = (string) sxseo_opt( 'indexnow_key' );
if ( '' === $key || ! $host || in_array( $host, array( 'localhost', '127.0.0.1' ), true ) || substr( $host, -6 ) === '.local' ) {
return;
}
$fresh = array();
foreach ( $urls as $u ) {
$t = 'sxseo_in_' . md5( $u );
if ( false === get_transient( $t ) ) {
set_transient( $t, 1, 10 * MINUTE_IN_SECONDS );
$fresh[] = $u;
}
}
if ( ! $fresh ) {
return;
}
wp_remote_post(
'https://api.indexnow.org/indexnow',
array(
'blocking' => false,
'timeout' => 3,
'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ),
'body' => wp_json_encode( array( 'host' => $host, 'key' => $key, 'keyLocation' => home_url( '/' . $key . '.txt' ), 'urlList' => $fresh ) ),
)
);
}
}