Contents
Introduction
This article is a comprehensive, ready-to-publish tutorial on how to register custom health checks in WordPress Site Health using PHP. It covers the underlying API, exact code examples you can drop in a plugin (or functions.php), the difference between synchronous (direct) and asynchronous checks, return structure expected by WordPress, best practices, debugging and testing, security considerations, and advanced patterns (badges, actions, links, and transients).
Background: Site Health in WordPress
WordPress introduced the Site Health feature in 5.2. It provides a central place (Tools → Site Health) to display automated checks about the environment and configuration. Developers can add their own checks so administrators can quickly discover issues relevant to plugins, themes, hosting environment or custom integrations.
Where custom checks appear
- Checks registered in the direct group run synchronously during the admin page load.
- Checks registered in the async group are executed via REST requests to avoid long admin page loads useful for network calls or slow operations.
How Site Health expects tests to be registered
WordPress exposes a filter named site_status_tests. You add your checks into this array. The array is structured by group (commonly direct and async), and within each group the map key is a unique test slug and the value is the callback (callable name). The callback should return an array describing the test result. Below is the canonical registration pattern:
add_filter( site_status_tests, myplugin_register_site_health_checks ) function myplugin_register_site_health_checks( tests ) { // Register a direct (synchronous) test tests[direct][myplugin_direct_check] = myplugin_direct_check_callback // Register an async (REST-driven) test tests[async][myplugin_async_check] = myplugin_async_check_callback return tests }
Groups summary
- direct — executes during page load. Use for quick, deterministic checks that are fast.
- async — executed by the Site Health REST call use for slow operations like remote API calls, DNS checks, or operations that can block.
What your callback must return
Each test callback must return an associative array with certain keys WordPress uses to render the check. The most common keys used in practice:
- label — (string) human-readable name shown in the Site Health UI.
- status — (string) one of the allowed status levels: good, recommended, or critical.
- badge — (array, optional) a compact badge with label and color describing the category (e.g., Performance).
- description — (string, HTML allowed) long HTML description shown under the label explain the problem and (optionally) remediation steps.
- actions — (array, optional) an array of action arrays (each with label and url) that Site Health displays as buttons/links for immediate remediation.
- href — (string, optional) URL to documentation or a settings page providing more info.
Status meanings (quick reference)
good | Everything is OK. |
recommended | Site configuration could be improved (advice / best practice) but not critical. |
critical | A problem that should be addressed as soon as possible. |
Minimum viable test example
This is the smallest correct callback: register it under the direct group and return a basic result. Perfect for checks that are very quick e.g. verify a constant, PHP function presence, or a file permission check that is immediate.
add_filter( site_status_tests, mv_register_health_check ) function mv_register_health_check( tests ) { tests[direct][mv_uploads_writable] = mv_uploads_writable_check return tests } function mv_uploads_writable_check() { writable = is_writable( WP_CONTENT_DIR . /uploads ) if ( writable ) { status = good description = __( The uploads directory is writable., my-textdomain ) } else { status = critical description = __( The uploads directory is not writable. This prevents media uploads., my-textdomain ) } return array( label => __( Uploads directory writable, my-textdomain ), status => status, badge => array( label => __( Filesystem, my-textdomain ), color => blue, ), description => wp_kses_post( description ), actions => array( array( label => __( Open Media Settings, my-textdomain ), url => admin_url( options-media.php ), ), ), href => admin_url( options-media.php ), ) }
Async test example (recommended for remote calls)
For operations that might take longer than a few hundred milliseconds — such as network requests — register the check in the async group. Site Health will run it via JavaScript REST and render results when available.
add_filter( site_status_tests, async_register_health_check ) function async_register_health_check( tests ) { tests[async][myplugin_remote_api] = myplugin_remote_api_check return tests } function myplugin_remote_api_check() { // Use a short timeout. The async mechanism still needs responsive endpoints. response = wp_safe_remote_get( https://api.example.com/health, array( timeout => 5 ) ) if ( is_wp_error( response ) ) { status = critical description = sprintf( __( Could not contact the remote API: %s, my-textdomain ), esc_html( response->get_error_message() ) ) } else { code = wp_remote_retrieve_response_code( response ) if ( 200 === (int) code ) { status = good description = __( Remote API reachable and returned HTTP 200., my-textdomain ) } else { status = recommended description = sprintf( __( Remote API returned HTTP %d., my-textdomain ), (int) code ) } } return array( label => __( Remote API connectivity, my-textdomain ), status => status, description => wp_kses_post( description ), actions => array( array( label => __( API Documentation, my-textdomain ), url => https://api.example.com/docs, ), ), href => https://api.example.com/docs, ) }
Advanced example: feature check with remediation and debug info
This example demonstrates a richer response: badge, multiple actions, contextual details and how to structure the description using HTML (WordPress will render it).
add_filter( site_status_tests, advanced_register_health_check ) function advanced_register_health_check( tests ) { tests[direct][myplugin_cache_enabled] = myplugin_cache_enabled_check return tests } function myplugin_cache_enabled_check() { // Example: check for object-cache drop-in (simple example) dropin_present = defined( WP_CACHE ) WP_CACHE file_exists( WP_CONTENT_DIR . /object-cache.php ) if ( dropin_present ) { status = good desc = __( An object cache drop-in is active., my-textdomain ) } else { status = recommended desc = __( No object cache detected. Adding an object cache can improve performance for dynamic sites., my-textdomain ) } return array( label => __( Object cache (drop-in) status, my-textdomain ), status => status, badge => array( label => __( Performance, my-textdomain ), color => purple, ), description => sprintf( / translators: %1s - short description, %2s - link to docs / __( %1s
Learn how to add an object cache., my-textdomain ), esc_html( desc ), esc_url( https://developer.wordpress.org/advanced-topics/caching/ ) ), actions => array( array( label => __( Caching Documentation, my-textdomain ), url => https://developer.wordpress.org/advanced-topics/caching/, ), array( label => __( Install Cache Plugin, my-textdomain ), url => admin_url( plugin-install.php?s=cachetab=searchtype=term ), ), ), href => https://developer.wordpress.org/advanced-topics/caching/, ) }
Best practices
- Use descriptive, unique slugs: choose something like pluginname_feature_check to avoid collisions.
- Prefer async for slow operations: network, DNS or large filesystem scans should be async to keep the admin UI snappy.
- Keep direct checks fast: direct checks influence page render. If it may block, move to async and set a reasonable timeout.
- Return localized text: wrap labels, descriptions and action labels in translation functions (e.g., __(), _e()).
- Use safe HTML and escaping: description may contain HTML sanitize it before returning (wp_kses_post or similar) to avoid XSS.
- Provide actionable remediation: users appreciate a link to settings, docs or an actionable admin page.
- Cache expensive results: if a check is expensive but must run occasionally, consider caching the result with a transient and return the cached result when valid.
- Respect capabilities: Site Health is shown to admins — but if your action URL performs privileged actions, ensure proper permission checks on those endpoints.
Notes about security and REST (async)
- Async tests are executed through a core REST endpoint for Site Health WordPress handles permissions for that endpoint. Keep the test callback side-effect free — it should not modify state.
- Never echo sensitive data (database credentials, secret tokens) in the description. If you provide debug info, ensure it is only accessible to authorized users and sanitized.
- If you add links to admin pages that perform operations, ensure those pages are protected with nonces / capability checks.
Using transients to cache a slow test
If you have a test that must run occasionally but is expensive, compute once and store the result in a transient and return the cached value if fresh.
function cached_slow_check_callback() { cache_key = myplugin_slow_check_result result = get_site_transient( cache_key ) if ( false !== result ) { return result } // Expensive operation here ok = myplugin_perform_heavy_check() result = array( label => __( Heavy check, my-textdomain ), status => ok ? good : recommended, description => ok ? __( All good, my-textdomain ) : __( Problem found, my-textdomain ), ) // Cache for 10 minutes set_site_transient( cache_key, result, 10 MINUTE_IN_SECONDS ) return result }
Testing and debugging custom checks
- Install your code as a plugin (recommended) or place into the active themes functions.php for quick tests.
- Go to WordPress admin → Tools → Site Health.
- For direct checks, results are visible right away. For async checks, open the Site Health page and wait — results should appear after the async request completes. Use browser developer tools to inspect REST calls to the Site Health endpoints.
- Use error_log or built-in logging for debugging, and ensure to remove verbose or sensitive logs before production release.
Common pitfalls
- Registering heavy operations in the direct group — causes slow admin response or timeouts.
- Returning non-expected keys or malformed arrays — WordPress will ignore or not render the check properly. Always return at least label and status.
- Exposing sensitive debug information to non-authorized users.
- Not wrapping strings with localization functions.
Checklist before shipping your check
- Use unique slug and function names to avoid collisions.
- Localize all user-facing strings.
- Escape and sanitize all returned HTML/URLs.
- Put heavy checks into async or cache them.
- Provide an actionable remediation path (actions/href) if the status is not good.
- Document the test in your plugin readme so admins know why its there and what it checks.
Further reading and resources
- WordPress Developer Resources — general developer docs.
- Plugin Developer Handbook — where plugin authors should place code and how to handle best practices.
- Make WordPress Core — discussions about Site Health evolution and related patches.
Summary
To add a custom Site Health check: hook into the site_status_tests filter, register your test callback in either the direct or async group depending on expected execution time, and return an array with keys like label, status, description, badge, actions and optionally href. Follow best practices for performance, security and localization, and prefer async or caching for heavy checks.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |