isEnabled()) { $this->addRewriteRules(); add_filter('query_vars', [$this, 'registerQueryVars']); add_action('template_redirect', [$this, 'maybeRender'], 0); // Core ships its own sitemap at /wp-sitemap.xml. Two sitemaps disagreeing // about what to crawl helps nobody, so ours replaces it. add_filter('wp_sitemaps_enabled', '__return_false'); } add_filter('robots_txt', [$this, 'addSitemapReference'], 10, 2); // Rewrite rules live in the database, so any setting that changes which // routes exist has to trigger a flush on the next request. add_action('rankify_settings_update:after', [$this, 'scheduleFlush']); add_action('init', [$this, 'maybeFlush'], 99); } protected function isEnabled() { return 'on' === Utils::getSetting('xmlSitemap'); } protected function addRewriteRules() { add_rewrite_rule('^sitemap\.xml$', 'index.php?rankify_sitemap=index', 'top'); add_rewrite_rule('^sitemap\.xsl$', 'index.php?rankify_sitemap=stylesheet', 'top'); // The subtype pattern is greedy so it matches the *last* "-" in the // name. That matters because a taxonomy slug may itself contain a hyphen — // "sitemap-tax-product-cat-2.xml" has to split as (tax-product-cat, 2). add_rewrite_rule('^sitemap-(.+)-([0-9]+)\.xml$', 'index.php?rankify_sitemap=urlset&rankify_sitemap_subtype=$matches[1]&rankify_sitemap_page=$matches[2]', 'top'); } public function registerQueryVars($vars) { $vars[] = 'rankify_sitemap'; $vars[] = 'rankify_sitemap_subtype'; $vars[] = 'rankify_sitemap_page'; return $vars; } public function maybeRender() { $request = get_query_var('rankify_sitemap'); if (empty($request)) { return; } if ('stylesheet' === $request) { Sitemaps\Renderer::renderStylesheet(); } $subtypes = Sitemaps\Providers::subtypes(); if ('index' === $request) { Sitemaps\Renderer::renderIndex($subtypes); } $subtypeKey = (string) get_query_var('rankify_sitemap_subtype'); $page = max(1, (int) get_query_var('rankify_sitemap_page')); if (!isset($subtypes[$subtypeKey])) { // An unknown or now-empty subtype must 404 rather than render an empty // document — a crawler treats an empty urlset as "everything here was // removed" and can drop those URLs from the index. $this->notFound(); return; } $subtype = $subtypes[$subtypeKey]; if ($page > $subtype['pages']) { $this->notFound(); return; } $entries = 'postType' === $subtype['type'] ? Sitemaps\Providers::posts($subtype['name'], $page) : Sitemaps\Providers::terms($subtype['name'], $page); if (empty($entries)) { $this->notFound(); return; } Sitemaps\Renderer::renderUrlSet($entries); } /** * Send a real 404 for a sitemap URL that does not exist. * * Simply returning is not enough: the query still carries our sitemap query * vars, so `redirect_canonical` decides the URL is wrong and 301s instead. * A redirect tells a crawler the sitemap moved, which is a worse lie than a * missing one, so the query is marked 404 before that hook runs — this * callback is registered at priority 0 for exactly that reason. */ protected function notFound() { global $wp_query; $wp_query->set_404(); status_header(404); nocache_headers(); // Let the theme render its 404 page rather than exiting on a blank body. remove_action('template_redirect', 'redirect_canonical'); } /** * Point robots.txt at the sitemap. * * Runs whether or not our sitemap is enabled: when it is off, core's own * sitemap is back in play and that is the one to advertise. */ public function addSitemapReference($output, $public) { if (!$public) { return $output; } if (!$this->isEnabled()) { return $output; } // Core adds its own "Sitemap: /wp-sitemap.xml" line from a robots_txt // filter at priority 0, and disabling core sitemaps via wp_sitemaps_enabled // does not take that line back out — so a site running Crownify would // advertise two sitemaps, one of which 404s. Strip it before adding ours. $output = preg_replace('#^\s*Sitemap:\s*\S*/wp-sitemap\.xml\s*$#mi', '', $output); $url = Sitemaps\Renderer::sitemapUrl(); if (false !== strpos($output, $url)) { return $output; } return rtrim($output, "\n") . "\n\nSitemap: " . esc_url_raw($url) . "\n"; } public function scheduleFlush() { update_option('rankify_flush_rewrite_rules', 'yes'); } /** * Flush on the request after a relevant setting changed. * * Deferred rather than flushed inline because the settings REST request has * already registered its rewrite rules by the time the option is written — * flushing there would persist the *old* rule set. */ public function maybeFlush() { if ('yes' !== get_option('rankify_flush_rewrite_rules')) { return; } delete_option('rankify_flush_rewrite_rules'); flush_rewrite_rules(false); } }