send_xml($cached); exit; } $xml = ''; switch ($type) { case 'index': $xml = $this->generate_index(); break; case 'posts': $xml = $this->generate_posts_sitemap('post'); break; case 'pages': $xml = $this->generate_posts_sitemap('page'); break; case 'taxonomies': $xml = $this->generate_taxonomies_sitemap(); break; default: global $wp_query; $wp_query->set_404(); status_header(404); return; } set_transient($cache_key, $xml, self::CACHE_TIME); $this->send_xml($xml); exit; } private function send_xml($content) { status_header(200); header('Content-Type: application/xml; charset=utf-8'); header('X-Robots-Tag: noindex, follow'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML output is generated and safe. echo $content; } private function generate_index() { $base_url = home_url('/'); $sitemaps = [ $base_url . 'sitemap-posts.xml', $base_url . 'sitemap-pages.xml', // Taxonomies optional or if enabled ]; // Only add taxonomies request if there are public taxonomies // For simplicity in Phase 4, we just add it ensuring it returns empty set if none. $sitemaps[] = $base_url . 'sitemap-taxonomies.xml'; $last_mod = wp_date('c'); // Use current time or query latest post $xml = ''; $xml .= ''; foreach ($sitemaps as $url) { $xml .= ''; $xml .= '' . esc_url($url) . ''; $xml .= '' . $last_mod . ''; $xml .= ''; } $xml .= ''; return $xml; } private function generate_posts_sitemap($post_type) { // Query posts $query = new \WP_Query([ 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => 1000, // Limit for Phase 4 safety 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ]); $xml = ''; $xml .= ''; foreach ($query->posts as $post_id) { if ($this->is_excluded($post_id)) { continue; } $permalink = get_permalink($post_id); $modified = get_the_modified_date('c', $post_id); $xml .= ''; $xml .= '' . esc_url($permalink) . ''; $xml .= '' . $modified . ''; $xml .= ''; } $xml .= ''; return $xml; } private function generate_taxonomies_sitemap() { // Basic impl for categories/tags $taxonomies = get_taxonomies(['public' => true], 'names'); $terms = get_terms([ 'taxonomy' => $taxonomies, 'hide_empty' => true, ]); $xml = ''; $xml .= ''; if (!is_wp_error($terms)) { foreach ($terms as $term) { // Todo: check for noindex on term meta if we added that feature $link = get_term_link($term); if (is_wp_error($link)) continue; $xml .= ''; $xml .= '' . esc_url($link) . ''; $xml .= ''; } } $xml .= ''; return $xml; } private function is_excluded($post_id) { $meta = Helpers::get_post_meta($post_id); // 1. Check noindex $robots = !empty($meta['robots']) ? $meta['robots'] : 'index, follow'; if (strpos($robots, 'noindex') !== false) { return true; } // 2. Check password protected? (WP Query handles post_status=publish, but password protected are publish) $post = get_post($post_id); if (!empty($post->post_password)) { return true; } return false; } public function invalidate_cache() { delete_transient(self::TRANSIENT_KEY_PREFIX . 'index'); delete_transient(self::TRANSIENT_KEY_PREFIX . 'posts'); delete_transient(self::TRANSIENT_KEY_PREFIX . 'pages'); delete_transient(self::TRANSIENT_KEY_PREFIX . 'taxonomies'); } }