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');
}
}