$vars Existing query vars. * @return array */ public function register_query_vars( array $vars ): array { $vars[] = 'modulustools_sitemap'; return $vars; } /** * Intercepts sitemap requests and virtual IndexNow key verification requests. * * @return void */ public function handle_sitemap_and_indexnow_requests(): void { $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; $path = trim( (string) wp_parse_url( $request_uri, PHP_URL_PATH ), '/' ); $indexnow_key = (string) get_option( 'modulustools_seo_indexnow_key', '' ); if ( '' !== $indexnow_key && $indexnow_key . '.txt' === $path ) { status_header( 200 ); header( 'Content-Type: text/plain; charset=utf-8' ); header( 'X-Robots-Tag: noindex' ); echo esc_html( $indexnow_key ); exit; } $sitemap_type = get_query_var( 'modulustools_sitemap' ); if ( '' === $sitemap_type ) { if ( 'sitemap_index.xml' === $path ) { $sitemap_type = 'index'; } elseif ( 'sitemap.xsl' === $path ) { $sitemap_type = 'xsl'; } elseif ( preg_match( '/^([a-z0-9_-]+)-sitemap\.xml$/', $path, $matches ) ) { $sitemap_type = sanitize_key( $matches[1] ); } } if ( '' === $sitemap_type ) { return; } if ( 'xsl' === $sitemap_type ) { $this->output_xsl_stylesheet(); } elseif ( 'index' === $sitemap_type ) { $this->output_sitemap_index(); } elseif ( 'news' === $sitemap_type ) { $this->output_news_sitemap(); } elseif ( 'video' === $sitemap_type ) { $this->output_video_sitemap(); } else { $this->output_child_sitemap( sanitize_key( $sitemap_type ) ); } } /** * Sends strict XML headers to prevent MIME-type errors. * * @return void */ private function send_xml_headers(): void { if ( ! headers_sent() ) { status_header( 200 ); header( 'Content-Type: application/xml; charset=utf-8' ); header( 'X-Robots-Tag: noindex, follow' ); } } /** * Outputs `/sitemap_index.xml` including News and Video sitemaps. * * @return void */ private function output_sitemap_index(): void { $this->send_xml_headers(); $xsl_url = home_url( '/sitemap.xsl' ); $now = gmdate( DATE_W3C ); echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; $groups = array( 'post', 'page', 'category', 'author', 'news', 'video' ); foreach ( $groups as $group ) { echo "\t\n"; echo "\t\t" . esc_url( home_url( '/' . $group . '-sitemap.xml' ) ) . "\n"; echo "\t\t" . esc_html( $now ) . "\n"; echo "\t\n"; } echo ''; exit; } /** * Outputs Google News compliant `/news-sitemap.xml`. * * @return void */ private function output_news_sitemap(): void { $this->send_xml_headers(); $site_name = get_bloginfo( 'name' ); $lang = substr( get_locale(), 0, 2 ); $recent_posts = get_posts( array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => 100, 'orderby' => 'date', 'order' => 'DESC', ) ); echo '' . "\n"; echo '' . "\n"; foreach ( $recent_posts as $post_item ) { echo "\t\n"; echo "\t\t" . esc_url( (string) get_permalink( $post_item->ID ) ) . "\n"; echo "\t\t\n"; echo "\t\t\t\n"; echo "\t\t\t\t" . esc_html( $site_name ) . "\n"; echo "\t\t\t\t" . esc_html( $lang ? $lang : 'en' ) . "\n"; echo "\t\t\t\n"; echo "\t\t\t" . esc_html( get_the_date( DATE_W3C, $post_item ) ) . "\n"; echo "\t\t\t" . esc_html( get_the_title( $post_item->ID ) ) . "\n"; echo "\t\t\n"; echo "\t\n"; } echo ''; exit; } /** * Outputs `/video-sitemap.xml` by automatically detecting YouTube embeds in posts. * * @return void */ private function output_video_sitemap(): void { $this->send_xml_headers(); $posts = get_posts( array( 'post_type' => array( 'post', 'page' ), 'post_status' => 'publish', 'posts_per_page' => 100, ) ); echo '' . "\n"; echo '' . "\n"; foreach ( $posts as $p ) { if ( preg_match( '/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i', $p->post_content, $yt_match ) ) { $video_id = $yt_match[1]; $player_loc = 'https://www.youtube.com/embed/' . $video_id; $thumb_loc = 'https://i.ytimg.com/vi/' . $video_id . '/hqdefault.jpg'; $desc = wp_trim_words( wp_strip_all_tags( $p->post_content ), 25 ); echo "\t\n"; echo "\t\t" . esc_url( (string) get_permalink( $p->ID ) ) . "\n"; echo "\t\t\n"; echo "\t\t\t" . esc_url( $thumb_loc ) . "\n"; echo "\t\t\t" . esc_html( get_the_title( $p->ID ) ) . "\n"; echo "\t\t\t" . esc_html( $desc ? $desc : get_the_title( $p->ID ) ) . "\n"; echo "\t\t\t" . esc_url( $player_loc ) . "\n"; echo "\t\t\n"; echo "\t\n"; } } echo ''; exit; } /** * Outputs child XML sitemaps (`post`, `page`, `category`, `author`) excluding `noindex` items. * * @param string $type Child sitemap type. * @return void */ private function output_child_sitemap( string $type ): void { global $wpdb; $this->send_xml_headers(); $xsl_url = home_url( '/sitemap.xsl' ); echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; if ( in_array( $type, array( 'post', 'page' ), true ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $posts = $wpdb->get_results( $wpdb->prepare( 'SELECT p.ID, p.post_modified_gmt FROM %i p LEFT JOIN %i m ON (p.ID = m.object_id AND m.object_type = %s) WHERE p.post_type = %s AND p.post_status = %s AND p.post_password = %s AND (m.robots_index IS NULL OR m.robots_index != %s) ORDER BY p.post_modified_gmt DESC LIMIT 1000', $wpdb->posts, $wpdb->prefix . 'modulustools_meta', 'post', $type, 'publish', '', 'noindex' ) ); if ( is_array( $posts ) ) { foreach ( $posts as $p ) { $loc = get_permalink( (int) $p->ID ); $lastmod = ! empty( $p->post_modified_gmt ) && '0000-00-00 00:00:00' !== $p->post_modified_gmt ? gmdate( DATE_W3C, strtotime( $p->post_modified_gmt ) ) : gmdate( DATE_W3C ); echo "\t\n"; echo "\t\t" . esc_url( (string) $loc ) . "\n"; echo "\t\t" . esc_html( $lastmod ) . "\n"; echo "\t\n"; } } } elseif ( 'category' === $type ) { $terms = get_terms( array( 'taxonomy' => 'category', 'hide_empty' => true, ) ); if ( ! is_wp_error( $terms ) && is_array( $terms ) ) { foreach ( $terms as $term ) { $link = get_term_link( $term ); if ( ! is_wp_error( $link ) ) { echo "\t\n"; echo "\t\t" . esc_url( $link ) . "\n"; echo "\t\n"; } } } } elseif ( 'author' === $type ) { $users = get_users( array( 'has_published_posts' => array( 'post' ), 'number' => 200, ) ); foreach ( $users as $user ) { echo "\t\n"; echo "\t\t" . esc_url( get_author_posts_url( $user->ID ) ) . "\n"; echo "\t\n"; } } echo ''; exit; } /** * Outputs the `/sitemap.xsl` stylesheet using inline style attributes (zero external stylesheet tags). * * @return void */ private function output_xsl_stylesheet(): void { $this->send_xml_headers(); echo ''; ?> XML Sitemap — ModulusTools SEO Suite

ModulusTools SEO Suite — XML Sitemap

Dynamic XML Sitemap generated from custom indexed tables.

Sitemap Location Last Modified
Canonical URL Last Modified (UTC)
ID ) ) { return; } delete_transient( 'modulustools_seo_sitemap_cache' ); delete_transient( 'modulustools_seo_llms_txt_cache' ); delete_transient( 'modulustools_seo_llms_full_cache' ); $settings = get_option( 'modulustools_seo_settings', array() ); if ( empty( $settings['enable_indexnow'] ) ) { return; } $permalink = get_permalink( $post->ID ); $key = (string) get_option( 'modulustools_seo_indexnow_key', '' ); if ( ! $permalink || '' === $key ) { return; } $host = (string) wp_parse_url( home_url(), PHP_URL_HOST ); $payload = array( 'host' => $host, 'key' => $key, 'keyLocation' => home_url( '/' . $key . '.txt' ), 'urlList' => array( $permalink ), ); wp_remote_post( 'https://api.indexnow.org/indexnow', array( 'blocking' => false, 'timeout' => 5, 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), 'body' => (string) wp_json_encode( $payload ), ) ); if ( ! empty( $settings['indexnow_webhook_url'] ) ) { wp_remote_post( esc_url_raw( $settings['indexnow_webhook_url'] ), array( 'blocking' => false, 'timeout' => 5, 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), 'body' => (string) wp_json_encode( array( 'event' => 'publish' === $old_status ? 'post_updated' : 'post_published', 'post_id' => $post->ID, 'url' => $permalink, 'timestamp' => current_time( 'mysql', true ), ) ), ) ); } } /** * Renders `[modulustools_html_sitemap]` shortcode for visitors and crawlers. * * @return string */ public function render_html_sitemap_shortcode(): string { $pages = get_pages( array( 'post_status' => 'publish' ) ); $posts = get_posts( array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => 100, ) ); ob_start(); ?>