<?php
/**
* Plugin Name: WordPress Doctor
* Description: A lightweight website health checker by DesignoPage.
* Version: 1.2.0
* Author: DesignoPage
* License: GPL-2.0-or-later
*/
if ( ! defined( 'ABSPATH' ) ) { exit; }
define( 'WPD_VERSION', '1.2.0' );
function wpd_register_shortcode() {
add_shortcode( 'wordpress_doctor', 'wpd_render_shortcode' );
}
add_action( 'init', 'wpd_register_shortcode' );
/** Normalize and validate a submitted public website URL. */
function wpd_normalize_url( $input ) {
$input = trim( (string) $input );
if ( $input === '' || preg_match( '/[\s\x00-\x1F\x7F]/', $input ) || strlen( $input ) > 2048 ) {
return new WP_Error( 'wpd_invalid_url', 'Please enter a valid website address.' );
}
if ( ! preg_match( '#^https?://#i', $input ) ) { $input = 'https://' . $input; }
$url = esc_url_raw( $input, array( 'http', 'https' ) );
$parts = wp_parse_url( $url );
if ( ! $url || ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ||
! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) || isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
return new WP_Error( 'wpd_invalid_url', 'Please enter a valid website address.' );
}
$host = strtolower( rtrim( $parts['host'], '.' ) );
if ( $host === 'localhost' || substr( $host, -10 ) === '.localhost' || substr( $host, -6 ) === '.local' ||
substr( $host, -9 ) === '.internal' || filter_var( $host, FILTER_VALIDATE_IP ) ) {
return new WP_Error( 'wpd_unsafe_url', 'Please enter a public website domain, not a local address or IP address.' );
}
if ( ! wp_http_validate_url( $url ) ) {
return new WP_Error( 'wpd_unsafe_url', 'This address could not be validated as a safe public website.' );
}
return $url;
}
/** Read quoted or unquoted HTML attributes, independent of attribute order. */
function wpd_get_attribute( $tag, $attribute ) {
$pattern = '/(?:^|\s)' . preg_quote( $attribute, '/' ) . '\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/i';
if ( preg_match( $pattern, $tag, $m ) ) {
$value = isset( $m[1] ) && $m[1] !== '' ? $m[1] : ( ( isset( $m[2] ) && $m[2] !== '' ) ? $m[2] : ( $m[3] ?? '' ) );
return html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
}
return '';
}
/** Return metadata lookup details so absent tags and empty content are distinct. */
function wpd_find_meta_content( $html, $name ) {
$found = false;
$empty_found = false;
$values = array();
if ( preg_match_all( '/<meta\b[^>]*>/i', $html, $matches ) ) {
foreach ( $matches[0] as $tag ) {
$tag_name = wpd_get_attribute( $tag, 'name' );
$property = wpd_get_attribute( $tag, 'property' );
if ( strcasecmp( $tag_name, $name ) === 0 || strcasecmp( $property, $name ) === 0 ) {
$found = true;
$content = trim( wpd_get_attribute( $tag, 'content' ) );
if ( $content !== '' ) { $values[] = $content; } else { $empty_found = true; }
}
}
}
return array( 'found' => $found, 'empty' => $empty_found, 'values' => $values );
}
/** Backward-compatible helper used by viewport checks. */
function wpd_get_meta_content( $html, $name ) {
$meta = wpd_find_meta_content( $html, $name );
return ! empty( $meta['values'] ) ? $meta['values'][0] : '';
}
function wpd_result_item( $name, $status, $detail ) {
return array( 'name' => $name, 'status' => $status, 'detail' => $detail );
}
/** Determine whether a directive token appears in any supplied value. */
function wpd_has_directive_token( $values, $token ) {
foreach ( (array) $values as $value ) {
if ( preg_match( '/(?:^|[\s,;])' . preg_quote( $token, '/' ) . '(?:$|[\s,;])/i', (string) $value ) ) { return true; }
}
return false;
}
function wpd_scan_website( $url ) {
$started = microtime( true );
$response = wp_safe_remote_get( $url, array(
'timeout' => 12, 'redirection' => 3, 'limit_response_size' => 1048576,
'user-agent' => 'WordPressDoctor/' . WPD_VERSION . '; ' . home_url( '/' ),
'headers' => array( 'Accept' => 'text/html,application/xhtml+xml' ),
) );
$elapsed = round( microtime( true ) - $started, 2 );
if ( is_wp_error( $response ) ) {
return array( 'error' => 'WordPress Doctor could not retrieve this website. ' . $response->get_error_message() );
}
$http_code = (int) wp_remote_retrieve_response_code( $response );
$html = wp_remote_retrieve_body( $response );
if ( $http_code < 200 || $http_code >= 400 || $html === '' ) {
return array( 'error' => sprintf( 'The website returned HTTP status %1$d or no readable HTML. The page checks could not be completed.', $http_code ) );
}
$results = array();
$results[] = wpd_result_item( 'Website response', ( $http_code >= 200 && $http_code < 300 ) ? 'Pass' : 'Detected — Please verify',
sprintf( 'The website returned HTTP %1$d in approximately %2$s seconds.', $http_code, number_format_i18n( $elapsed, 2 ) ) );
// 2. Page title.
$title = '';
if ( preg_match( '/<title\b[^>]*>(.*?)<\/title>/is', $html, $m ) ) {
$title = trim( html_entity_decode( wp_strip_all_tags( $m[1] ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
}
$results[] = wpd_result_item( 'Page title', $title !== '' ? 'Pass' : 'Issue found',
$title !== '' ? 'Title detected: ' . $title : 'No page title was detected in the retrieved HTML. JavaScript-rendered titles may not be visible to this scanner.' );
// 3. Meta description: inspect all matching tags and distinguish absent, empty, and unreadable cases.
$description_meta = wpd_find_meta_content( $html, 'description' );
$descriptions = array_values( array_unique( $description_meta['values'] ) );
if ( ! empty( $descriptions ) ) {
$description = implode( ' | ', $descriptions );
$description_status = 'Pass';
$description_detail = 'Meta description detected in the retrieved HTML: ' . $description;
if ( count( $descriptions ) > 1 ) { $description_status = 'Detected — Please verify'; $description_detail = 'Multiple non-empty meta description values were detected in the retrieved HTML: ' . $description . '. Keep one intended description. JavaScript-rendered changes may not be visible to this scanner.'; }
} elseif ( ! empty( $description_meta['found'] ) && ! empty( $description_meta['empty'] ) ) {
$description_status = 'Issue found';
$description_detail = 'A meta description tag was detected, but its content was empty. The scanner inspects retrieved HTML; JavaScript-rendered metadata may not be visible.';
} else {
$description_status = 'Detected — Please verify';
$description_detail = 'No meta description tag was detected in the retrieved HTML. This may mean it is missing, or it may be added later by JavaScript and therefore not visible to this basic HTTP scanner.';
}
$results[] = wpd_result_item( 'Meta description', $description_status, $description_detail );
// 4. H1 heading.
$h1 = '';
if ( preg_match( '/<h1\b[^>]*>(.*?)<\/h1>/is', $html, $m ) ) { $h1 = trim( wp_strip_all_tags( $m[1] ) ); }
$results[] = wpd_result_item( 'H1 heading', $h1 !== '' ? 'Pass' : 'Issue found',
$h1 !== '' ? 'H1 heading detected: ' . $h1 : 'No H1 heading was detected in the retrieved HTML. JavaScript-rendered headings may not be visible to this scanner.' );
// 5. Mobile viewport.
$viewport = wpd_get_meta_content( $html, 'viewport' );
$results[] = wpd_result_item( 'Mobile viewport', $viewport !== '' ? 'Pass' : 'Issue found',
$viewport !== '' ? 'A viewport meta tag was detected.' : 'No viewport meta tag was detected in the retrieved HTML.' );
// 6. Canonical URL.
$canonical = '';
if ( preg_match_all( '/<link\b[^>]*>/i', $html, $link_matches ) ) {
foreach ( $link_matches[0] as $tag ) {
$rel = strtolower( wpd_get_attribute( $tag, 'rel' ) );
if ( in_array( 'canonical', preg_split( '/\s+/', trim( $rel ) ), true ) ) { $canonical = wpd_get_attribute( $tag, 'href' ); break; }
}
}
$results[] = wpd_result_item( 'Canonical URL', $canonical !== '' ? 'Pass' : 'Detected — Please verify',
$canonical !== '' ? 'Canonical URL detected: ' . $canonical : 'No canonical link was detected. Confirm whether this page needs one.' );
// 7. Indexing directives: all robots/googlebot meta tags and all X-Robots-Tag values.
$robots_values = array();
$googlebot_values = array();
if ( preg_match_all( '/<meta\b[^>]*>/i', $html, $meta_tags ) ) {
foreach ( $meta_tags[0] as $tag ) {
$name = strtolower( trim( wpd_get_attribute( $tag, 'name' ) ) );
if ( $name === 'robots' ) { $robots_values[] = wpd_get_attribute( $tag, 'content' ); }
if ( $name === 'googlebot' ) { $googlebot_values[] = wpd_get_attribute( $tag, 'content' ); }
}
}
$headers = wp_remote_retrieve_headers( $response );
$xrobots_values = array();
if ( is_object( $headers ) && method_exists( $headers, 'getAll' ) ) {
$all_headers = $headers->getAll();
foreach ( $all_headers as $header_name => $header_value ) {
if ( strtolower( (string) $header_name ) === 'x-robots-tag' ) {
foreach ( (array) $header_value as $value ) { $xrobots_values[] = (string) $value; }
}
}
} elseif ( is_array( $headers ) ) {
foreach ( $headers as $header_name => $header_value ) {
if ( strtolower( (string) $header_name ) === 'x-robots-tag' ) {
foreach ( (array) $header_value as $value ) { $xrobots_values[] = (string) $value; }
}
}
}
// WordPress header objects normally return a single combined header value; preserve arrays when provided.
if ( empty( $xrobots_values ) ) {
$single_header = wp_remote_retrieve_header( $response, 'x-robots-tag' );
if ( is_array( $single_header ) ) { $xrobots_values = array_map( 'strval', $single_header ); }
elseif ( is_string( $single_header ) && $single_header !== '' ) { $xrobots_values = array( $single_header ); }
}
$all_index_values = array_merge( $robots_values, $googlebot_values, $xrobots_values );
$noindex = wpd_has_directive_token( $all_index_values, 'noindex' );
$index_sources = array();
if ( ! empty( $robots_values ) ) { $index_sources[] = 'robots meta tag(s)'; }
if ( ! empty( $googlebot_values ) ) { $index_sources[] = 'googlebot meta tag(s)'; }
if ( ! empty( $xrobots_values ) ) { $index_sources[] = 'X-Robots-Tag response header(s)'; }
if ( $noindex ) {
$index_status = 'Issue found';
$index_detail = 'A noindex directive was detected in ' . implode( ', ', $index_sources ) . '. Search engines may be instructed not to index this page.';
} elseif ( ! empty( $index_sources ) ) {
$index_status = 'Detected — Please verify';
$index_detail = 'Indexing directives were found in ' . implode( ', ', $index_sources ) . ', but no noindex token was detected. Other restrictions or search-engine-specific rules may still apply.';
} else {
$index_status = 'Detected — Please verify';
$index_detail = 'No robots/googlebot meta directive or X-Robots-Tag header was detected in the retrieved HTML and response headers. This does not guarantee that the page can be indexed; JavaScript-rendered metadata and other restrictions may not be visible.';
}
$results[] = wpd_result_item( 'Indexing directive', $index_status, $index_detail );
// 8. HTTPS check. Use the submitted URL scheme; the HTTP API does not reliably expose the final URL.
$is_https = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) ) === 'https';
$results[] = wpd_result_item( 'HTTPS connection', $is_https ? 'Pass' : 'Issue found',
$is_https ? 'The submitted website address uses HTTPS. This does not independently verify every aspect of the SSL configuration.' : 'The submitted website address uses HTTP rather than HTTPS.' );
// Prepare origin for robots.txt and sitemap checks.
$url_parts = wp_parse_url( $url );
$origin = '';
if ( is_array( $url_parts ) && ! empty( $url_parts['scheme'] ) && ! empty( $url_parts['host'] ) ) {
$origin = $url_parts['scheme'] . '://' . $url_parts['host'];
if ( ! empty( $url_parts['port'] ) ) { $origin .= ':' . (int) $url_parts['port']; }
}
// 9. robots.txt.
$robots_body = false; $robots_status = 0;
if ( $origin !== '' ) {
$robots_response = wp_safe_remote_get( $origin . '/robots.txt', array(
'timeout' => 8, 'redirection' => 3, 'limit_response_size' => 262144,
'user-agent' => 'WordPressDoctor/' . WPD_VERSION . '; ' . home_url( '/' ),
) );
if ( ! is_wp_error( $robots_response ) ) {
$robots_status = (int) wp_remote_retrieve_response_code( $robots_response );
if ( $robots_status >= 200 && $robots_status < 300 ) { $robots_body = wp_remote_retrieve_body( $robots_response ); }
}
}
if ( is_string( $robots_body ) && trim( $robots_body ) !== '' ) {
$results[] = wpd_result_item( 'robots.txt', 'Pass', 'robots.txt was retrieved successfully. Its directives still need to be reviewed for your intended crawling rules.' );
} elseif ( $robots_status === 404 ) {
$results[] = wpd_result_item( 'robots.txt', 'Detected — Please verify', 'No robots.txt file was found at the standard location. This is not automatically an SEO problem.' );
} else {
$results[] = wpd_result_item( 'robots.txt', 'Unable to verify', 'WordPress Doctor could not confirm that robots.txt is readable. The file may be unavailable, blocked, or temporarily inaccessible.' );
}
// 10. Crawling directives (preserved behaviour).
if ( is_string( $robots_body ) && trim( $robots_body ) !== '' ) {
if ( preg_match( '/^\s*Disallow\s*:\s*\/\s*(?:#.*)?$/im', $robots_body ) ) {
$results[] = wpd_result_item( 'Crawling directives', 'Detected — Please verify', 'A broad Disallow: / rule was detected. It can block crawling for the user-agent group containing that rule. Review the group and confirm that blocking is intentional.' );
} elseif ( preg_match( '/^\s*Disallow\s*:/im', $robots_body ) ) {
$results[] = wpd_result_item( 'Crawling directives', 'Detected — Please verify', 'One or more selective Disallow rules were detected. These may block particular paths rather than the entire website. Review the user-agent groups and paths to confirm they are intentional.' );
} else {
$results[] = wpd_result_item( 'Crawling directives', 'Pass', 'No Disallow directives were detected in the readable robots.txt file. This does not guarantee that every page is crawlable.' );
}
} else {
$results[] = wpd_result_item( 'Crawling directives', 'Unable to verify', 'Crawling directives could not be inspected because a readable robots.txt file was not retrieved.' );
}
// 11 and 12. Sitemap discovery and robots.txt sitemap reference (preserved behaviour).
$sitemap_candidates = array();
if ( is_string( $robots_body ) && trim( $robots_body ) !== '' && preg_match_all( '/^\s*Sitemap\s*:\s*(\S+)/im', $robots_body, $sitemap_matches ) ) {
foreach ( $sitemap_matches[1] as $sitemap_url ) {
if ( preg_match( '#^https?://#i', $sitemap_url ) ) { $candidate = esc_url_raw( $sitemap_url, array( 'http', 'https' ) ); }
else { $candidate = esc_url_raw( $origin . '/' . ltrim( $sitemap_url, '/' ), array( 'http', 'https' ) ); }
if ( $candidate && wp_http_validate_url( $candidate ) ) { $sitemap_candidates[] = $candidate; }
}
}
$robots_lists_sitemap = ! empty( $sitemap_candidates );
if ( $origin !== '' ) {
$sitemap_candidates[] = $origin . '/sitemap.xml';
$sitemap_candidates[] = $origin . '/sitemap_index.xml';
$sitemap_candidates[] = $origin . '/wp-sitemap.xml';
}
$sitemap_candidates = array_values( array_unique( $sitemap_candidates ) );
$sitemap_found = false; $sitemap_unverified = false;
foreach ( $sitemap_candidates as $sitemap_url ) {
if ( ! wp_http_validate_url( $sitemap_url ) ) { continue; }
$sitemap_response = wp_safe_remote_get( $sitemap_url, array(
'timeout' => 8, 'redirection' => 3, 'limit_response_size' => 262144,
'user-agent' => 'WordPressDoctor/' . WPD_VERSION . '; ' . home_url( '/' ),
'headers' => array( 'Accept' => 'application/xml,text/xml,*/*' ),
) );
if ( is_wp_error( $sitemap_response ) ) { $sitemap_unverified = true; continue; }
$sitemap_code = (int) wp_remote_retrieve_response_code( $sitemap_response );
$sitemap_body = wp_remote_retrieve_body( $sitemap_response );
if ( $sitemap_code >= 200 && $sitemap_code < 300 && is_string( $sitemap_body ) && preg_match( '/<(?:[a-z0-9_-]+:)?(?:urlset|sitemapindex)\b/i', $sitemap_body ) ) { $sitemap_found = true; break; }
if ( $sitemap_code !== 404 ) { $sitemap_unverified = true; }
}
if ( $sitemap_found ) {
$results[] = wpd_result_item( 'Sitemap discovery', 'Pass', 'An accessible XML sitemap or sitemap index was detected at a common location or a URL advertised by robots.txt.' );
} elseif ( $sitemap_unverified ) {
$results[] = wpd_result_item( 'Sitemap discovery', 'Unable to verify', 'No accessible XML sitemap could be confirmed. A sitemap may exist at a custom location or access may be restricted.' );
} else {
$results[] = wpd_result_item( 'Sitemap discovery', 'Detected — Please verify', 'No XML sitemap was found at the checked locations. Confirm whether your website publishes a sitemap at another URL.' );
}
if ( ! is_string( $robots_body ) || trim( $robots_body ) === '' ) {
$results[] = wpd_result_item( 'Sitemap reference in robots.txt', 'Unable to verify', 'A sitemap reference could not be checked because robots.txt was not readable.' );
} elseif ( $robots_lists_sitemap ) {
$results[] = wpd_result_item( 'Sitemap reference in robots.txt', 'Pass', 'robots.txt contains at least one valid, publicly addressable sitemap URL.' );
} else {
$results[] = wpd_result_item( 'Sitemap reference in robots.txt', 'Detected — Please verify', 'No sitemap reference was detected in robots.txt. A sitemap can still exist without being listed there.' );
}
return array( 'url' => $url, 'http_code' => $http_code, 'results' => $results );
}
function wpd_render_results( $scan ) {
if ( isset( $scan['error'] ) ) { echo '<div class="wpd-error">' . esc_html( $scan['error'] ) . '</div>'; return; }
echo '<div class="wpd-results"><h3>Website Health Report</h3><p class="wpd-scanned-url">Scanned address: ' . esc_html( $scan['url'] ) . '</p>';
foreach ( $scan['results'] as $item ) {
$status_class = sanitize_html_class( strtolower( str_replace( array( ' ', '—' ), array( '-', '-' ), $item['status'] ) ) );
echo '<div class="wpd-result-item"><div class="wpd-result-heading"><strong>' . esc_html( $item['name'] ) . '</strong>';
echo '<span class="wpd-status wpd-status-' . esc_attr( $status_class ) . '">' . esc_html( $item['status'] ) . '</span></div><p>' . esc_html( $item['detail'] ) . '</p></div>';
}
echo '</div>';
}
function wpd_render_shortcode() {
$scan = null;
if ( isset( $_SERVER['REQUEST_METHOD'] ) && strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) === 'POST' && isset( $_POST['wpd_scan_submit'] ) ) {
$nonce = isset( $_POST['wpd_scan_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['wpd_scan_nonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'wpd_scan_action' ) ) {
$scan = array( 'error' => 'Your form session could not be verified. Refresh the page and try again.' );
} else {
$input = isset( $_POST['wpd_url'] ) ? sanitize_text_field( wp_unslash( $_POST['wpd_url'] ) ) : '';
$url = wpd_normalize_url( $input );
if ( is_wp_error( $url ) ) { $scan = array( 'error' => $url->get_error_message() ); }
else { $scan = wpd_scan_website( $url ); }
}
}
ob_start();
?>
<style>
.wpd-wrap{max-width:920px;margin:30px auto;color:#253142;font-family:inherit}
.wpd-hero{padding:32px 24px;border-radius:14px;background:#263746;color:#fff;text-align:center;box-sizing:border-box}
.wpd-hero h2{margin:0 0 10px;color:#fff;font-size:30px;line-height:1.25}
.wpd-hero p{margin:0;color:#edf2f6;line-height:1.6}
.wpd-form{display:flex;align-items:stretch;gap:10px;margin:24px 0}
.wpd-form input[type="text"]{flex:1;min-width:0;padding:14px;border:1px solid #cbd5df;border-radius:7px;background:#fff;color:#253142;font-family:inherit;font-size:16px;box-sizing:border-box}
.wpd-form input[type="text"]:focus{outline:none;border-color:#087f8c;box-shadow:0 0 0 2px rgba(8,127,140,.12)}
.wpd-form button[type="submit"]{padding:14px 20px;border:0;border-radius:7px;background:#087f8c;color:#fff;font-family:inherit;font-size:15px;font-weight:700;white-space:nowrap;cursor:pointer;transition:background .2s ease}
.wpd-form button[type="submit"]:hover{background:#066772;color:#fff}
.wpd-results{margin-top:30px}.wpd-results h3{margin-bottom:8px;color:#263746}.wpd-scanned-url{overflow-wrap:anywhere;color:#555}
.wpd-result-item{margin:12px 0;padding:16px;border:1px solid #e0e5ea;border-radius:8px;background:#fff}
.wpd-result-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.wpd-result-item p{margin:10px 0 0;line-height:1.6;overflow-wrap:anywhere;color:#4b5563}
.wpd-status{display:inline-block;padding:5px 9px;border-radius:4px;background:#eef2f6;color:#344054;font-size:12px;font-weight:700}
.wpd-status-pass{background:#e6f6ec;color:#176b36}.wpd-status-issue-found{background:#fff0ed;color:#a52b1d}
.wpd-status-detected---please-verify{background:#fff5d8;color:#795500}.wpd-status-unable-to-verify{background:#eef2f6;color:#344054}
.wpd-error{margin:20px 0;padding:15px;border:1px solid #f1c6c2;border-radius:7px;background:#fff1f0;color:#922b21;overflow-wrap:anywhere}
@media(max-width:600px){.wpd-hero{padding:25px 18px}.wpd-hero h2{font-size:25px}.wpd-form{flex-direction:column}.wpd-form input[type="text"],.wpd-form button[type="submit"]{width:100%}}
</style>
<div class="wpd-wrap">
<div class="wpd-hero"><h2>WordPress Doctor</h2><p>Check important technical and SEO elements of your website.</p></div>
<form class="wpd-form" method="post">
<?php wp_nonce_field( 'wpd_scan_action', 'wpd_scan_nonce' ); ?>
<input type="text" inputmode="url" autocomplete="url" id="wpd_url" name="wpd_url" placeholder="example.com" aria-label="Website address" required>
<button type="submit" name="wpd_scan_submit" value="1">Check My Website</button>
</form>
<?php if ( is_array( $scan ) ) { wpd_render_results( $scan ); } ?>
</div>
<?php
return ob_get_clean();
}