Enterprise-grade WordPress SEO plugin.
This repository uses PHPUnit with phpunit.xml.dist.
- PHP CLI
- PHPUnit available on your PATH (for example,
phpunit --version)
phpunit -c phpunit.xml.disttests/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.
phpunit -c phpunit.xml.dist tests/OptionsTest.phpphpunit -c phpunit.xml.dist --filter test_returns_defaults_when_options_are_missingIf 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.distcelestial-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
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 | 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:
HeadManager::collect()attemplate_redirect(priority 10) — resolves description, canonical, OGP, schema in a single passHeadManager::outputHead()atwp_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.
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 |
// 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;
});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)
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.
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.
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.
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.
- Nonce verification — every AJAX handler calls
check_ajax_referer(), meta boxes and term fields usewp_verify_nonce() - Capability check —
current_user_can('manage_options'),edit_post, ormanage_categories - Input sanitization —
sanitize_text_field,esc_url_raw,absint,sanitize_textarea_field - URL field validation — canonical and OG image fields use
esc_url_raw()(rejectsjavascript:URIs) - Output escaping —
esc_html,esc_attr,esc_urlthroughout templates - SQL injection prevention — all queries via
$wpdb->prepare() - XSS prevention — no raw
$_GET/$_POSTin output; all escaped - CSRF prevention — nonce on all forms and AJAX
- Password-protected content — meta descriptions not auto-generated for password-protected posts
- Options key whitelist —
Options::set()only accepts knowncel_*keys - 404 rate limiting — prevents attackers from flooding the database with unique 404 URLs
- IndexNow key isolation — per-site random 32-char key served only from
/cel-indexnow-key.txtwithX-Robots-Tag: noindex
- Zero frontend JS — no scripts enqueued on public pages
- Zero frontend CSS — no stylesheets on public pages
- Admin assets conditional — loaded only on plugin pages, post editor, and taxonomy screens
- Two-tier sitemap caching — file cache (zero DB queries) + transient fallback, 1 hour TTL, versioned cache keys
- Sitemap pre-generation — cron-based background pre-generation (debounced 30s after content changes) ensures frontend requests are instant
- Sitemap batch queries —
_prime_post_caches()+update_post_meta_cache()eliminates N+1 queries - ETag + 304 — sitemaps support conditional requests
- Unified head builder —
HeadManager::collect()resolves all metadata (description, canonical, OGP, schema) in a single pass attemplate_redirect;outputHead()atwp_headoutputs the pre-built HTML with zero DB queries - Options bulk-loaded — all
cel_*options loaded in a single DB query, cached in memory per request - 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
- Versioned cache invalidation — sitemap caches invalidated by incrementing a version counter (no LIKE queries)
- Multisite-safe file cache — per-site cache directory (
cel-sitemaps/{blog_id}/) prevents cross-site collisions - 404 dedup —
ON DUPLICATE KEY UPDATEavoids row explosion - 404 rate limiting — transient-based per-minute rate limiter prevents log flooding
- 404 table cap — configurable max row count prevents unbounded growth
- 90-day auto-purge — cron-based cleanup of old 404 entries
- WP core sitemaps disabled — prevents duplicate sitemap systems
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 flagsdefined('GOOGLESITEKIT_VERSION')(orGoogle\Site_Kit\Pluginclass) 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(), andCIPHER_ALGOconstant 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 whenGOOGLESITEKIT_VERSIONis defined
Files changed:
celestial-sitemap.php— version bump to 3.7.1includes/Admin/ConflictDetector.php— Site Kit entry added to docblock anddetectConflicts()includes/Core/Options.php— deletedencrypt(),decrypt(),CIPHER_ALGOtests/OptionsTest.php— removedtest_encrypt_and_decrypt_support_plaintext_and_round_triptests/ConflictDetectorTest.php— new characterization testsREADME.md— Competing SEO Plugin Detection table expanded to list all detected integrations
New Features — IndexNow, search engine verification, timezone-aware sitemaps:
- IndexNow ping-on-publish — On every
draft → publishtransition, 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 ascel_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 throughesc_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:00for Asia/Tokyo) instead of UTC (Z). A newSitemapRouter::formatLastmod(?int)helper replaces all 10gmdate('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'sfeed_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 viacel_flush_rewrite_rulesflag)
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 debouncetests/SeoManagersTest.php— verification tag emission on front page, skip on non-front, XSS escape; RSS alternate link emission/suppression/escapingtests/SitemapRouterTest.php—formatLastmodtimezone offset and null-timestamp fallback,buildIndexsite-timezone lastmodtests/wp-stubs.php— new stubs forwp_date,wp_generate_password,wp_remote_post,wp_json_encode,wp_is_post_revision,wp_is_post_autosave,has_action, and extendedget_bloginfo
Files Added:
includes/Indexing/IndexNowPinger.phptests/IndexNowPingerTest.php
Files Modified:
celestial-sitemap.php— version bump to 3.7.0includes/Plugin.php— wiresIndexNowPingerincludes/Core/Activator.php— verification option defaults, IndexNow rewrite rule, removed GSC defaultsincludes/Core/Options.php—VERIFICATION_PROVIDERSconstant,verificationCode(),indexNowKey(); removed GSC accessors and encryptionincludes/SEO/HeadManager.php—buildVerificationTags(),buildFeedLink()includes/Sitemap/SitemapRouter.php—formatLastmod()helper replaces allgmdate('c')sitesincludes/Admin/AdminController.php— verification code save loop, dashboard ensures IndexNow key on render, removed GSC encryptiontemplates/admin/dashboard.php— Submit-to-Search-Console sub-section, Verification card, IndexNow paneltests/TestCase.php— resets IndexNow/verification globals between tests
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_typethrough 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_noindexand 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.3includes/Redirect/RedirectManager.php— large-table fallback matching, redirect validationincludes/Sitemap/SitemapRouter.php— taxonomy pagination, 404 sitemap responses, News fallback handling, filtered chunk discoveryincludes/Core/NotFoundLogger.php— row-cap update ordering, row-count cache invalidationincludes/SEO/CanonicalManager.php— static posts page canonical handlingincludes/SEO/HeadManager.php— OGP type fix for non-post singular contentincludes/Admin/AdminController.php— submenu asset hooks, redirect AJAXmatch_typeincludes/Admin/TaxonomyFields.php— taxonomy-specific capability checkstemplates/admin/redirects.php— redirect match-type UIassets/js/admin.js— redirect match-type submissiontests/*— regression coverage for all of the above fixes
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.2includes/Sitemap/SitemapRouter.php— fallback query ingetNewsUrls()for empty time-window results
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:newsandxmlns:imagenamespaces 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.1includes/Sitemap/SitemapRouter.php— conditional namespace declaration inbuildNewsSitemap()
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_txtfilter. 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:orDisallow: /wp-admin/directives (AdminController.php) - Physical file detection — Warns when a physical
robots.txtfile exists atABSPATH, 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_txtfilter output (AdminController.php) - State transitions —
enabled=falseor empty content falls back to WordPress default + sitemap URL append. Plugin deactivation removes the filter (viaboot()not being called). Plugin deletion removes both options via existingcel_%wildcard cleanup (RobotsManager.php,uninstall.php)
New Options:
cel_robots_txt_enabled(bool, default: 0) — enable/disable custom robots.txtcel_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.0includes/Core/Options.php—ALLOWED_KEYS+robotsTxtEnabled(),robotsTxtContent()gettersincludes/Core/Activator.php— default options forcel_robots_txt_enabled,cel_robots_txt_contentincludes/SEO/RobotsManager.php—filterRobotsTxt()extended to serve custom content when enabledincludes/Admin/AdminController.php—cel-robots-txtsubmenu,renderRobotsTxt(),detectRobotsTxtConflicts(),ajaxSaveRobotsTxt()with security checks and validationassets/js/admin.js— robots.txt form handler with warning display,showMessageselector updatedassets/css/admin.css—.cel-robots-textareamonospace styling
Bug Fix — Sitemap routing stability & plugin coexistence:
- Rewrite rules flush moved to
wp_loaded— Previously,flush_rewrite_rulesran atinit: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 atinit:10+. The flush now runs atwp_loaded, after allinitcallbacks have completed (SitemapRouter.php) - 404 sitemap response headers — Requests for non-existent sitemap types (e.g.,
/cel-sitemap-foobar.xml) now return properContent-Type: application/xml,X-Robots-Tag: noindex, andX-Content-Type-Options: nosniffheaders. Previously, the 404 path sent raw XML with defaulttext/htmlContent-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 causeflush_rewrite_rulesto 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
SitemapRouternow correctly describesinit:99URI-based serving andwp_loadedflush timing instead of the removedtemplate_redirectapproach (SitemapRouter.php)
Files Modified:
celestial-sitemap.php— version bump to 3.5.2includes/Sitemap/SitemapRouter.php— flush timing, 404 headers, self-healing guard, docblockincludes/Core/Migrator.php— comment update (init→wp_loaded)README.md— hook table updated (wp_loadedentry added), changelog
Bug Fix — Settings not reflected in sitemaps:
- Sitemap cache invalidation on settings save —
AdminController::ajaxSaveSettings()now callsSitemapRouter::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 inajaxSaveSettings()andOptions::set()no longer passfalseas the third argument. Previously, every settings save silently downgradedautoloadfromtrue(set byActivator::setDefaultOptions()) tofalsefor all options, increasing frontend DB queries on subsequent requests (AdminController.php,Options.php)
Files Modified:
celestial-sitemap.php— version bump to 3.5.1includes/Admin/AdminController.php— sitemap cache invalidation on save,update_option()autoload fixincludes/Core/Options.php—update_option()autoload fix inset()
New Features — Specialized Google Sitemaps:
- Google News sitemap (
cel-sitemap-news.xml) — Dedicated sitemap conforming to Google News sitemap spec. Primarily includesposttype 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. Includesnews:publication(name + language),news:publication_date,news:title, and optionalimage:imageextension 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. Usesimage:imageextension withimage:locandimage: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. Includesvideo:thumbnail_loc,video:title,video:description,video:player_loc, andvideo:publication_date. Paginated at 2,500 URLs per file (SitemapRouter.php)
Sitemap Index Integration:
cel-sitemap.xmlindex 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 fix —
update_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 fix —
batchLoadThumbnails()rewritten to use post→thumbnail mapping instead of thumbnail→post, correctly handling multiple posts sharing the same featured image strtotime()false guard —post_modified_gmtvalue'0000-00-00 00:00:00'no longer causes fatal error; falls back to current timeget_permalink()false guard — Posts returningfalsefromget_permalink()are safely skipped- XML control character stripping —
escXml()helper strips illegal XML 1.0 characters (\x00-\x08,\x0B,\x0C,\x0E-\x1F) usingesc_xml()(WP 5.5+) withesc_html()fallback - Defensive array checks —
is_array()guards on filter results and nested image/video arrays throughout sitemap builders
XSL Stylesheet Update:
sitemap-xsl.phpupdated withxmlns:newsandxmlns:videonamespace 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 arraycel_sitemap_video_urls— filter video sitemap URL arraycel_sitemap_image_urls— filter image sitemap URL array
Files Modified:
celestial-sitemap.php— version bump to 3.5.0includes/Sitemap/SitemapRouter.php— news/image/video sitemap builders, data fetchers, count helpers, routing, index integration, pre-generation,escXml(), runtime safety fixestemplates/sitemap-xsl.php— namespace declarations, badge rendering for news/video
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 renamed —
UltimateSEOPro\→CelestialSitemap\(all PHP files underincludes/) - Constant prefix renamed —
USP_*→CEL_*(CEL_VERSION,CEL_FILE,CEL_DIR,CEL_URL,CEL_BASENAME,CEL_MIN_WP,CEL_MIN_PHP) - Option key prefix renamed —
usp_*→cel_*(all 24 options inOptions::ALLOWED_KEYS) - Post/term meta prefix renamed —
_usp_*→_cel_*(_cel_title,_cel_description,_cel_canonical,_cel_noindex,_cel_og_image) - DB table prefix renamed —
usp_redirects→cel_redirects,usp_404_log→cel_404_log - Transient prefix renamed —
usp_*→cel_*(cel_sm_v{N}_*,cel_redirect_compiled,cel_404_rate_count,cel_404_row_count) - Cache directory renamed —
usp-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 renamed —
usp_sitemap→cel_sitemap,usp_sitemap_page→cel_sitemap_page,usp_xsl→cel_xsl - Cron events renamed —
usp_cleanup_404_log→cel_cleanup_404_log,usp_pregenerate_sitemaps→cel_pregenerate_sitemaps - AJAX actions renamed —
usp_save_settings→cel_save_settings,usp_flush_sitemap→cel_flush_sitemap,usp_add_redirect→cel_add_redirect,usp_delete_redirect→cel_delete_redirect,usp_delete_404→cel_delete_404,usp_clear_404→cel_clear_404 - Admin menu slugs renamed —
usp-dashboard→cel-dashboard,usp-redirects→cel-redirects,usp-404-log→cel-404-log - Asset handles renamed —
usp-admin→cel-admin - JS global renamed —
uspAdmin→celAdmin - Nonce action renamed —
usp_admin_nonce→cel_admin_nonce,usp_save_meta→cel_save_meta,usp_save_term_meta→cel_save_term_meta - All extensibility filters renamed —
usp_*→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 renamed —
usp-*→cel-*in all templates, CSS classes, and element IDs - Text domain renamed —
ultimate-seo-pro→celestial-sitemap - Main entry file renamed —
ultimate-seo-pro.php→celestial-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
Architecture — Unified Head Builder:
- Single-pass metadata generation —
HeadManager::collect()runs attemplate_redirectand resolves ALL head metadata (meta description, canonical, prev/next, OGP, Twitter Card, JSON-LD schema) in one pass.outputHead()atwp_headechoes 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_headindependently. ProvidesgetCanonicalHtml()returning canonical + prev/next as an HTML string, called by HeadManager during the collect phase (CanonicalManager.php) - SchemaManager refactored — no longer hooks
wp_headindependently. ProvidesgetSchemaHtml()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 cron —
cel_pregenerate_sitemapssingle 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 protection —
index.phpauto-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), andregex(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 column —
VARCHAR(10) NOT NULL DEFAULT 'exact'added to{prefix}cel_redirectstable with index. Backward-compatible: missing column detected viaSHOW COLUMNSand 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 cleanup —
glob()+array_map('unlink')pattern replaced withscandir()+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 unscheduling —
wp_clear_scheduled_hook()replaced with explicitwhile (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), sormdir()actually succeeds. Parentcel-sitemaps/directory removed on uninstall if empty (Deactivator.php,uninstall.php) - Cleanup symmetry —
Deactivator.phpanduninstall.phpnow perform identical cleanup: cron unscheduling, file cache removal, transient deletion (Deactivator.php,uninstall.php)
Files Modified:
celestial-sitemap.php— version bump to 3.3.0includes/Plugin.php— SchemaManager injected into HeadManager constructor; SchemaManager no longer callsregister()includes/Core/Options.php— random IV encryption, backward-compatible decryptionincludes/Core/Activator.php—match_typecolumn and index added to redirects tableincludes/Core/Deactivator.php— safe cleanup withscandir(), multisite-safe cache path, explicit cron unschedulingincludes/SEO/HeadManager.php— unified head builder:collect()attemplate_redirect,outputHead()atwp_headincludes/SEO/CanonicalManager.php—getCanonicalHtml()method, no directwp_headhookincludes/Schema/SchemaManager.php—getSchemaHtml()method, no directwp_headhookincludes/Sitemap/SitemapRouter.php— two-tier file cache, cron pre-generation, per-type cache, multisite-safe paths, safeflushFileCache()includes/Redirect/RedirectManager.php— compiled map with exact/prefix/regex, loop detection, regex validationuninstall.php— safe cleanup, multisite-safe cache path, parent directory cleanup
Performance — Critical (production-grade for 100k+ DAU):
- Sitemap N+1 query fix —
getPostTypeUrls()now batch-primes post caches via_prime_post_caches()andupdate_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 toautoload=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 everysave_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_limitoption (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 encryption —
cel_gsc_client_secretandcel_gsc_access_tokenare now encrypted at rest using AES-256-CBC with keys derived fromwp_salt(). Backward-compatible: existing plaintext values are read as-is and encrypted on next save (Options.php,AdminController.php) - Options key whitelist —
Options::set()now validates the key against an allowed list and throws\InvalidArgumentExceptionfor unknown keys (Options.php) - URL field validation hardened — Canonical URL and OG image meta fields now use
esc_url_raw()instead ofsanitize_text_field(), rejectingjavascript: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 disabled —
wp_sitemaps_enabledfilter set tofalseduring plugin boot to prevent two competing sitemap systems (Plugin.php) - Paginated prev/next fix —
CanonicalManager::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:urlcan be determined, avoiding invalid Open Graph markup (HeadManager.php) - URL-encoded redirect paths —
RedirectManager::normalise()now appliesrawurldecode()to match requests for encoded URLs (e.g.,/café/vs/caf%C3%A9/) (RedirectManager.php)
New Features:
- Facebook App ID —
fb:app_idmeta tag, configurable via Settings → Open Graph / Social (cel_fb_app_idoption) - Twitter/X site handle —
twitter:sitemeta tag, configurable via Settings → Open Graph / Social (cel_twitter_siteoption)
Robustness:
- Migration error handling —
Migrator::maybeRun()wrapped in try/catch. On failure, the version is NOT updated (migration retries on next request) and the error is logged whenWP_DEBUGis enabled (Migrator.php) - TRUNCATE replaced with DELETE —
NotFoundLogger::clearAll()now usesDELETE FROMinstead ofTRUNCATE TABLEto avoid requiringDROPprivilege on shared hosting (NotFoundLogger.php) - Multisite uninstall —
uninstall.phpnow iterates all sites viaget_sites()on multisite installations (uninstall.php)
Files Modified:
celestial-sitemap.php— version bump to 3.2.0includes/Plugin.php—wp_sitemaps_enabledfilterincludes/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 fixincludes/Core/NotFoundLogger.php— rate limiter, table cap, DELETE instead of TRUNCATEincludes/Core/Migrator.php— try/catch, conditional version updateincludes/Sitemap/SitemapRouter.php— batch cache priming, versioned cache keys,batchLoadThumbnails()includes/Redirect/RedirectManager.php—rawurldecode(), 3-tier cache strategyincludes/SEO/HeadManager.php— password protection check, empty og:url guard,fb:app_id,twitter:siteincludes/SEO/CanonicalManager.php—singularPageUrl()method,get_the_ID()validationincludes/Admin/AdminController.php—esc_url_raw()for URL fields, GSC encryption on save, new social settingstemplates/admin/dashboard.php— Facebook App ID and Twitter/X handle fieldsuninstall.php— multisite cleanup viaget_sites()
New Features:
- Term Meta Admin UI — SEO fields (title, description, canonical, noindex) on all public taxonomy add/edit screens via
TaxonomyFieldsclass - Competing SEO Plugin Detection — Admin notice when Yoast SEO, Rank Math, All in One SEO, or SEOPress is active simultaneously via
ConflictDetectorclass - Custom Post Type Schema — All public singular CPTs now receive WebPage structured data by default; override via
cel_schema_typefilter - 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_titlefilter applied infilterTitleParts()for full context coverage (singular, front page, taxonomy)resolveDescription()result cached per request to prevent double filter executioncel_canonicalterm field usesesc_url_raw()for proper URL sanitization- Admin CSS enqueued on taxonomy screens (
edit-tags.php,term.php)
Files Added:
includes/Admin/TaxonomyFields.phpincludes/Admin/ConflictDetector.php
Files Modified:
celestial-sitemap.php— version bump to 3.1.0includes/Plugin.php— wires TaxonomyFields and ConflictDetectorincludes/SEO/HeadManager.php— filters + description cachingincludes/SEO/CanonicalManager.php— pagination strategy + filterincludes/SEO/RobotsManager.php— filterincludes/Schema/SchemaManager.php— CPT support + filtersincludes/Sitemap/SitemapRouter.php— filtersincludes/Admin/AdminController.php— taxonomy screen CSS enqueue
- Initial release with title/meta/canonical/OGP/Schema/Sitemap/Redirect/404 log
- Internal link analysis — requires content parsing; planned as background cron job
- WooCommerce — product schema not implemented (use
cel_schema_typefilter with'Product') - Multilingual (WPML/Polylang) — hreflang sitemap extension not yet active
- Bulk editor — per-post SEO fields editable only in post editor, not quick-edit
- Google Search Console API — Google does not participate in IndexNow; GSC submission remains manual (via the Dashboard's "Submit to Search Console" URL)