noindex SEO

Plugin banner

Control search engine indexing with robots directives using HTML meta tags or HTTP headers.


Plugin icon

Information

Author:

ROBOTSTXT

Download

Price:

€0.00
Get plugin ZIP file

This ZIP installs like any WordPress plugin. To keep it updated automatically, install the Manager (by ROBOTSTXT) plugin on your site and connect it with the API key from your ROBOTSTXT account.


Requirements

Version:

3.7.0

WordPress:

7.0

–

7.2

PHP >=

8.3

Changelog

3.7.0

Release date: 2026-11-03

Highlights

  • Import from other SEO plugins (spec section 5): on-demand migration of the noindex / nofollow flags stored by Yoast SEO, Rank Math, SEOPress and All in One SEO into noindex SEO per-post and per-term overrides — detected from data presence (the source plugin does not need to be installed), user-confirmed, and never overwriting content that already has its own override.
  • Virtual robots.txt management (spec section 7): block (Disallow: /) or allow (Allow: /) individual crawlers, appended to the robots.txt WordPress generates virtually — never replacing it — with one-click presets for the well-known AI and search-engine crawlers. Inactive by default.

Added

  • Import from other plugins. A new section on the Import / Export tab lists every supported source plugin together with how many posts and terms carry its robots flags, and imports the checked ones on explicit confirmation. The runner converts source flags into granular overrides (_noindex_seo_override=1 plus noindex / nofollow), skips and reports content that already carries a noindex SEO override row (an explicit “off” counts as data), re-verifies LIKE-based matches (Rank Math’s serialized arrays) per post so false positives are dropped, and whitelists source slugs server-side. Sources are data-driven and filterable (noindex_seo_import_sources) so third parties can register or correct one.
  • Robots.txt tab. A fourth settings tab manages up to 30 bot rules (User-agent token + block/allow policy; wildcard supported, duplicates collapse, agents stripped to the robots.txt-legal charset). The rules are appended through core’s robots_txt filter only while the feature is enabled, a live preview shows the exact block appended, and preset buttons add the well-known AI crawlers (GPTBot, ClaudeBot, Google-Extended, CCBot, Bytespider… — filterable via noindex_seo_ai_bots) or the main search engine crawlers (noindex_seo_search_bots) as blocked rules. The core output — and a physical robots.txt or “Discourage search engines” — always takes precedence.
  • The robots.txt settings ride the consolidated option and the JSON export/import.

Changed

  • The settings page gained a fourth tab (General / SEO Settings / Import-Export / Robots.txt); the rule-table JavaScript is now shared between the conditional rules and the robots.txt tables, scoped per container.

Fixed

  • Block editor granular overrides now save on custom post types registered without custom-fields support (a very common CPT registration). Without that support the REST API exposes no meta field for the post type, so the editor sidebar’s saves were silently dropped with an HTTP 200 — the override checkbox appeared to do nothing. The plugin now adds custom-fields support for exactly the post types it registers meta on (while per-post granular control is enabled).

Security

  • No new attack surface: the import handler is manage_options + nonce gated with whitelisted sources, writes only through the meta API after re-reading each source flag, and skips content with existing override data; robots.txt agents are charset-stripped (no newline or comment injection into the file) and policies are whitelisted; the tab form rides the existing nonce + capability path. PHPCS/PHPStan/composer audit clean.

Compatibility

  • WordPress: 7.0 – 7.2
  • PHP: 8.3 – 8.6

Tests

  • PHPUnit: 9.6.38 (230 tests, 1152 assertions, all passing) on the WordPress 7.2-beta3 test library, run under PHP 8.3, 8.4, 8.5 and 8.6.0RC3 — including the new robots.txt sanitization/output/filter/form suites and the import detection/conversion/never-overwrite/false-positive/handler suites.
  • PHPCompatibility: 0 errors on the declared range 8.3-8.6 and on the full review range 5.6-8.6.
  • PHPCS (WordPress-Core, -Docs, -Extra) and PHPStan level 9: 0 errors.
  • Translations: Catalan and Spanish catalogs regenerated and 100% complete (209 strings each).

3.6.0

Release date: 2026-11-03

Highlights

  • Modern robots directives (TODO #5): notranslate, max-snippet, max-image-preview, max-video-preview and unavailable_after join the five existing booleans — on every surface: the global context grid, per-post and per-term granular overrides, Quick Edit, the block editor sidebar, the REST /effective endpoint and WP-CLI (name=value tokens).
  • Conditional auto-noindex rules (TODO #11): apply noindex automatically to any post whose custom field (for example an ACF field) exactly matches a value; matched posts also drop out of the core XML sitemap.
  • Multisite behavior documented (TODO #9, docs variant): per-site by design, with an export/import-based rollout recipe.

Added

  • Value directives everywhere. Boolean directives are flags; value directives carry a value and render as name:value (max-snippet:-1, max-image-preview:large, unavailable_after: Tue, 01 Jan 2030 09:00:00 +0000 in site time with a numeric UTC offset). They are configured with typed inputs (number, size select, datetime-local) on the settings grid, the post meta box, Quick Edit, the term panel and the Gutenberg sidebar; stored in the consolidated option (contexts) and in _noindex_seo_{directive} post/term meta; registered with the REST API (register_post_meta/register_term_meta with per-type sanitization); exported/imported with normalization; and supported by WP-CLI — wp noindex-seo set archive max-snippet=-1 max-image-preview=large, wp noindex-seo unset archive max-snippet (plain name clears the value), and list reports the configured value (max-snippet:-1) or off.
  • notranslate as the sixth boolean directive, including a matching SVG icon in the plugin’s icon registry and membership in the “Apply Recommended Defaults” set (which enables the six booleans; value directives stay untouched — “enabling” them without a value is meaningless).
  • Conditional rules (TODO #11). A “Conditional rules” card on the SEO Settings tab manages up to 20 exact-match rules (custom field name + value; rows with empty field/value are ignored, and the plugin’s own _noindex_seo_ meta keys are rejected). Rules run on the front-end below an explicit per-post override and above per-CPT/global defaults; matched posts surface as source: auto_rule in /effective and are excluded from the pages sitemap. The rules read plain post meta, so no ACF installation is required.
  • A notranslate SVG directive icon, plus badge support for value directives in the Robots columns (max-snippet:-1 tokens).

Changed

  • noindex_seo_metarobots() now takes a directive map (name => true|value); a flat list of boolean names (the pre-3.6.0 third-party calling convention) is still accepted and normalized. The wp_robots integration passes directive values through to core ($robots['max-snippet'] = '-1').
  • noindex_seo_get_setting() returns the normalized string value for value directives (” = off) instead of coercing everything to 0/1; the hourly transient cache stores values the same way.
  • The WP-CLI set/unset default (no directive arguments) now enables/disables the six boolean directives; value directives always require explicit name=value tokens (a value on a boolean, or an invalid/missing value, is rejected with a descriptive error). Datetime values need quoting or a T separator because of the space: unavailable_after=2030-01-01T09:00.
  • Block editor sidebar labels dropped their emoji prefixes (matching the 3.5.0 admin UI icon direction), and the sidebar gained controls for all five new directives.

Fixed

  • Nothing.

Security

  • No new attack surface: rules and value directives ride the existing nonce + capability paths (settings form: manage_options + check_admin_referer; meta box/Quick Edit: edit_post; term panel: manage_categories); every value normalizes through a single whitelist-driven normalizer (integers ≥ -1, a none/standard/large enum, a parse-validated datetime); rule fields strip everything but [A-Za-z0-9_-] and values go through sanitize_text_field(); sitemap rule clauses use WP_Query’s meta_query (prepared by core). PHPCS/PHPStan/composer audit clean.

Compatibility

  • WordPress: 7.0 – 7.2
  • PHP: 8.3 – 8.6

Tests

  • PHPUnit: 9.6.38 (216 tests, 1107 assertions, all passing) on the WordPress 7.2-beta3 test library, run under PHP 8.3, 8.4, 8.5 and 8.6.0RC3 — including the new registry/normalization, settings-merge, form-roundtrip, front-end value output, granular value overrides, REST formatted tokens, CLI name=value, rules sanitization/matching/precedence/sitemap/auto_rule suites.
  • PHPCompatibility: 0 errors on the declared range 8.3-8.6 and on the full review range 5.6-8.6.
  • PHPCS (WordPress-Core, -Docs, -Extra) and PHPStan level 9: 0 errors.
  • Translations: Catalan and Spanish catalogs regenerated and 100% complete (188/187 strings), including the previously untranslated strings found during the merge.

3.5.0

Release date: 2026-11-03

Highlights

  • WooCommerce special-page contexts (TODO #8): dedicated directives for the WooCommerce Cart, Checkout and My Account pages, detected through the page-ID options (no WooCommerce code needed), recommended by default, and mirrored across the front-end, the REST /effective endpoint and the core XML sitemaps.
  • Compatibility window narrowed to WordPress 7.0 – 7.2 and PHP 8.3 – 8.6: Requires at least raised from 4.7 to 7.0, Requires PHP raised from 5.6 to 8.3, Tested up to set to WordPress 7.2 (verified on 7.2-beta3) and PHP support documented through 8.6 (verified on 8.6.0RC3).
  • Fixed wp noindex-seo list, which fatalled on every real WP-CLI invocation since 3.4.0 (nonexistent static API call; --context filter renamed to --only to avoid WP-CLI 2.11+’s reserved global flag).
  • Directive icons modernized for the SVG era: on WordPress 7.2+ the five directive icons render through the core Icons API — the same system WordPress 7.2 uses for its own admin chrome — while WordPress 7.0 – 7.1 keep the legacy emoji glyphs as a retrocompatible fallback.

Added

  • WooCommerce contexts (woo_cart, woo_checkout, woo_myaccount): the Cart, Checkout and My Account pages are regular pages that conventionally deserve noindex but previously fell into the generic page bucket. Each now has its own context with all five directives, a “WooCommerce” section on the SEO Settings tab (rendered only when the store pages are configured), and Recommended badges — the three contexts also joined the “Apply Recommended Defaults” set (now 15 contexts).
  • Detection is option-driven (woocommerce_cart_page_id, woocommerce_checkout_page_id, woocommerce_myaccount_page_id): no WooCommerce class or function dependency; without WooCommerce the contexts are inert. My Account endpoints (order history, addresses) and the checkout order-pay / order-received endpoints share their parent page’s ID, so they are covered automatically.
  • Precedence: the dedicated contexts run before the generic page context on the front-end and in /wp-json/noindex-seo/v1/effective, so a Woo page with directives gets them instead of the page-level ones; with no dedicated directives the page context still applies.
  • Sitemap: special pages whose context has noindex=1 drop out of the pages sitemap even when the page context is not noindexed.
  • SVG directive icons (retrocompatible). The five robots-directive icons render as color-adaptive 16px SVGs through the WordPress Icons API (wp_register_icon() / wp_get_icon()), registered in a private noindex-seo icon collection — everywhere they appear: the settings grid, the post meta box, Quick Edit, and the post and taxonomy list-table Robots columns. The SVG path is gated to WordPress 7.2+, whose icon sanitizer understands the stroke-based designs (rect/circle, stroke attributes) and whose registry supports private collections; on WordPress 7.0 – 7.1 the legacy emoji glyphs remain (7.1’s narrower sanitizer would strip the SVGs to unstyled fragments), so rendering stays correct on every supported version — and consistent on every OS, unlike the emoji (which each platform draws differently). Follows the core direction of replacing admin dashicons with SVG (wordpress-develop#12270, landed in WP 7.2’s admin chrome). The Quick Edit label strings dropped their emoji prefix; the plain directive names were already in the translation catalogs, so no translations were lost.

Changed

  • Compatibility window: WordPress 7.0 – 7.2 (was 4.7 – 7.2), PHP 8.3 – 8.6 (was 5.6 – 8.5). The full-range PHPCompatibility scan (PHP 5.6 – 8.6) still passes clean — the code keeps its PHP 5.6-parseable syntax — but the declared and supported window is now 7.0 / 8.3 and up, per the product support policy. The runtime guards for WordPress APIs older than 7.0 remain in place as harmless no-ops and may be removed in 4.0.
  • Tooling aligned with the new window: phpcs.xml testVersion 8.3-8.6 and minimum_supported_wp_version 7.0, phpstan.neon phpVersion 80300, bin/preflight.sh gates scanning the declared range, composer.json PHP constraint >=8.3, and the obsolete WPCompat. PHPStan ignores removed (every guarded hook exists since 7.0).
  • Development dependencies refreshed via composer update: PHPUnit 9.6.38, PHPStan 2.3.5, wordpress-stubs 7.1.2 (no CVEs; composer audit clean).
  • The “Apply Recommended Defaults” set grew from 12 to 15 contexts (button title and description updated; existing configurations are untouched — the button is opt-in).

Fixed

  • wp noindex-seo list was broken on real WP-CLI installs since 3.4.0: the output call went to WP_CLIUtils::format_items() (a static method that does not exist — the real API is the namespaced WP_CLIUtilsformat_items() function), and the --context filter parameter collided with the --context global flag reserved by WP-CLI 2.11+, so the value never reached the handler. The filter is now --only= and the command renders through the real function API. The unit-test stub was aligned with the real WP-CLI surface (namespaced function, not a Utils class), and every wp noindex-seo command was smoke-tested against the real WP-CLI 2.12 phar.

Security

  • No security changes; the new contexts reuse the consolidated settings storage and existing sanitization paths, and PHPCS/PHPStan/composer audit stay clean.

Compatibility

  • WordPress: 7.0 – 7.2
  • PHP: 8.3 – 8.6

Tests

  • PHPUnit: 9.6.38 (198 tests, 817 assertions, all passing) on the WordPress 7.2-beta3 test library, run under PHP 8.3, 8.4, 8.5 and 8.6.0RC3 — including the icon registration, SVG rendering, emoji fallback and admin-markup tests.
  • PHPCompatibility: 0 errors on the declared range 8.3-8.6 and on the full review range 5.6-8.6.
  • PHPCS (WordPress-Core, WordPress-Docs, WordPress-Extra) and PHPStan level 9: 0 errors.

3.4.0

Release date: 2026-09-19

Highlights

  • WP-CLI support (TODO #7): a wp noindex-seo command namespace with list, set, unset, export, import and bulk per-post overrides — automation-friendly and scaling past the ~200-post ceiling of the bulk UI.

Added

  • WP-CLI commands. wp noindex-seo list [--context=] [--format=table|csv|json|yaml] (every context × directive pair with its status), wp noindex-seo set [...] / wp noindex-seo unset [...] (default: all five directives; saving clears the transient cache), wp noindex-seo export [] (JSON payload to STDOUT or a file, same as the tools tab), wp noindex-seo import [--dry-run] (same validation and sanitization as the import form; --dry-run reports the number of values that would change), and wp noindex-seo override ... [--off] (bulk per-post granular overrides without the UI’s selection limit; --off clears the override and directive meta). The commands enforce the same rules as the settings page: unknown contexts/directives are rejected and the attachment context refuses to enable while the implementation method is meta-only.
  • stubs/wp-cli-stubs.php: minimal WP-CLI signatures for PHPStan and a recording stub for the test suite (never shipped; excluded from the release ZIP and from PHPCS).
  • The undefined “migrate-attachments” command from the original TODO was replaced by the general-purpose override bulk command, which covers the same need for every post type.

Changed

  • Nothing.

Fixed

  • Nothing.

Security

  • No security changes; the CLI handlers reuse the existing validated/sanitized helpers, operator-supplied file paths are checked with is_readable() before reading, and PHPCS/PHPStan/composer audit stay clean.

Compatibility

  • WordPress: 4.7 – 7.2
  • PHP: 5.6 – 8.5

Tests

  • PHPUnit: 9.6.36 (185 tests, all passing): command registration, set/unset round-trips (including transient invalidation and defaults), unknown context/directive rejection, the attachment header-only invariant, list rows and filtering, export to STDOUT and file, import dry-run/apply/rejection, and override enable/disable/skip-missing.

3.3.0

Release date: 2026-09-19

Highlights

  • The settings page is reorganized with the native WordPress tab navigation (General / SEO Settings / Import-Export) and titled sections, built exclusively on core nav-tab classes with URL-based switching — no proprietary tab CSS or JavaScript.

Added

  • Native WordPress tab navigation on the settings page (nav-tab-wrapper / nav-tab, the same pattern as the core Updates and Plugins screens): General (implementation method and configuration options), SEO Settings (the directive grid), and Import / Export (the JSON tools). Tabs switch through the tab query parameter and the active panel renders server-side, so they work without JavaScript and are bookmarkable.
  • Titled sections inside the tabs: “Recommended settings” (one-click defaults), “Overview” (the number cards) and “Page contexts” (the directive grid) on SEO Settings; “Export settings” and “Import settings” headers on the Import / Export tab. Spacing added between the tab bar and the content below.

Changed

  • noindex_seo_process_form() is now tab-scoped: saving from the General tab updates only the configuration and preserves the directive grid, saving from SEO Settings updates only the directives and preserves the configuration — hidden tabs can no longer reset each other’s values. The form carries a noindex_seo_form_scope marker and the redirect returns to the tab the user saved from.
  • The header-only restriction for the attachment context now follows the saved implementation method (the method select lives on the General tab), and switching the method to meta/HTML-only from General zeroes the attachment directives to keep the invariant consistent.
  • The apply-suggestions redirect and the import success/error redirects return to their source tabs (tab=settings, tab=tools).
  • Removed the dead custom tab machinery: the .noindex-seo-tab CSS rules, the initTabs() JavaScript with its localStorage persistence, and the unused .noindex-seo-apply-note class (previously flagged as dead code by the 3.1.6 deploy audit).

Fixed

  • Nothing.

Security

  • No security changes; the tab parameter is strictly safelisted server-side (sanitize_key + allow list) and PHPCS/PHPStan/composer audit stay clean.

Compatibility

  • WordPress: 4.7 – 7.2
  • PHP: 5.6 – 8.5

Tests

  • PHPUnit: 9.6.36 (172 tests, all passing): tab markup (three tabs, active class, unknown-tab fallback), per-tab rendering isolation, scope-aware saving (config-only / grid-only / preserve-other-side / attachment invariant / tab-aware redirect), and section ordering.

3.2.0

Release date: 2026-09-18

Highlights

  • Two roadmap features: JSON export/import of the settings (#6) and runtime conflict verification against the wp_robots API (#10).

Added

  • Export / Import of settings (JSON). Two new controls on the settings page: “Export settings” downloads the consolidated settings as a JSON file (noindex-seo-settings-{date}.json), and “Import settings” restores the global settings from a previously exported file. Imports are strictly validated and sanitized: plugin marker check, 1 MB size limit, is_uploaded_file() verification, the upload is never persisted to the uploads directory, every directive is normalized to 0/1, config values go through the same safelists as the settings form, and contexts that do not exist on the importing site (e.g. custom post types from another install) are dropped. Per-post and per-term granular overrides are content data and are never touched by an import. Both actions require manage_options and a nonce; success and error notices survive the redirect.
  • Runtime conflict verification (TODO #10). When no known SEO plugin conflicts, noindex_seo_detect_conflicts() now also inspects the callbacks actually registered on the wp_robots filter (WordPress 5.7+): anything that is not a core callback or one of ours — a foreign function, a class method, or a closure from a theme or unknown plugin — produces a dismissible admin notice naming the detected callback. The core callback safelist is filterable via noindex_seo_core_robots_callbacks. The notice honours the existing “disable compatibility warnings” opt-out.

Changed

  • Nothing.

Fixed

  • Nothing.

Security

  • No security changes; the new endpoints follow the canonical checklist (capability + nonce on both actions, strict input validation, safelisted config values, no persistence of uploads) and PHPCS/PHPStan/composer audit stay clean.

Compatibility

  • WordPress: 4.7 – 7.2
  • PHP: 5.6 – 8.5

Tests

  • PHPUnit: 9.6.36 (165 tests, all passing): export payload shape and JSON round-trip, import sanitizer (happy path, normalization, unknown-context dropping, malformed-payload rejection), handler guards, foreign-callback detection (functions, class methods, closures, own-prefix skipping, filterable safelist) and the runtime notice.

3.1.6

Release date: 2026-09-18

Highlights

  • Admin UI modernization: all inline styles removed in favour of WordPress admin color-scheme-aware, RTL-ready classes (audit finding M7), plus the two minor items deferred from the 3.1.5 deploy audit (stale wp_die message, unreachable REST fallbacks).

Added

  • Regression tests locking in the new UI conventions: the settings page and meta box render without any style="..." attribute, and the stylesheet is verified to load on the post-list / post-edit / term-edit screens (gated by the granular flags) while staying off everything else.

Changed

  • All 61 inline style="..." attributes removed from the settings page, post meta box, term edit panel, Quick Edit, and the post/term list “Robots” columns. Styling now lives in assets/css/admin.css under semantic class names.
  • Admin styling now follows the WordPress admin color scheme: accents use var(--wp-admin-theme-color, #2271b1), status boxes and badges use the core admin palette, and neutral grays match wp-admin. The UI adapts to Light / Blue / Midnight / Coffee / Ocean / etc. instead of hardcoded Material-Design colors.
  • RTL support: layout uses logical CSS properties (margin-inline-start, border-inline-start) and the toggle switch mirrors under [dir="rtl"].
  • Dynamic visibility (override toggles in the meta box and term panel) uses the core .hidden class with classList.toggle() instead of inline display manipulation.
  • assets/css/admin.css also enqueues on the admin screens where the plugin renders UI (post list, post editor, term editor), gated by the granular-control flags; the interactive JavaScript remains settings-page-only.
  • Admin stylesheet cleanup: duplicate .noindex-seo-view-link rule block removed; keyframe names prefixed to avoid collisions.

Fixed

  • Stale wp_die() message in the two form handlers: after the 3.1.5 nonce split only the capability path is reachable there, so the message is now “Permission denied.” instead of “Permission denied or invalid nonce.” (deploy-audit finding I2). Catalan and Spanish translations and the .pot template updated; .mo files recompiled.
  • Unreachable function_exists( 'rest_authorization_required_code' ) ternaries removed from both REST permission callbacks — the function exists on every supported WordPress version (deploy-audit finding I5).

Security

  • No security changes; the full canonical checklist (AGENTS-security.md), PHPCS security sniffs and composer audit remain clean.

Compatibility

  • WordPress: 4.7 – 7.2
  • PHP: 5.6 – 8.5

Tests

  • PHPUnit: 9.6.36 (150 tests, all passing). Coverage maintained above the 80% floor.

3.1.5

Release date: 2026-09-18

Highlights

  • Maintenance review: composer update, code review, security review and compatibility review (WordPress 4.7 – 7.2, PHP 5.6 – 8.5). Declared WordPress floor corrected to the real tested minimum (4.7). Test suite expanded to 147 tests with 94.78% line coverage, meeting the 80% AGENTS target.

Added

  • 76 new PHPUnit tests: front-end output (noindex_seo_metarobots, all major noindex_seo_show contexts, overrides, CPT defaults, transient shortcut), admin surface (settings page, meta boxes, Quick Edit, bulk actions, columns, filters, enqueues, conflict detection, helpers), sitemap per-term exclusion, and uninstall cleanup.

Changed

  • Minimum WordPress requirement raised from 4.0 to 4.7 in the plugin header, readme.txt and phpcs.xml (minimum_supported_wp_version). Version 4.7 is the oldest release in the tested range and the first with the complete REST API, so every advertised feature (including the /wp-json/noindex-seo/v1/ endpoints) is available on all supported versions. Newer-API integrations keep degrading gracefully above the floor (wp_robots 5.7+, sitemaps 5.5+, block editor 5.0+, register_post_meta, term meta).
  • Tested up to WordPress 7.2 in readme.txt.
  • Removed three now-obsolete WPCompat PHPStan ignores (rest_api_init action, WP_REST_Request::get_param() × 2): with the 4.7 floor those APIs always exist, and PHPStan reports unmatched ignores as errors.
  • Dev dependencies updated via composer update (phpstan/phpstan 2.2.8 → 2.2.14, nikic/php-parser 5.8.0 → 5.9.0, php-stubs/wordpress-stubs 6.9.4 → 7.1.0, szepeviktor/phpstan-wordpress 2.0.3 → 2.0.4, wp-hooks/wordpress-core 1.12.0 → 1.13.0). No production code ships these packages.
  • Coverage regression floor in bin/preflight.sh raised from 25% to 80% now that the suite meets the AGENTS target.

Fixed

  • Dead nonce check in noindex_seo_process_form() and noindex_seo_process_apply_suggestions(): ! check_admin_referer() with a specific action can never be true (the function exits on failure instead of returning false), so the || branch was dead code flagged by PHPStan level 9 with the updated WordPress stubs. Both handlers now check current_user_can( 'manage_options' ) first and call check_admin_referer() separately, which dies on an invalid nonce. No behavior change for valid requests.

Security

  • Full canonical checklist (AGENTS-security.md) reviewed against the release diff: no findings. Input validation (safelist + strict types), sanitization (sanitize_text_field, absint, sanitize_key), late output escaping, nonces on every state-changing action, capability checks in UI and execution logic, $wpdb->prepare() on all custom SQL, no dangerous functions, REST permission_callbacks intact, no secrets in code. composer audit clean.
  • The nonce-handler fix above is hardening only: the previous code was still safe in practice because check_admin_referer() dies on failure.

Compatibility

  • WordPress: 4.7 – 7.2
  • PHP: 5.6 – 8.5

Tests

  • PHP Coding Standards: 3.13.6
  • WordPress Coding Standards: 3.4.1
  • PHPUnit: 9.6.36 (147 tests, 569 assertions — all passing on WordPress 7.1.1 test library; staging site runs WordPress 7.2-alpha, PHP 8.5.10, MariaDB 11.8.9)
  • PHPCompatibility 5.6-8.5 scan: clean (declared PHP floor 5.6 confirmed)
  • PHPStan level 9: clean; bin/preflight.sh: full PASS (coverage 94.78%, above the 80% AGENTS target and floor)

3.1.3

Release date: 2026-08-17

Highlights

  • Compatibility release: WordPress 4.0+ and PHP 5.6+ supported again (declared floors now match the real compatibility of the code), and branding moved to robotstxt.software.

Added

  • Classic ` output on wp_head for WordPress versions older than 5.7, restoring the pre-wp_robots fallback so the declared WordPress 4.0 floor is real. On WordPress 5.7+ the directives keep flowing through the core wp_robots` API.
  • noindex_seo_get_cpt_label() helper returning a safe display label for custom post types on the settings page.

Changed

  • Minimum requirements lowered back to WordPress 4.0 and PHP 5.6 in the plugin header, readme.txt, composer.json and phpcs.xml, verified with a full-range PHPCompatibility 5.6-8.5 scan (clean) and the WP-Compat report.
  • Plugin URI and Author URI now point to robotstxt.software.
  • register_setting() calls use the two-argument form: the $args array parameter requires WordPress 4.7+, and all sanitization already happens in the admin-post form handler.

Fixed

  • Potential fatal errors on older WordPress versions: is_privacy_policy() (WP 5.2+), is_customize_preview(), get_term_meta() (WP 4.4+), register_post_meta(), register_term_meta(), wp_set_script_translations() (WP 5.0+) and WP_Screen::is_block_editor (WP 5.0+) are now guarded; the features that depend on them degrade gracefully instead of crashing.
  • Removed PHP 7-only syntax (strict types, scalar type hints, return type declarations, null coalescing operator) so the code parses and runs on PHP 5.6 as declared.
  • Restored the repository governance files (CLAUDE.md, .claude/settings.json) that were accidentally dropped from the repository during the 3.1.2 release cleanup.
  • Bundled translations (Catalan and Spanish) regenerated against the current source: 9 strings added in 3.0.2–3.1.0 are now translated, 7 fuzzy entries reviewed, 149 messages complete per locale with 0 untranslated and 0 fuzzy.

Security

  • No security changes; the 5.6-8.5 PHPCompatibility scan, the dependency audit and the WordPress Coding Standards security sniffs are clean.

Compatibility

  • WordPress: 4.0 – 7.1
  • PHP: 5.6 – 8.5

Tests

  • PHP Coding Standards: 3.13.6
  • WordPress Coding Standards: 3.4.1
  • PHPUnit: 9.6.36 (71 tests, 385 assertions)

3.1.2 [2026-08-10]

Audit-driven maintenance release.

Security

  • Fixed the composer-audit gate in bin/preflight.sh. The previous version grepped for “security vulnerability advisory” (singular), which never matched Composer’s actual output “security vulnerability advisories” (plural) — the y→i pluralization change silently disabled CVE detection, green-lighting every release regardless of known vulnerabilities. The gate now uses Composer’s bitmask exit code (bit 0 = security), which is immune to wording changes.
  • Bumped squizlabs/php_codesniffer to ^3.13.6 and wp-coding-standards/wpcs to ^3.4.1, fixing CVE-2026-67434 (OS command injection) and CVE-2026-45293 (arbitrary code execution) in the development tooling. These are dev-only dependencies and were never shipped in the release ZIP.

Fixed

  • Nested ` elements on the settings page. The "Apply Recommended Defaults" form was physically nested inside the main settings form — HTML5 forbids this, and the stray ` could close the main form early, orphaning the “Save Changes” button and all directive checkboxes. The apply-suggestions form is now a standalone sibling rendered before the main form.
  • Meta-box “Effective Directives” reference panel now reads from the consolidated noindex_seo_settings option via noindex_seo_get_setting() instead of the stale legacy individual options that may diverge after a settings save.

Added

  • load_plugin_textdomain('noindex-seo') hooked on init. The plugin ships .mo files for Catalan and Spanish (es_ES) in languages/, but without this call WordPress only loaded translations from wp-content/languages/plugins/ — the bundled translations were effectively dead code.
  • PHPUnit coverage measurement in bin/preflight.sh (requires Xdebug). Enforces a 25% regression floor against the current 26.64% baseline; the AGENTS target remains 80%.
  • ` configuration in phpunit.xml so –coverage-text` produces a report.
  • README.md for developer and contributor onboarding.
  • tests/wp-tests-config.sample.php template. The real tests/wp-tests-config.php is now gitignored and reads from environment variables.

Changed

  • Pinned all remaining wildcard "" dev dependencies to caret constraints in composer.json (phpcsutils ^1.2, php-compatibility ^9.3, dealerdirect/phpcodesniffer-composer-installer ^1.2). Unpinned ranges were how the CVE-affected versions were resolved.
  • Lowered phpstan.neon phpVersion from 80500 (PHP 8.5) to 80200 (PHP 8.2) so PHPStan flags any accidental use of PHP 8.3+ syntax that would break on the development floor.
  • Refreshed CLAUDE.md to reflect 3.1.x reality (version, line count, v3 consolidated storage model, four-priority output logic, current 8-file test suite). Documented the uninstall direct-DB exception.
  • Updated TODO.md to mark shipped items (#1–#4).

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • 71 tests, 385 assertions — all passing. Coverage measured at 26.64% (471/1768 lines).

3.1.1 [2026-07-18]

Security

  • Tightened the ACL on the two REST endpoints introduced in 3.1.0. The original release used __return_true for both permission_callbacks, which would have leaked the full noindex configuration to anyone hitting /wp-json/noindex-seo/v1/settings. Fixed callbacks:
  • /settings now requires manage_options. The consolidated array reveals the site’s full SEO strategy in a single response, so it is admin-only.
  • /effective?post_id=N is public only for posts with post_status=publish and no password (the directives for those would appear in the HTML ` tag of the rendered page anyway). Drafts, private posts, scheduled posts, password-protected posts, and trashed posts require edit_post` capability for the specific post.

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • Added 4 tests covering the new permission model: unauthenticated rejected on /settings, editor rejected on /settings, unauthenticated/subscriber rejected on /effective for drafts, public access preserved on /effective for published posts.

3.1.0 [2026-07-18]

Closes the three remaining HIGH-priority items from the original gap audit.

Added

  • WP core XML sitemap integration. Hooks wp_sitemaps_posts_query_args, wp_sitemaps_taxonomies_query_args, and wp_sitemaps_users_query_args so URLs that should not be indexed drop out of /wp-sitemap.xml. Two layers of exclusion: (1) whole-type exclusion — if singular.noindex=1 hides every post type; single.noindex=1 hides all posts; page.noindex=1 hides all pages; attachment.noindex=1 hides all attachments; cpt_{type}.noindex=1 hides that CPT; category.noindex=1 / tag.noindex=1 hide those taxonomies; author.noindex=1 hides the entire users sitemap. (2) per-item exclusion — posts/terms with the granular _noindex_seo_override=1 + _noindex_seo_noindex=1 flag are excluded from their type’s sitemap. Closes the biggest SEO-quality gap (Google Search Console flags noindexed URLs in sitemaps as a coverage warning).
  • Per-CPT global defaults. Every public custom post type (anything public + non-built-in: WooCommerce product, EDD download, LMS lesson/course, etc.) now gets its own context cpt_{post_type} and a “Custom Post Types” section in the settings page. Previously every CPT single fell into the generic single bucket; now each CPT can have its own noindex / nofollow / noarchive / nosnippet / noimageindex defaults that apply globally to all of its items.
  • REST API exposure. Two new read-only endpoints under /wp-json/noindex-seo/v1/:
  • GET /settings — returns the consolidated settings array (contexts × directives + config), so headless consumers (Next.js, Gatsby, mobile apps) can read the global configuration.
  • GET /effective?post_id=123 — returns the active directive list for a specific post, computed by the same precedence used on the front-end (per-post override → per-CPT default → built-in single/page/attachment context). Includes the implementation method so consumers know whether to render a ` tag, an X-Robots-Tag` header, or both.

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • Added tests/test-sitemap.php (per-post + global-context exclusion for posts/terms/users sitemaps).
  • Added tests/test-rest-api.php (settings endpoint, effective endpoint for post-meta and CPT-default sources, 404 for unknown post).
  • Added tests/test-cpt-contexts.php (dynamic context registration, per-CPT settings application).

3.0.4 [2026-07-18]

Changed

  • Moved the “Apply Recommended Defaults” button from above the Search Results section to immediately above the “Search options…” input box. The previous position pushed the button below the fold on smaller screens; the new position sits it next to the search field where users naturally look for global actions.

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

3.0.3 [2026-07-18]

Added

  • “Robots” column on taxonomy list tables. When per-term granular control is on, the Categories / Tags / custom taxonomy list pages now show a “Robots” column with the same directive badges (🔍 noindex, 🔗 nofollow, 💾 noarchive, 📄 nosnippet, 🖼️ noimageindex) used on the post list. Makes it possible to scan at a glance which terms have an override and which directives are active — parity with the existing post-list UX.

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • Added 3 tests covering the term column renderer (no override, with override, passthrough for other columns).

3.0.2 [2026-07-18]

Fixed

  • Duplicate “Search Engine Visibility” panel in the block editor. The plugin previously registered both a classic PHP meta box (add_meta_box) AND a native Gutenberg sidebar panel (PluginDocumentSettingPanel via assets/js/editor-sidebar.js) with the same title — both showed up in the block editor sidebar as two panels with the same name. The classic meta box is now suppressed on block-editor screens (detected via get_current_screen()->is_block_editor, which respects the Classic Editor plugin’s per-request override). Classic editor users still get the meta box.

Added

  • Per-term granular control for taxonomies. Mirrors the existing per-post granular pattern: when “Enable per-term granular control” is on, every public taxonomy (category, post_tag, and any custom public taxonomy like WooCommerce’s product_cat / product_tag) gets a “Search Engine Visibility” panel on the Edit Tag screen with an override checkbox and the five directive checkboxes. Term-level directives take precedence over the global category / tag / post_type_archive settings when the term has the override enabled. Backed by register_term_meta() with show_in_rest=true so headless consumers can read them.

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • Added tests/test-term-meta.php covering registration, save handler, front-end override precedence, and uninstall cleanup.

3.0.1 [2026-07-18]

Fixed

  • Self-healing v3 migration. If noindex_seo_config_version claims migration completed (version >= 3) but the consolidated noindex_seo_settings option is somehow missing, noindex_seo_check_migration() now re-runs the migration automatically. This recovers silently from partial failures: object-cache staleness after the write, request interrupted mid-migration, a third-party plugin clearing options, or DB write errors that did not raise a PHP error. Without this guard, the front-end would fall back to all-zero defaults (site fully indexable) until someone re-saved the settings page.

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • Added test for the self-healing migration path.

3.0.0 [2026-07-18]

Major release — storage consolidation. Settings are now stored in a single autoloaded option (noindex_seo_settings) instead of ~110 individual options. Reduces wp_options autoload weight on every page load. Legacy options are preserved (marked non-autoloaded) for one major version to allow safe rollback to 2.x; they will be removed in 4.0.

Added

  • “Apply recommended defaults” button on the settings page. One click enables all 12 contexts flagged as “Recommended” in the UI (privacy policy, date archives, pagination, search results, attachment pages, preview/customize-preview, and 404). Previously required ~60 individual checkbox toggles.
  • Filterable conflict-detection list. The list of plugins that may conflict with noindex SEO is now passed through apply_filters('noindex_seo_conflicting_plugins', ...). Third-party code can extend or replace the list without editing the plugin.

Changed

  • Storage consolidated: 110 directive options ({directive}_seo_{context}) + 5 config options (noindex_seo_config_) merged into one noindex_seo_settings array. Reads go through new accessors (noindex_seo_get_settings(), noindex_seo_get_setting(), noindex_seo_get_config()). The hourly transient cache is preserved on top.
  • Transient invalidation is now automatic: any update to noindex_seo_settings clears the noindex_seo_options transient via the update_option_noindex_seo_settings hook. Previously only the admin form save cleared it, leaving WP-CLI / REST / programmatic edits stale for up to one hour.
  • Migration: existing 2.x installs are migrated automatically on plugins_loaded (idempotent, version-gated by noindex_seo_config_version). Legacy options are kept and marked non-autoloaded so a rollback to 2.x restores the pre-3.0 state.
  • Uninstall retrocompatibility: uninstall.php deletes both the new consolidated option and all legacy individual options (via wildcard SQL). Sites that upgrade from 2.x and later uninstall will be cleaned up completely regardless of whether migration ran.

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • Added PHPUnit test suite (previously the project shipped only a plugin-header smoke test). Coverage: contexts helper, settings accessors, v2→v3 migration, form save, bulk actions, meta box save, conflict-detection filter.

2.0.2 [2026-07-18]

Fixed

  • Bulk action “Enable Robots Override” no longer creates duplicate postmeta rows. The previous implementation used INSERT ... ON DUPLICATE KEY UPDATE, but WordPress’s postmeta table has no unique key on (post_id, meta_key), so the ON DUPLICATE KEY UPDATE branch never fired and every bulk run on a post that already had the meta inserted a duplicate row. The bulk handler now uses update_post_meta() and delete_post_meta() exclusively.
  • Bulk action “Disable Robots Override” now removes the override meta entirely (previously set its value to 0, leaving a redundant row).

Changed

  • Removed three dead contexts (feed, comment_feed, robots) that were registered, migrated, and cleaned up but never evaluated at runtime and never exposed in the admin UI. The plugin now consistently registers and evaluates 22 page contexts (down from a nominal 25). Existing orphan options from earlier installs are cleaned by the uninstall routine’s wildcard delete.
  • Extracted noindex_seo_get_contexts() as the single source of truth for the context list shared by settings registration, v2 migration, form processing. uninstall.php retains its own copy (technical constraint: the main plugin file is not loaded during uninstall) with a sync warning comment.
  • Corrected phpcs.xml minimum_supported_wp_version from 6.6 to 5.7 to match the plugin header. This setting was stale from before the 2.0.1 minimum-WP correction.
  • Updated readme.txt description counts from 25 to 22 page contexts.
  • Bulk action performance characteristic changed: the rewrite from a single batched SQL statement to per-post update_post_meta() / delete_post_meta() calls trades O(1) queries for O(N) queries on bulk selections, in exchange for correctness (no more duplicate rows) and proper WP meta-cache invalidation. Typical admin bulk selections (≤200 posts) are unaffected; very large selections (thousands of posts) may take noticeably longer.

Deprecated

  • noindex_seo_clear_transient(): was hooked to update_option_noindexseo, a hook that never fires because noindexseo is the Settings API group name, not an option name. The function now emits a deprecation notice via _deprecated_function() and continues to clear the noindex_seo_options transient if called directly. The transient is cleared correctly inside noindex_seo_process_form() after every successful save. The function will be removed in a future release.

Code Quality

  • bin/preflight.sh: hardened temp-file handling (mktemp moved to script top, trap … EXIT cleanup, no more predictable /tmp/composer-audit.* paths that were vulnerable to symlink overwrite on shared hosting); fixed 2>/null typo; added rsync / .distignore / build-tree-non-empty guards so the forbidden-artefact scan cannot pass vacuously; replaced npm-audit subshell with pushd/popd so gate counters are not lost.
  • Added bin/preflight.sh: automated pre-deploy verification covering PHPCS, PHPStan level 9, PHPCompatibility, PHPUnit, dependency audit, and candidate-ZIP artefact inspection. Prints a PASS/FAIL report per section and exits non-zero on any failure.
  • Added .claude/settings.json deny rules for deploy.sh, git push, git tag, and git merge, enforcing the agent operating boundaries mandated by AGENTS.md mechanically rather than only in prose.

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • PHP Coding Standards: 3.13.5
  • WordPress Coding Standards: 3.3.0
  • PHPStan: 2.1.55
  • PHPUnit: 9.6.34

2.0.1 [2026-05-25]

Added

  • New option “Delete all plugin data on uninstall” (Settings → noindex SEO → General Configuration). By default, all data is preserved when the plugin is uninstalled. Enable this option to remove all settings and per-post directives on uninstall.

Changed

  • Lowered minimum WordPress requirement from 6.6 to 5.7, matching the actual oldest API used (wp_robots filter, introduced in WP 5.7). Verified with johnbillion/wp-compat.
  • Custom post types registered by plugins or themes on the init hook now correctly receive the Robots column, bulk actions, and list filter in granular control mode. Previously only built-in post types (post, page, attachment) were supported.

Code Quality

  • Added NOINDEX_SEO_VERSION constant; all enqueued assets now use it for cache-busting.
  • PHP type safety: replaced (string) casts on mixed values from get_option(), get_post_meta(), and $_POST/$_GET/$_REQUEST superglobals with proper is_string()/is_scalar() narrowing guards. Passes PHPStan level 9 on PHP 8.5 with zero errors.
  • Extracted noindex_seo_save_directives_from_post() helper; meta box save and Quick Edit save now share a single implementation.
  • Improved noindex_seo_contexts filter documentation: documents the str_replace-based directive key derivation and the noindex_seo_{context} key format requirement.
  • Removed dead code: isset($field['conditional']) block (key never existed in field shape), redundant isset($field['suggestion']) guard, redundant isset($section_icons[$section_id]) guard, and a provably non-empty $meta_query check.
  • Removed dead hook registration for update_option_noindexseo (Settings API group name, not an option name; hook never fired).

Compatibility

  • WordPress: 5.7 – 7.1
  • PHP: 7.2 – 8.5

Tests

  • PHP Coding Standards: 3.13.5
  • WordPress Coding Standards: 3.3.0
  • PHPStan: 2.1.55
  • PHPUnit: 9.6.34

2.0.0 [2026-01-20]

New Features

  • 5 independent robots directives: noindex, nofollow, noarchive, nosnippet, noimageindex
  • Each directive can be enabled independently for any page context (125 total options)
  • HTTP Headers support: Choose between HTML meta tags, HTTP headers, or both
  • Granular per-post/page control (optional):
  • Override global settings for individual posts/pages
  • Meta boxes for Classic Editor
  • Native Gutenberg sidebar panel for Block Editor
  • Quick Edit support for fast inline editing
  • Bulk actions to enable/disable overrides
  • Custom “Robots” column in post lists
  • Filter posts by override status
  • Modern checkbox-based interface with emoji icons and tooltips

Security & Code Quality

  • Modernized code with PHP 7.2+ strict types and type declarations

Compatibility & Migration

  • WordPress: 6.6 – 6.9
  • PHP: 7.2 – 8.5
  • Automatic migration from v1.x (your existing settings are preserved)
  • No manual configuration needed – just update and go
  • Fully backward compatible

1.2.0 [2025-04-08]

Changes

  • Improved functions documentation.

Fixes

  • The way the options are saved.

Compatibility

  • WordPress: 4.1 – 6.8
  • PHP: 5.6 – 8.4

Tests

  • PHP Coding Standards: 3.12.1
  • WordPress Coding Standards: 3.1.0
  • Plugin Check (PCP): 1.4.0

1.1.1 [2024-11-04]

Added

  • Configuration option to dismiss other SEO plugin incompatibilities.

Compatibility

  • WordPress: 4.1 – 6.7
  • PHP: 5.6 – 8.4

1.1.0 [2024-11-02]

Added

  • Detects other WordPress SEO plugins, and creates a notice about it, to avoid conflicts.
  • Has filters, so other plugins can hack.

Changed

  • Uses native wp_robots functions (since WP 5.7+)
  • Big refactory.
  • Less size, improved code quality.

Compatibility

  • WordPress: 4.1 – 6.7
  • PHP: 5.6 – 8.4

Tests

  • PHP Coding Standards: 3.10.3
  • WordPress Coding Standards: 3.1.0
  • Plugin Check (PCP): 1.1.0

1.0.12

  • Compatibility: WordPress 4.1 – WordPress 6.4.
  • Compatibility: PHP 5.6 – PHP 8.3.
  • Compatibility: WordPress Coding Standards 3.0.1.

1.0.11

  • Fix showing all the text.

1.0.10

  • WordPress Coding Standards 3.0.0 compatible.
  • Compatibility: PHP 5.6 – PHP 8.3.

1.0.9

  • Improved functions checking.
  • Compatibility: WordPress 5.2 – WordPress 6.3.
  • Compatibility: PHP 7.0 – PHP 8.3.

1.0.8

  • Compatibility: WordPress 5.2 – WordPress 6.2
  • Compatibility: PHP 7.2 – PHP 8.1

1.0.7

  • Minor refactoring
  • Compatibility: WordPress 4.9 – WordPress 6.1
  • Compatibility: PHP 5.6.20 – PHP 8.1

1.0.6

  • Compatibility: WordPress 4.9 – WordPress 6.0
  • Compatibility: PHP 5.6.20 – PHP 8.1

1.0.5

  • Compatibility: WordPress 4.9 – WordPress 5.9
  • Compatibility: PHP 5.6.20 – PHP 8.1

1.0.4

  • Compatibility: WordPress 5.8

1.0.3

  • Compatibility: WordPress 5.7

1.0.2

  • Compatibility: WordPress 5.6
  • Compatibility: PHP 8.0

1.0.1

  • Compatibility: WordPress 5.5

1.0.0

  • Compatibility: WordPress 5.4

0.1

  • First version.