Skip to content

Repository files navigation

Celestial Sitemap v3.7.1

Enterprise-grade WordPress SEO plugin.

Running Tests

This repository uses PHPUnit with phpunit.xml.dist.

Requirements

  • PHP CLI
  • PHPUnit available on your PATH (for example, phpunit --version)

Run all tests

phpunit -c phpunit.xml.dist

tests/bootstrap.php falls back to the bundled WordPress stubs when the official WordPress test library is not installed, so the test suite can be executed locally without Composer or a full WordPress test environment.

Run a single test file

phpunit -c phpunit.xml.dist tests/OptionsTest.php

Run a specific test method

phpunit -c phpunit.xml.dist --filter test_returns_defaults_when_options_are_missing

Optional: use the official WordPress test library

If you already have the WordPress test library installed, set WP_TESTS_DIR before running PHPUnit:

WP_TESTS_DIR=/path/to/wordpress-tests-lib phpunit -c phpunit.xml.dist

Directory Structure

celestial-sitemap/
├── celestial-sitemap.php          # Bootstrap (autoloader, constants, hooks)
├── uninstall.php                  # Clean removal
├── includes/
│   ├── Plugin.php                 # Singleton orchestrator
│   ├── Core/
│   │   ├── Activator.php          # DB tables, default options, rewrite rules
│   │   ├── Deactivator.php        # Transient cleanup, cron removal
│   │   ├── Options.php            # Typed option accessors
│   │   └── NotFoundLogger.php     # 404 upsert logger
│   ├── Admin/
│   │   ├── AdminController.php    # Menu, meta boxes, AJAX, asset enqueue
│   │   ├── TaxonomyFields.php     # Term meta SEO fields (add/edit screens)
│   │   └── ConflictDetector.php   # Competing SEO plugin detection + admin notice
│   ├── SEO/
│   │   ├── HeadManager.php        # Title, meta description, OGP
│   │   ├── CanonicalManager.php   # Canonical URL, prev/next, paginated archives
│   │   └── RobotsManager.php      # noindex rules, robots.txt, custom robots.txt editor
│   ├── Schema/
│   │   └── SchemaManager.php      # Article, WebPage (all CPTs), BreadcrumbList, Organization
│   ├── Sitemap/
│   │   └── SitemapRouter.php      # XML sitemaps, XSL, caching, invalidation
│   ├── Indexing/
│   │   └── IndexNowPinger.php     # IndexNow ping-on-publish (Bing/Yandex/Naver/Seznam)
│   └── Redirect/
│       └── RedirectManager.php    # 301/302/307/308 redirect engine
├── templates/
│   ├── admin/
│   │   ├── dashboard.php          # Settings page
│   │   ├── redirects.php          # Redirect management
│   │   ├── 404-log.php            # 404 log viewer
│   │   └── robots-txt.php         # robots.txt editor
│   └── sitemap-xsl.php            # XSLT 1.0 stylesheet
├── assets/
│   ├── css/admin.css
│   └── js/admin.js
└── languages/
    └── celestial-sitemap.pot

Dependency Graph

Plugin (singleton)
 ├── Options ─────────────────┐
 ├── CanonicalManager ←───────┤ (reads Options)
 ├── SchemaManager ←──────────┤
 ├── HeadManager ←────────────┤ (unified head builder, receives CanonicalManager + SchemaManager)
 ├── RobotsManager ←──────────┤
 ├── SitemapRouter ←──────────┤
 ├── IndexNowPinger ←─────────┤ (reads/writes cel_indexnow_key)
 ├── RedirectManager          │ (standalone, no Options dep)
 ├── NotFoundLogger           │ (standalone)
 └── [admin only]             │
     ├── AdminController ←────┘ (reads/writes Options, uses RedirectManager, NotFoundLogger, SitemapRouter)
     ├── TaxonomyFields        (standalone – registers term meta fields for all public taxonomies)
     └── ConflictDetector      (standalone – detects Yoast, RankMath, AIOSEO, SEOPress)

No circular dependencies. All modules receive dependencies via constructor injection. HeadManager orchestrates CanonicalManager and SchemaManager — they no longer hook into wp_head independently.

Hook Registration Summary

Hook Callback Priority
plugins_loaded Plugin::boot() 10
init SitemapRouter::addRewriteRules 1
init SitemapRouter::maybeServeEarly 99
wp_loaded SitemapRouter::maybeFlushRewriteRules 10
admin_init TaxonomyFields::registerTaxonomyHooks 10
template_redirect RedirectManager::handleRedirect 1
template_redirect HeadManager::collect 10
template_redirect NotFoundLogger::log404 99
wp_head HeadManager::outputHead 1
pre_get_document_title HeadManager::filterTitle 99
document_title_parts HeadManager::filterTitleParts 99
document_title_separator HeadManager::filterSeparator 99
wp_robots HeadManager::filterRobotsDirectives 50
wp_robots RobotsManager::filterRobots 99
robots_txt RobotsManager::filterRobotsTxt 99
save_post SitemapRouter::invalidateOnSave 10
save_post AdminController::saveMetaBox 10
delete_post SitemapRouter::invalidateAll 10
created_term SitemapRouter::invalidateAll 10
edited_term SitemapRouter::invalidateAll 10
delete_term SitemapRouter::invalidateAll 10
wp_ajax_cel_save_robots_txt AdminController::ajaxSaveRobotsTxt 10
admin_menu AdminController::registerMenu 10
admin_enqueue_scripts AdminController::enqueueAssets 10
admin_notices ConflictDetector::maybeShowNotice 10
query_vars SitemapRouter::addQueryVars 10
cel_pregenerate_sitemaps SitemapRouter::pregenerateSitemaps 10
init IndexNowPinger::registerRewrite 10
query_vars IndexNowPinger::addQueryVars 10
template_redirect IndexNowPinger::maybeServeKeyFile 1
transition_post_status IndexNowPinger::handleTransition 10
{$taxonomy}_add_form_fields TaxonomyFields::renderAddFields 10
{$taxonomy}_edit_form_fields TaxonomyFields::renderEditFields 10
created_{$taxonomy} TaxonomyFields::saveFields 10
edited_{$taxonomy} TaxonomyFields::saveFields 10

Total: 34 hook registrations.

Unified head builder flow:

  1. HeadManager::collect() at template_redirect (priority 10) — resolves description, canonical, OGP, schema in a single pass
  2. HeadManager::outputHead() at wp_head (priority 1) — echoes the pre-built HTML fragment (zero DB queries at render time)

CanonicalManager and SchemaManager provide getCanonicalHtml() / getSchemaHtml() called internally by HeadManager — they do not hook wp_head independently.

Extensibility Filters

Third-party code can modify plugin output via apply_filters():

Filter Name Type Description
cel_head_title string Resolved document title
cel_head_description string Resolved meta description
cel_head_ogp array<string,string> Open Graph / Twitter Card tag array
cel_canonical_url string Resolved canonical URL
cel_robots_directives array<string,bool|string> Robots meta directives array
cel_schema_website array WebSite JSON-LD schema
cel_schema_organization array Organization/Person JSON-LD schema
cel_schema_article array Article JSON-LD schema (receives $post)
cel_schema_webpage array WebPage JSON-LD schema (receives $post)
cel_schema_breadcrumbs array BreadcrumbList JSON-LD schema
cel_schema_type string Schema @type for singular CPTs (receives post type)
cel_sitemap_post_urls array Sitemap post type URL array (receives post type)
cel_sitemap_taxonomy_urls array Sitemap taxonomy URL array (receives taxonomy)
cel_sitemap_news_urls array News sitemap URL array
cel_sitemap_video_urls array Video sitemap URL array
cel_sitemap_image_urls array Image sitemap URL array

Filter Examples

// Change schema type for WooCommerce products
add_filter('cel_schema_type', function (string $type, string $postType): string {
    if ($postType === 'product') {
        return 'Product';
    }
    return $type;
}, 10, 2);

// Add custom OGP tags (fb:app_id and twitter:site are now built-in settings)
add_filter('cel_head_ogp', function (array $og): array {
    $og['og:video'] = 'https://example.com/video.mp4';
    return $og;
});

// Override canonical URL for specific conditions
add_filter('cel_canonical_url', function (string $url): string {
    if (is_tax('product_cat')) {
        return home_url('/shop/');
    }
    return $url;
});

DB Usage

Custom Tables (created on activation via dbDelta):

  • {prefix}cel_redirects — redirect rules
  • {prefix}cel_404_log — 404 error log (deduped by URL)

Options:

  • cel_* — all prefixed, ~28 options
  • Frequently-read options (title, noindex, schema, OGP flags, verification codes) are autoload=true
  • Infrequently-read options (sitemap lists, robots.txt content, IndexNow key, 404 caps) are autoload=false
  • All options are bulk-loaded in a single query on first access via Options::loadAll()

Post Meta:

  • _cel_title, _cel_description, _cel_canonical, _cel_og_image, _cel_noindex

Term Meta:

  • _cel_title, _cel_description, _cel_canonical, _cel_noindex

Transients:

  • cel_sm_v{N}_* — versioned sitemap cache (1 hour TTL, invalidated via version counter)
  • cel_redirect_compiled — compiled redirect map (exact/prefix/regex, 1 hour TTL, ≤10,000 entries)
  • cel_404_rate_count — 404 rate limiter counter (60s window)
  • cel_404_row_count — 404 table row count cache (5 min TTL)
  • cel_indexnow_ping_{md5(url)} — IndexNow per-URL debounce flag (60s TTL)

File Cache:

  • wp-content/cache/cel-sitemaps/{blog_id}/ — pre-generated sitemap XML files (multisite-safe, per-site subdirectory)

Term Meta Admin UI

SEO fields are available on all public taxonomy add/edit screens:

  • SEO Title — Custom title tag for the term archive
  • Meta Description — Custom meta description for the term archive
  • Canonical URL — Custom canonical URL override
  • Noindex — Prevent search engines from indexing this term archive

Fields are rendered via {$taxonomy}_add_form_fields and {$taxonomy}_edit_form_fields. Saved via created_{$taxonomy} and edited_{$taxonomy} with nonce verification and sanitization.

Archive Pagination Canonical Strategy

Paginated archive pages (taxonomy, post type, author, home) use a self-referencing canonical:

  • /category/news/ → canonical: /category/news/
  • /category/news/page/2/ → canonical: /category/news/page/2/
  • /category/news/page/3/ → canonical: /category/news/page/3/

This ensures paginated pages are not incorrectly canonicalized to page 1. rel="prev" and rel="next" links are output for both singular paginated posts and paginated archives.

Competing SEO Plugin Detection

On admin pages, the plugin checks for:

Plugin Detection Method
Yoast SEO defined('WPSEO_VERSION') or class_exists('WPSEO_Options')
Rank Math defined('RANK_MATH_VERSION') or class_exists('RankMath')
All in One SEO defined('AIOSEO_VERSION') or function_exists('aioseo')
SEOPress defined('SEOPRESS_VERSION') or function_exists('seopress_init')
Google XML Sitemaps defined('SM_VERSION') or class_exists('GoogleSitemapGenerator')
XML Sitemap & Google News class_exists('XMLSF_Sitemaps')
Jetpack Sitemaps Jetpack::is_module_active('sitemaps')
Google Site Kit defined('GOOGLESITEKIT_VERSION') or class_exists('Google\\Site_Kit\\Plugin')

If detected, a warning notice is displayed. The plugin does not automatically deactivate competing plugins or remove their hooks.

Custom Post Type Schema Support

All public singular post types receive structured data:

  • post — Article schema (unchanged)
  • page — WebPage schema (unchanged)
  • Any other public CPT — WebPage schema by default

Use the cel_schema_type filter to set CPT-specific schema types:

add_filter('cel_schema_type', function (string $type, string $postType): string {
    return match ($postType) {
        'product' => 'Product',
        'event'   => 'Event',
        'recipe'  => 'Recipe',
        default   => $type,
    };
}, 10, 2);

The full schema array can be further customized via cel_schema_webpage.

Security Model

  1. Nonce verification — every AJAX handler calls check_ajax_referer(), meta boxes and term fields use wp_verify_nonce()
  2. Capability checkcurrent_user_can('manage_options'), edit_post, or manage_categories
  3. Input sanitizationsanitize_text_field, esc_url_raw, absint, sanitize_textarea_field
  4. URL field validation — canonical and OG image fields use esc_url_raw() (rejects javascript: URIs)
  5. Output escapingesc_html, esc_attr, esc_url throughout templates
  6. SQL injection prevention — all queries via $wpdb->prepare()
  7. XSS prevention — no raw $_GET/$_POST in output; all escaped
  8. CSRF prevention — nonce on all forms and AJAX
  9. Password-protected content — meta descriptions not auto-generated for password-protected posts
  10. Options key whitelistOptions::set() only accepts known cel_* keys
  11. 404 rate limiting — prevents attackers from flooding the database with unique 404 URLs
  12. IndexNow key isolation — per-site random 32-char key served only from /cel-indexnow-key.txt with X-Robots-Tag: noindex

Performance Design

  1. Zero frontend JS — no scripts enqueued on public pages
  2. Zero frontend CSS — no stylesheets on public pages
  3. Admin assets conditional — loaded only on plugin pages, post editor, and taxonomy screens
  4. Two-tier sitemap caching — file cache (zero DB queries) + transient fallback, 1 hour TTL, versioned cache keys
  5. Sitemap pre-generation — cron-based background pre-generation (debounced 30s after content changes) ensures frontend requests are instant
  6. Sitemap batch queries_prime_post_caches() + update_post_meta_cache() eliminates N+1 queries
  7. ETag + 304 — sitemaps support conditional requests
  8. Unified head builderHeadManager::collect() resolves all metadata (description, canonical, OGP, schema) in a single pass at template_redirect; outputHead() at wp_head outputs the pre-built HTML with zero DB queries
  9. Options bulk-loaded — all cel_* options loaded in a single DB query, cached in memory per request
  10. Compiled redirect map — three match types (exact O(1) hash, prefix longest-first, regex PCRE) cached in a single transient; direct indexed DB fallback for tables >10,000 entries
  11. Versioned cache invalidation — sitemap caches invalidated by incrementing a version counter (no LIKE queries)
  12. Multisite-safe file cache — per-site cache directory (cel-sitemaps/{blog_id}/) prevents cross-site collisions
  13. 404 dedupON DUPLICATE KEY UPDATE avoids row explosion
  14. 404 rate limiting — transient-based per-minute rate limiter prevents log flooding
  15. 404 table cap — configurable max row count prevents unbounded growth
  16. 90-day auto-purge — cron-based cleanup of old 404 entries
  17. WP core sitemaps disabled — prevents duplicate sitemap systems

Changelog

3.7.1

Low-effort / high-impact follow-ups on top of 3.7.0:

  • Conflict detection extended to Google Site Kit — Site Kit's Search Console module emits its own <link rel="canonical"> and <meta name="robots"> on connected pages, which visibly duplicate Celestial Sitemap's output in the rendered <head>. ConflictDetector::detectConflicts() now flags defined('GOOGLESITEKIT_VERSION') (or Google\Site_Kit\Plugin class) and includes "Google Site Kit" in the admin notice alongside existing SEO plugin detections (includes/Admin/ConflictDetector.php)
  • GSC-era encryption helpers actually removed — The 3.7.0 release notes announced the removal of the AES-256-CBC helpers along with the GSC OAuth scaffolding, but the Options::encrypt(), Options::decrypt(), and CIPHER_ALGO constant were left in place (no remaining callers). They have now been deleted alongside their round-trip test, finishing the dead-code cleanup claimed by 3.7.0 (includes/Core/Options.php, tests/OptionsTest.php)

Test Coverage:

  • tests/ConflictDetectorTest.php (new) — 2 cases: empty baseline in a clean environment and Site Kit detection when GOOGLESITEKIT_VERSION is defined

Files changed:

  • celestial-sitemap.php — version bump to 3.7.1
  • includes/Admin/ConflictDetector.php — Site Kit entry added to docblock and detectConflicts()
  • includes/Core/Options.php — deleted encrypt(), decrypt(), CIPHER_ALGO
  • tests/OptionsTest.php — removed test_encrypt_and_decrypt_support_plaintext_and_round_trip
  • tests/ConflictDetectorTest.php — new characterization tests
  • README.md — Competing SEO Plugin Detection table expanded to list all detected integrations

3.7.0

New Features — IndexNow, search engine verification, timezone-aware sitemaps:

  • IndexNow ping-on-publish — On every draft → publish transition, the plugin notifies Bing, Yandex, Naver, and Seznam via the IndexNow protocol (Google does not participate). A per-site 32-character key is generated lazily on first dashboard view, persisted as cel_indexnow_key, and served at the fixed verification URL /cel-indexnow-key.txt. Requests are non-blocking (blocking => false, 2s timeout) and deduplicated per URL via a 60-second transient debounce. Revisions, autosaves, posts marked _cel_noindex = 1, and non-public post types are skipped (includes/Indexing/IndexNowPinger.php)
  • Search engine verification meta tags — Admin-configurable verification codes for Google Search Console, Bing Webmaster Tools, Yandex, Baidu, Naver, and Pinterest. Each provider's vendor-specific <meta name="…"> tag is emitted on the front page only, with values run through esc_attr() to prevent attribute-boundary escapes (includes/SEO/HeadManager.php, includes/Core/Options.php, templates/admin/dashboard.php)
  • Site-timezone lastmod — XML sitemap <lastmod> values now use the site's configured timezone (e.g. +09:00 for Asia/Tokyo) instead of UTC (Z). A new SitemapRouter::formatLastmod(?int) helper replaces all 10 gmdate('c') call sites across index, recent, news, image, video, taxonomy, post-type, and hub sitemaps (includes/Sitemap/SitemapRouter.php)
  • RSS auto-discovery link<link rel="alternate" type="application/rss+xml"> is emitted on the front page when the theme or another plugin has unhooked WordPress core's feed_links(), preventing the discovery signal from being lost without duplicating it (includes/SEO/HeadManager.php)

Dashboard UI:

  • "Submit to Search Console" sub-section — one-click-copy sitemap URL with deep links to Google Search Console and Bing Webmaster Tools (templates/admin/dashboard.php)
  • "Search Engine Verification" card — input fields for all six verification providers with per-provider output examples (templates/admin/dashboard.php)
  • IndexNow read-only panel — displays the generated site key and the key file URL, both select-on-focus for easy copying (templates/admin/dashboard.php)

Removals (dead code):

  • Removed the unused Google Search Console OAuth2 scaffolding (cel_gsc_client_id, cel_gsc_client_secret, cel_gsc_access_token, cel_gsc_refresh_token, AES-256-CBC encryption helpers, dashboard fields). The integration was never wired and Google Search Console submission remains manual (includes/Core/Options.php, includes/Core/Activator.php, includes/Admin/AdminController.php, templates/admin/dashboard.php)

New Options:

  • cel_verify_google, cel_verify_bing, cel_verify_yandex, cel_verify_baidu, cel_verify_naver, cel_verify_pinterest (string, default: '', autoload) — verification codes per provider

New Rewrite Rule:

  • ^cel-indexnow-key\.txt/?$index.php?cel_indexnow_key=1 (Migrator flushes on upgrade via cel_flush_rewrite_rules flag)

Test Coverage:

  • tests/IndexNowPingerTest.php — 8 cases covering key generation/persistence, key file serving, publish-edge triggering, revision/autosave/noindex/empty-key guards, and URL debounce
  • tests/SeoManagersTest.php — verification tag emission on front page, skip on non-front, XSS escape; RSS alternate link emission/suppression/escaping
  • tests/SitemapRouterTest.phpformatLastmod timezone offset and null-timestamp fallback, buildIndex site-timezone lastmod
  • tests/wp-stubs.php — new stubs for wp_date, wp_generate_password, wp_remote_post, wp_json_encode, wp_is_post_revision, wp_is_post_autosave, has_action, and extended get_bloginfo

Files Added:

  • includes/Indexing/IndexNowPinger.php
  • tests/IndexNowPingerTest.php

Files Modified:

  • celestial-sitemap.php — version bump to 3.7.0
  • includes/Plugin.php — wires IndexNowPinger
  • includes/Core/Activator.php — verification option defaults, IndexNow rewrite rule, removed GSC defaults
  • includes/Core/Options.phpVERIFICATION_PROVIDERS constant, verificationCode(), indexNowKey(); removed GSC accessors and encryption
  • includes/SEO/HeadManager.phpbuildVerificationTags(), buildFeedLink()
  • includes/Sitemap/SitemapRouter.phpformatLastmod() helper replaces all gmdate('c') sites
  • includes/Admin/AdminController.php — verification code save loop, dashboard ensures IndexNow key on render, removed GSC encryption
  • templates/admin/dashboard.php — Submit-to-Search-Console sub-section, Verification card, IndexNow panel
  • tests/TestCase.php — resets IndexNow/verification globals between tests

3.6.3

Bug Fixes — Redirects, Sitemaps, SEO metadata, and admin UX:

  • Redirect engine correctness restored for all match types — Large redirect tables now preserve exact/prefix/regex behavior during direct DB fallback, and the admin Redirects screen can finally create non-exact rules by sending match_type through the AJAX endpoint (RedirectManager.php, AdminController.php, templates/admin/redirects.php, assets/js/admin.js)
  • Sitemap index and pre-generation no longer advertise empty filtered segments — Taxonomy/image/video/post-type page discovery now respects _cel_noindex and media availability when counting chunks, and the sitemap index only lists segments that actually build non-empty XML. This prevents broken -2.xml, -3.xml, etc. links near pagination boundaries (SitemapRouter.php)
  • Sitemap routing and taxonomy discovery tightened — Unknown or out-of-range sitemap URLs now return 404 XML responses, and taxonomy pagination is emitted into the index and pregenerated correctly (SitemapRouter.php)
  • 404 log table-cap behavior fixed — Existing rows continue to update when the table is full, and the cached row count is now invalidated after successful new inserts so the max-row cap stays effective under sustained 404 traffic (NotFoundLogger.php)
  • SEO metadata corrected for archive and page contexts — Static posts pages now canonicalize to the configured posts page instead of the site root, and non-post singular content no longer emits article:* Open Graph metadata (CanonicalManager.php, HeadManager.php)
  • Admin save paths aligned with real WordPress permissions/hooks — Plugin submenu pages now load their admin assets correctly, and taxonomy field saves use taxonomy-specific capabilities instead of assuming manage_categories (AdminController.php, TaxonomyFields.php)

Test Coverage:

  • Added regression coverage for redirect large-table fallback, redirect validation, redirect admin match_type, taxonomy sitemap pagination/indexing, filtered empty sitemap segments, News fallback behavior, sitemap 404 responses, 404 row-cap cache invalidation, taxonomy capability checks, static posts-page canonical URLs, and non-post singular OGP output (tests/RedirectManagerTest.php, tests/AdminControllerTest.php, tests/SitemapRouterTest.php, tests/NotFoundLoggerTest.php, tests/TaxonomyFieldsTest.php, tests/SeoManagersTest.php, tests/wp-stubs.php, tests/TestCase.php)

Files Modified:

  • celestial-sitemap.php — version bump to 3.6.3
  • includes/Redirect/RedirectManager.php — large-table fallback matching, redirect validation
  • includes/Sitemap/SitemapRouter.php — taxonomy pagination, 404 sitemap responses, News fallback handling, filtered chunk discovery
  • includes/Core/NotFoundLogger.php — row-cap update ordering, row-count cache invalidation
  • includes/SEO/CanonicalManager.php — static posts page canonical handling
  • includes/SEO/HeadManager.php — OGP type fix for non-post singular content
  • includes/Admin/AdminController.php — submenu asset hooks, redirect AJAX match_type
  • includes/Admin/TaxonomyFields.php — taxonomy-specific capability checks
  • templates/admin/redirects.php — redirect match-type UI
  • assets/js/admin.js — redirect match-type submission
  • tests/* — regression coverage for all of the above fixes

3.6.2

Bug Fix — News sitemap empty fallback:

  • News sitemap fallback for empty results — When no posts exist within the 48-hour news window, the sitemap previously output an empty <urlset> with zero entries. Now falls back to displaying the single most recent published post, ensuring the news sitemap always contains at least one valid entry. Only returns empty if no published posts exist at all (SitemapRouter.php)

Files Modified:

  • celestial-sitemap.php — version bump to 3.6.2
  • includes/Sitemap/SitemapRouter.php — fallback query in getNewsUrls() for empty time-window results

3.6.1

Bug Fix — News sitemap GSC validation error:

  • News sitemap empty namespace fix — When no news articles exist (no posts within 48 hours), the news sitemap previously declared xmlns:news and xmlns:image namespaces with zero <url> entries, causing Google Search Console to report "XML tag not specified". The namespace declarations are now conditionally included only when news URLs exist. Empty news sitemaps output a plain <urlset> without news-specific namespaces (SitemapRouter.php)

Files Modified:

  • celestial-sitemap.php — version bump to 3.6.1
  • includes/Sitemap/SitemapRouter.php — conditional namespace declaration in buildNewsSitemap()

3.6.0

New Feature — robots.txt Editor:

  • Custom robots.txt editor — Admin UI (Celestial Sitemap → robots.txt) for editing the virtual robots.txt output via WordPress robots_txt filter. When enabled, replaces the WordPress-generated content with user-defined directives. The plugin's sitemap URL is auto-appended if not already present in the custom content (RobotsManager.php, AdminController.php, templates/admin/robots-txt.php)
  • Input validation & sanitization — Null byte removal, CRLF→LF normalization, sanitize_textarea_field, 64 KB size limit. Content is saved before the enabled toggle to prevent mid-request inconsistency (AdminController.php)
  • Soft warnings on save — Non-blocking warnings when custom content is missing User-agent: or Disallow: /wp-admin/ directives (AdminController.php)
  • Physical file detection — Warns when a physical robots.txt file exists at ABSPATH, which would override the virtual output (AdminController.php, templates/admin/robots-txt.php)
  • Competing plugin detection — robots.txt page shows a warning if Yoast SEO, Rank Math, All in One SEO, or SEOPress are active, as they may also modify robots_txt filter output (AdminController.php)
  • State transitionsenabled=false or empty content falls back to WordPress default + sitemap URL append. Plugin deactivation removes the filter (via boot() not being called). Plugin deletion removes both options via existing cel_% wildcard cleanup (RobotsManager.php, uninstall.php)

New Options:

  • cel_robots_txt_enabled (bool, default: 0) — enable/disable custom robots.txt
  • cel_robots_txt_content (text, default: '') — custom robots.txt content

Files Added:

  • templates/admin/robots-txt.php — robots.txt editor UI with physical file warning, conflict detection, preview link

Files Modified:

  • celestial-sitemap.php — version bump to 3.6.0
  • includes/Core/Options.phpALLOWED_KEYS + robotsTxtEnabled(), robotsTxtContent() getters
  • includes/Core/Activator.php — default options for cel_robots_txt_enabled, cel_robots_txt_content
  • includes/SEO/RobotsManager.phpfilterRobotsTxt() extended to serve custom content when enabled
  • includes/Admin/AdminController.phpcel-robots-txt submenu, renderRobotsTxt(), detectRobotsTxtConflicts(), ajaxSaveRobotsTxt() with security checks and validation
  • assets/js/admin.js — robots.txt form handler with warning display, showMessage selector updated
  • assets/css/admin.css.cel-robots-textarea monospace styling

3.5.2

Bug Fix — Sitemap routing stability & plugin coexistence:

  • Rewrite rules flush moved to wp_loaded — Previously, flush_rewrite_rules ran at init:1, before other plugins registered their rewrite rules. This caused the persisted rule set to be incomplete, silently breaking URL routing for WooCommerce, Yoast, and other plugins that register rules at init:10+. The flush now runs at wp_loaded, after all init callbacks have completed (SitemapRouter.php)
  • 404 sitemap response headers — Requests for non-existent sitemap types (e.g., /cel-sitemap-foobar.xml) now return proper Content-Type: application/xml, X-Robots-Tag: noindex, and X-Content-Type-Options: nosniff headers. Previously, the 404 path sent raw XML with default text/html Content-Type, causing browsers to render an invisible page. Output buffer cleanup (cleanOutputBuffer()) is also applied to prevent stray plugin output from corrupting the XML (SitemapRouter.php)
  • Self-healing flush loop prevention — Added a transient guard (cel_self_heal_done, 1-hour TTL) to the self-healing rewrite rule check. In environments with object cache (Redis/Memcached), stale cache data could cause flush_rewrite_rules to fire on every request. The guard limits self-healing flushes to at most once per hour (SitemapRouter.php)
  • Stale class docblock updated — Architecture documentation in SitemapRouter now correctly describes init:99 URI-based serving and wp_loaded flush timing instead of the removed template_redirect approach (SitemapRouter.php)

Files Modified:

  • celestial-sitemap.php — version bump to 3.5.2
  • includes/Sitemap/SitemapRouter.php — flush timing, 404 headers, self-healing guard, docblock
  • includes/Core/Migrator.php — comment update (initwp_loaded)
  • README.md — hook table updated (wp_loaded entry added), changelog

3.5.1

Bug Fix — Settings not reflected in sitemaps:

  • Sitemap cache invalidation on settings saveAdminController::ajaxSaveSettings() now calls SitemapRouter::invalidateAll() after saving settings. Previously, changing sitemap post types, taxonomies, or other settings had no effect until the cache expired naturally (1 hour) or was manually flushed via the "Flush Sitemap Cache" button (AdminController.php)
  • autoload flag preservation — All update_option() calls in ajaxSaveSettings() and Options::set() no longer pass false as the third argument. Previously, every settings save silently downgraded autoload from true (set by Activator::setDefaultOptions()) to false for all options, increasing frontend DB queries on subsequent requests (AdminController.php, Options.php)

Files Modified:

  • celestial-sitemap.php — version bump to 3.5.1
  • includes/Admin/AdminController.php — sitemap cache invalidation on save, update_option() autoload fix
  • includes/Core/Options.phpupdate_option() autoload fix in set()

3.5.0

New Features — Specialized Google Sitemaps:

  • Google News sitemap (cel-sitemap-news.xml) — Dedicated sitemap conforming to Google News sitemap spec. Primarily includes post type articles published within the last 48 hours, with a fallback to the most recent published post when that window is empty so the sitemap can still emit one entry. Max 1,000 URLs. Includes news:publication (name + language), news:publication_date, news:title, and optional image:image extension for featured images (SitemapRouter.php)
  • Image sitemap (cel-sitemap-image.xml) — Standalone image sitemap listing all published posts with featured images across all configured post types. Uses image:image extension with image:loc and image:title. Paginated at 2,500 URLs per file (SitemapRouter.php)
  • Video sitemap (cel-sitemap-video.xml) — Dedicated video sitemap for posts containing YouTube embeds. Extracts YouTube video IDs from post content via regex. Includes video:thumbnail_loc, video:title, video:description, video:player_loc, and video:publication_date. Paginated at 2,500 URLs per file (SitemapRouter.php)

Sitemap Index Integration:

  • cel-sitemap.xml index now conditionally includes entries for news, image, and video sitemaps when qualifying content exists
  • News entry appears when at least one eligible published post exists; if the 48-hour window is empty, the most recent published post is used as the fallback entry
  • Image entry appears only when posts with featured images exist
  • Video entry appears only when posts containing YouTube embeds exist
  • All specialized sitemaps support pagination in the index (e.g., cel-sitemap-image-1.xml, cel-sitemap-image-2.xml)

Background Pre-generation:

  • pregenerateSitemaps() extended to pre-generate news, image, and video sitemaps via cron, ensuring zero-latency frontend responses

Runtime Safety Hardening:

  • Fatal error fixupdate_post_meta_cache() (non-existent) replaced with correct \update_postmeta_cache() WordPress API
  • Global namespace resolution — All WordPress core function calls in namespaced code prefixed with \ to prevent fatal errors from incorrect namespace resolution
  • Shared thumbnail bug fixbatchLoadThumbnails() rewritten to use post→thumbnail mapping instead of thumbnail→post, correctly handling multiple posts sharing the same featured image
  • strtotime() false guardpost_modified_gmt value '0000-00-00 00:00:00' no longer causes fatal error; falls back to current time
  • get_permalink() false guard — Posts returning false from get_permalink() are safely skipped
  • XML control character strippingescXml() helper strips illegal XML 1.0 characters (\x00-\x08, \x0B, \x0C, \x0E-\x1F) using esc_xml() (WP 5.5+) with esc_html() fallback
  • Defensive array checksis_array() guards on filter results and nested image/video arrays throughout sitemap builders

XSL Stylesheet Update:

  • sitemap-xsl.php updated with xmlns:news and xmlns:video namespace declarations
  • URL table column renamed from "Images" to "Info" to accommodate News and Video badges
  • Badge rendering for News (badge-news) and Video (badge-video) entries alongside existing Image badges

New Extensibility Filters:

  • cel_sitemap_news_urls — filter news sitemap URL array
  • cel_sitemap_video_urls — filter video sitemap URL array
  • cel_sitemap_image_urls — filter image sitemap URL array

Files Modified:

  • celestial-sitemap.php — version bump to 3.5.0
  • includes/Sitemap/SitemapRouter.php — news/image/video sitemap builders, data fetchers, count helpers, routing, index integration, pre-generation, escXml(), runtime safety fixes
  • templates/sitemap-xsl.php — namespace declarations, badge rendering for news/video

3.4.0

Plugin Identity Refactor — Full Rename:

  • Plugin renamed from "Ultimate SEO Pro" to "Celestial Sitemap" across the entire source tree. Plugin slug, text domain, namespace, constants, menu titles, and all user-facing strings updated
  • Namespace renamedUltimateSEOPro\CelestialSitemap\ (all PHP files under includes/)
  • Constant prefix renamedUSP_*CEL_* (CEL_VERSION, CEL_FILE, CEL_DIR, CEL_URL, CEL_BASENAME, CEL_MIN_WP, CEL_MIN_PHP)
  • Option key prefix renamedusp_*cel_* (all 24 options in Options::ALLOWED_KEYS)
  • Post/term meta prefix renamed_usp_*_cel_* (_cel_title, _cel_description, _cel_canonical, _cel_noindex, _cel_og_image)
  • DB table prefix renamedusp_redirectscel_redirects, usp_404_logcel_404_log
  • Transient prefix renamedusp_*cel_* (cel_sm_v{N}_*, cel_redirect_compiled, cel_404_rate_count, cel_404_row_count)
  • Cache directory renamedusp-sitemaps/cel-sitemaps/
  • Sitemap endpoint renamed/sitemap.xml/cel-sitemap.xml, /sitemap-{type}.xml/cel-sitemap-{type}.xml, /sitemap.xsl/cel-sitemap.xsl
  • Query vars renamedusp_sitemapcel_sitemap, usp_sitemap_pagecel_sitemap_page, usp_xslcel_xsl
  • Cron events renamedusp_cleanup_404_logcel_cleanup_404_log, usp_pregenerate_sitemapscel_pregenerate_sitemaps
  • AJAX actions renamedusp_save_settingscel_save_settings, usp_flush_sitemapcel_flush_sitemap, usp_add_redirectcel_add_redirect, usp_delete_redirectcel_delete_redirect, usp_delete_404cel_delete_404, usp_clear_404cel_clear_404
  • Admin menu slugs renamedusp-dashboardcel-dashboard, usp-redirectscel-redirects, usp-404-logcel-404-log
  • Asset handles renamedusp-admincel-admin
  • JS global renameduspAdmincelAdmin
  • Nonce action renamedusp_admin_noncecel_admin_nonce, usp_save_metacel_save_meta, usp_save_term_metacel_save_term_meta
  • All extensibility filters renamedusp_*cel_* (13 filters: cel_head_title, cel_head_description, cel_head_ogp, cel_canonical_url, cel_robots_directives, cel_schema_website, cel_schema_organization, cel_schema_article, cel_schema_webpage, cel_schema_breadcrumbs, cel_schema_type, cel_sitemap_post_urls, cel_sitemap_taxonomy_urls)
  • HTML/CSS prefix renamedusp-*cel-* in all templates, CSS classes, and element IDs
  • Text domain renamedultimate-seo-procelestial-sitemap
  • Main entry file renamedultimate-seo-pro.phpcelestial-sitemap.php

Integrity Verification:

  • Zero legacy identifiers (usp_, USP_, UltimateSEOPro, uspAdmin, ultimate-seo-pro) remain in any file
  • Full cross-reference audit passed: all hooks, meta keys, option keys, AJAX actions, HTML IDs, CSS classes, nonces, DB tables, cache keys, cron events, rewrite rules, and constants are internally consistent

Files Modified: All 19 PHP files, 1 JS file, 1 CSS file, 3 template files, README.md

3.3.0

Architecture — Unified Head Builder:

  • Single-pass metadata generationHeadManager::collect() runs at template_redirect and resolves ALL head metadata (meta description, canonical, prev/next, OGP, Twitter Card, JSON-LD schema) in one pass. outputHead() at wp_head echoes the pre-built HTML fragment with zero DB queries or computation at render time (HeadManager.php, CanonicalManager.php, SchemaManager.php, Plugin.php)
  • CanonicalManager refactored — no longer hooks wp_head independently. Provides getCanonicalHtml() returning canonical + prev/next as an HTML string, called by HeadManager during the collect phase (CanonicalManager.php)
  • SchemaManager refactored — no longer hooks wp_head independently. Provides getSchemaHtml() returning all <script type="application/ld+json"> blocks as an HTML string, called by HeadManager during the collect phase (SchemaManager.php)

Architecture — Sitemap Pre-generation:

  • Two-tier file cache — sitemaps are first served from filesystem (wp-content/cache/cel-sitemaps/{blog_id}/), falling back to transient cache, then generation. File cache eliminates all DB queries for cached sitemaps (SitemapRouter.php)
  • Background pre-generation via croncel_pregenerate_sitemaps single event fires 30 seconds after content changes (debounced), pre-generating all sitemap segments so frontend requests are instant (SitemapRouter.php)
  • Per-type cache files — each sitemap segment has its own cache file (v{N}-idx.xml, v{N}-post_p1.xml, v{N}-category_p1.xml), avoiding full-cache rebuilds when only one segment changes (SitemapRouter.php)
  • Directory listing protectionindex.php auto-created in cache directory (SitemapRouter.php)

Architecture — Compiled Redirect Map:

  • Three match types — redirect rules now support exact (O(1) hash lookup), prefix (longest-prefix-first matching), and regex (PCRE with backreference substitution in target URLs) (RedirectManager.php)
  • Compiled map cache — all redirect rules compiled into a structured map (['exact' => [...], 'prefix' => [...], 'regex' => [...]]) and cached in a single transient. Prefix rules sorted by source length for correct matching (RedirectManager.php)
  • Large table fallback — tables exceeding 10,000 entries skip compiled map and use direct indexed DB queries for exact matches (RedirectManager.php)
  • match_type columnVARCHAR(10) NOT NULL DEFAULT 'exact' added to {prefix}cel_redirects table with index. Backward-compatible: missing column detected via SHOW COLUMNS and cached statically (Activator.php, RedirectManager.php)
  • Loop detection — exact-match redirects check for simple redirect loops (A→B→A) before inserting (RedirectManager.php)
  • Regex validation — regex patterns validated via preg_match() before insert (RedirectManager.php)

Security — Encryption Upgrade:

  • Random IV per encryption — AES-256-CBC now uses openssl_random_pseudo_bytes() for each encryption instead of a deterministic IV derived from the key. Format: enc:{base64_iv}:{base64_cipher}. Backward-compatible: legacy format (enc:{base64_cipher}) with deterministic IV still decrypted correctly (Options.php)

Robustness — Glitch Audit:

  • Safe filesystem cleanupglob() + array_map('unlink') pattern replaced with scandir() + is_file() guard. Prevents PHP warnings in restricted environments (open_basedir) and correctly handles non-XML files (index.php, .htaccess) left in cache directories (Deactivator.php, uninstall.php, SitemapRouter.php)
  • Multisite-safe cache paths — sitemap file cache now uses per-site subdirectories (cel-sitemaps/{blog_id}/) to prevent cross-site cache collisions on multisite installations (SitemapRouter.php, Deactivator.php, uninstall.php)
  • Robust cron unschedulingwp_clear_scheduled_hook() replaced with explicit while (wp_next_scheduled()) { wp_unschedule_event() } loop to ensure all scheduled instances are removed (Deactivator.php, uninstall.php)
  • Complete deactivation cleanup — cache directory removal now deletes all files (not just *.xml), so rmdir() actually succeeds. Parent cel-sitemaps/ directory removed on uninstall if empty (Deactivator.php, uninstall.php)
  • Cleanup symmetryDeactivator.php and uninstall.php now perform identical cleanup: cron unscheduling, file cache removal, transient deletion (Deactivator.php, uninstall.php)

Files Modified:

  • celestial-sitemap.php — version bump to 3.3.0
  • includes/Plugin.php — SchemaManager injected into HeadManager constructor; SchemaManager no longer calls register()
  • includes/Core/Options.php — random IV encryption, backward-compatible decryption
  • includes/Core/Activator.phpmatch_type column and index added to redirects table
  • includes/Core/Deactivator.php — safe cleanup with scandir(), multisite-safe cache path, explicit cron unscheduling
  • includes/SEO/HeadManager.php — unified head builder: collect() at template_redirect, outputHead() at wp_head
  • includes/SEO/CanonicalManager.phpgetCanonicalHtml() method, no direct wp_head hook
  • includes/Schema/SchemaManager.phpgetSchemaHtml() method, no direct wp_head hook
  • includes/Sitemap/SitemapRouter.php — two-tier file cache, cron pre-generation, per-type cache, multisite-safe paths, safe flushFileCache()
  • includes/Redirect/RedirectManager.php — compiled map with exact/prefix/regex, loop detection, regex validation
  • uninstall.php — safe cleanup, multisite-safe cache path, parent directory cleanup

3.2.0

Performance — Critical (production-grade for 100k+ DAU):

  • Sitemap N+1 query fixgetPostTypeUrls() now batch-primes post caches via _prime_post_caches() and update_post_meta_cache(), and batch-loads thumbnail URLs. Eliminates 12,500+ individual DB queries per 2,500-post sitemap chunk (SitemapRouter.php)
  • Options bulk loading — All cel_* options loaded in a single DB query on first access and cached in memory. Frequently-used options (title, noindex flags, schema, OGP) set to autoload=true (Options.php, Activator.php)
  • Versioned sitemap cache invalidation — Replaced DELETE FROM wp_options WHERE option_name LIKE ... with a version counter (cel_sitemap_cache_version). Incrementing the counter invalidates all sitemap caches; old entries expire naturally via TTL. Eliminates expensive LIKE scans on every save_post (SitemapRouter.php)
  • Redirect tiered caching — 3-tier strategy: full map transient (≤5,000), partitioned bucket cache by first path segment (5,001–20,000), direct indexed DB query (>20,000). Eliminates the performance cliff at 5,001 entries (RedirectManager.php)

Security:

  • 404 log rate limiting — Transient-based per-minute rate limiter prevents attackers from flooding the database with unique 404 URLs. Configurable via cel_404_rate_limit option (default: 100 new URLs/min). Existing URL upserts (hit count increments) are always allowed (NotFoundLogger.php)
  • 404 table size cap — Configurable maximum row count (cel_404_max_rows, default: 50,000). Inserts are skipped when the cap is exceeded. Row count cached in a 5-minute transient to avoid COUNT(*) on every 404 (NotFoundLogger.php)
  • GSC credential encryptioncel_gsc_client_secret and cel_gsc_access_token are now encrypted at rest using AES-256-CBC with keys derived from wp_salt(). Backward-compatible: existing plaintext values are read as-is and encrypted on next save (Options.php, AdminController.php)
  • Options key whitelistOptions::set() now validates the key against an allowed list and throws \InvalidArgumentException for unknown keys (Options.php)
  • URL field validation hardened — Canonical URL and OG image meta fields now use esc_url_raw() instead of sanitize_text_field(), rejecting javascript: and other non-HTTP URIs (AdminController.php)
  • Password-protected content protection — Auto-generated meta descriptions are suppressed for password-protected posts to prevent content leakage via cached pages (HeadManager.php)

SEO Correctness:

  • WP core sitemaps disabledwp_sitemaps_enabled filter set to false during plugin boot to prevent two competing sitemap systems (Plugin.php)
  • Paginated prev/next fixCanonicalManager::outputPrevNext() now uses _wp_link_page() for correct URL construction under both pretty and default (?p=123) permalink structures (CanonicalManager.php)
  • Empty og:url prevention — OGP tags are now entirely suppressed when no valid og:url can be determined, avoiding invalid Open Graph markup (HeadManager.php)
  • URL-encoded redirect pathsRedirectManager::normalise() now applies rawurldecode() to match requests for encoded URLs (e.g., /café/ vs /caf%C3%A9/) (RedirectManager.php)

New Features:

  • Facebook App IDfb:app_id meta tag, configurable via Settings → Open Graph / Social (cel_fb_app_id option)
  • Twitter/X site handletwitter:site meta tag, configurable via Settings → Open Graph / Social (cel_twitter_site option)

Robustness:

  • Migration error handlingMigrator::maybeRun() wrapped in try/catch. On failure, the version is NOT updated (migration retries on next request) and the error is logged when WP_DEBUG is enabled (Migrator.php)
  • TRUNCATE replaced with DELETENotFoundLogger::clearAll() now uses DELETE FROM instead of TRUNCATE TABLE to avoid requiring DROP privilege on shared hosting (NotFoundLogger.php)
  • Multisite uninstalluninstall.php now iterates all sites via get_sites() on multisite installations (uninstall.php)

Files Modified:

  • celestial-sitemap.php — version bump to 3.2.0
  • includes/Plugin.phpwp_sitemaps_enabled filter
  • includes/Core/Options.php — bulk loader, encryption, key whitelist, invalidateCache(), fbAppId(), twitterSite()
  • includes/Core/Activator.php — autoload flags, new default options (cel_fb_app_id, cel_twitter_site, cel_404_rate_limit, cel_404_max_rows)
  • includes/Core/Deactivator.php — cron hook name fix
  • includes/Core/NotFoundLogger.php — rate limiter, table cap, DELETE instead of TRUNCATE
  • includes/Core/Migrator.php — try/catch, conditional version update
  • includes/Sitemap/SitemapRouter.php — batch cache priming, versioned cache keys, batchLoadThumbnails()
  • includes/Redirect/RedirectManager.phprawurldecode(), 3-tier cache strategy
  • includes/SEO/HeadManager.php — password protection check, empty og:url guard, fb:app_id, twitter:site
  • includes/SEO/CanonicalManager.phpsingularPageUrl() method, get_the_ID() validation
  • includes/Admin/AdminController.phpesc_url_raw() for URL fields, GSC encryption on save, new social settings
  • templates/admin/dashboard.php — Facebook App ID and Twitter/X handle fields
  • uninstall.php — multisite cleanup via get_sites()

3.1.0

New Features:

  • Term Meta Admin UI — SEO fields (title, description, canonical, noindex) on all public taxonomy add/edit screens via TaxonomyFields class
  • Competing SEO Plugin Detection — Admin notice when Yoast SEO, Rank Math, All in One SEO, or SEOPress is active simultaneously via ConflictDetector class
  • Custom Post Type Schema — All public singular CPTs now receive WebPage structured data by default; override via cel_schema_type filter
  • Archive Pagination Canonical — Paginated archives self-reference their own URL instead of incorrectly canonicalizing to page 1; rel="prev" / rel="next" output for archives
  • Extensibility Filters — 13 apply_filters() hooks added across all output paths (title, description, OGP, canonical, robots, schema, sitemap)

Improvements:

  • cel_head_title filter applied in filterTitleParts() for full context coverage (singular, front page, taxonomy)
  • resolveDescription() result cached per request to prevent double filter execution
  • cel_canonical term field uses esc_url_raw() for proper URL sanitization
  • Admin CSS enqueued on taxonomy screens (edit-tags.php, term.php)

Files Added:

  • includes/Admin/TaxonomyFields.php
  • includes/Admin/ConflictDetector.php

Files Modified:

  • celestial-sitemap.php — version bump to 3.1.0
  • includes/Plugin.php — wires TaxonomyFields and ConflictDetector
  • includes/SEO/HeadManager.php — filters + description caching
  • includes/SEO/CanonicalManager.php — pagination strategy + filter
  • includes/SEO/RobotsManager.php — filter
  • includes/Schema/SchemaManager.php — CPT support + filters
  • includes/Sitemap/SitemapRouter.php — filters
  • includes/Admin/AdminController.php — taxonomy screen CSS enqueue

3.0.0

  • Initial release with title/meta/canonical/OGP/Schema/Sitemap/Redirect/404 log

Known Limitations / Planned Improvements

  1. Internal link analysis — requires content parsing; planned as background cron job
  2. WooCommerce — product schema not implemented (use cel_schema_type filter with 'Product')
  3. Multilingual (WPML/Polylang) — hreflang sitemap extension not yet active
  4. Bulk editor — per-post SEO fields editable only in post editor, not quick-edit
  5. Google Search Console API — Google does not participate in IndexNow; GSC submission remains manual (via the Dashboard's "Submit to Search Console" URL)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages