Short verdict [ChatGPT 5.4]
I’d rate it about 6.5/10 overall 👍
It’s a good minimalist prototype and it does cover the core requirements:
- backend textarea for URLs
- periodic checks via WP-Cron
- saving results
- frontend shortcode output
- pruning data older than 365 days
But as a real plugin for production use, it has a few important weaknesses and a couple of actual bugs / design issues.
What’s good
The plugin already does several things well:
-
Very simple UX
- The admin UI is easy to understand.
- One URL per line is exactly the right minimalist input format.
-
Uses core WordPress APIs
register_setting()wp_schedule_event()wp_remote_head()- shortcode API
-
Keeps data structure simple
- Storing current status plus history per URL is easy to reason about.
-
Frontend is lightweight
- Clean table output
- No external CSS/JS dependencies
-
Retention logic exists
- Old history is pruned, which is important.
The main issues
1. Activation hook inside the class constructor is problematic
This is one of the biggest issues.
You register:
register_activation_hook( __FILE__, [ $this, 'activate' ] );
register_deactivation_hook( __FILE__, [ $this, 'deactivate' ] );
inside __construct().
This often works, but it’s not the cleanest or safest pattern. Activation hooks should ideally be registered in the main plugin file scope, not buried in runtime object construction. It can lead to surprises depending on load timing and plugin structure.
Better
Use static methods and register them outside the instantiated object:
register_activation_hook( __FILE__, [ 'URL_Health_Monitor', 'activate' ] );
register_deactivation_hook( __FILE__, [ 'URL_Health_Monitor', 'deactivate' ] );
and make those methods public static.
2. sslverify => false is a bad security practice
This is a clear red flag 🚩
'sslverify' => false,
Disabling SSL verification means HTTPS checks are less trustworthy and less secure. Unless there is a very specific reason, this should not be disabled.
Better
Just remove it, or explicitly set:
'sslverify' => true,
3. wp_remote_head() alone is not reliable
Many websites do not respond correctly to HEAD requests. Some block them, some misconfigure them, and some return different status codes than for GET.
That means the plugin may report false negatives.
Better approach
- Try
HEADfirst - If that fails or returns an unusable response, fall back to a lightweight
GET
For example:
$response = wp_remote_head( $url, $args );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) === 0 ) {
$response = wp_remote_get( $url, $args );
}
4. Manual check form processing is done after output starts
In render_admin_status_table(), the form submission is handled after some HTML is already printed, and then a meta refresh is used:
echo '<meta http-equiv="refresh" content="0">';
This is clunky and not ideal.
Why this is not great
- not a proper redirect
- mixes display logic with request handling
- can be unreliable
- not very WordPress-like
Better
Handle the action via:
admin_post_*- or detect submit at top of admin page and use
wp_safe_redirect()
5. Old data is only pruned when checks run
The requirement says data older than 365 days should be deleted. Technically this happens, but only during check_all_urls().
Edge case
If cron doesn’t run for a while, old data stays longer than intended.
Better
Prune in more places, for example:
- before rendering frontend
- before rendering admin
- during scheduled checks
That makes retention more consistent.
6. Inline CSS is output every time the shortcode is used
The <style> block is embedded directly in shortcode output.
This is okay for a tiny plugin, but not ideal.
Downsides
- duplicate CSS if shortcode appears multiple times
- less cacheable
- harder to override in themes
Better
Register/enqueue a small stylesheet.
7. No uninstall cleanup
On deactivation, cron is cleared, but plugin options remain forever.
That may be okay for some plugins, but usually it’s nice to offer cleanup on uninstall.
Better
Add uninstall.php or register_uninstall_hook() to remove:
uhm_urlsuhm_results
if complete cleanup is desired.
8. Storing all history in one option may become inefficient
This is an architectural limitation.
Right now everything is stored in one option:
uhm_results
For a small number of URLs and hourly checks, this is fine. But over time:
- lots of URLs
- 365 days of history
- repeated writes to one large serialized option
can become inefficient.
Rough example
If you monitor many URLs hourly for a year, history can get quite large.
Better options
- keep only summary stats in options
- store history in a custom database table
- limit history granularity
For a minimalist plugin, this is not a deal-breaker, but it is a scalability concern.
9. Uptime calculation is simplistic
Current uptime is based on:
number of successful checks / total checks
That’s acceptable, but not a true time-weighted uptime calculation.
Why it matters
If cron is irregular, the uptime number may be misleading.
For example:
- one check succeeds
- site is down for 10 hours
- next check succeeds
The uptime percentage may still look artificially good.
For a minimalist plugin
This is okay. But it should be understood as check success rate, not perfect uptime monitoring.
You may want to label it more honestly.
10. Removed URLs are deleted immediately, including history
In prune_results(), any URL no longer present in the textarea is removed entirely:
if ( ! in_array( $url, $current_urls, true ) ) {
unset( $results[ $url ] );
continue;
}
That may be intended, but it’s worth noting.
Potential issue
A user may accidentally delete a URL from the textarea, save, and lose all stored history immediately.
Possible improvement
Keep removed URLs for a grace period, or have an “active/inactive” state.
For a minimalist plugin, immediate deletion may still be acceptable.
Actual bugs or likely bugs
1. unset() inside foreach by reference is risky
You have:
foreach ( $results as $url => &$data ) {
if ( ! in_array( $url, $current_urls, true ) ) {
unset( $results[ $url ] );
continue;
}
...
}
This can work, but mixing foreach by reference with unsetting array elements is a bit risky and can cause subtle issues.
Better
Loop without reference and assign back explicitly, or build a new pruned array.
For example:
$pruned = [];
foreach ( $results as $url => $data ) {
if ( ! in_array( $url, $current_urls, true ) ) {
continue;
}
$data['history'] = array_values(array_filter($data['history'], function ($entry) use ($cutoff) {
return ! empty($entry['time']) && $entry['time'] >= $cutoff;
}));
$pruned[$url] = $data;
}
return $pruned;
2. No fallback if cron event already exists with a different schedule
On activation you only schedule if not already scheduled:
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
wp_schedule_event( time(), 'hourly', self::CRON_HOOK );
}
If later you change the interval in code, existing installs may keep the old schedule.
Better
Unschedule and reschedule when needed, especially on version upgrades.
3. Status code 0 is displayed for transport errors without message
If a request fails with a WP_Error, you store:
$status_code = 0;
That means the frontend says “Offline (0)”, which is not very informative.
Better
Store the error message too:
'error' => $response->get_error_message()
Then display something like:
- Offline
TimeoutCould not resolve hostSSL certificate problem
That would make the plugin much more useful.
4. Potentially no data until cron fires, which confuses users
The plugin says results will appear after the first scheduled check. That’s true, but in WordPress, cron may not fire quickly on low-traffic sites.
Better UX
Run an initial check on activation, or after saving URLs.
That would significantly improve first-run experience.
5. Processing POST in rendering function is messy
Not exactly a fatal bug, but definitely a code smell.
The admin rendering function both:
- outputs HTML
- validates nonce
- processes POST
- triggers checks
This should be separated.
Best improvements I’d recommend first
If I were improving this plugin while keeping it minimalist, I’d prioritize these:
- Remove
sslverify => false - Add HEAD → GET fallback
- Process manual checks cleanly with redirect
- Store error messages, not just status code
0 - Prune data before display as well
- Move activation/deactivation hooks outside constructor
- Run checks when URLs are saved or on activation
- Avoid storing large long-term history in one option if scale matters
A cleaner rating by category
| Area | Rating | Comment |
|---|---|---|
| Simplicity | 9/10 | Very easy to understand |
| WordPress integration | 7/10 | Mostly good, some structural issues |
| Security | 5/10 | sslverify => false hurts the score |
| Reliability | 6/10 | HEAD-only checks can misreport status |
| Scalability | 5/10 | Single-option history storage is limited |
| UX | 7/10 | Good basic UX, but first-run experience could improve |
| Maintainability | 6/10 | Reasonably readable, but request handling and hook registration could be cleaner |
Overall recommendation
As a minimal MVP: yes, it’s solid enough.
As a production-ready plugin: not yet.
If you want, I can do one of these next:
- provide an improved version of the plugin
- rewrite it in a more production-ready way while keeping it minimalist
- convert storage from options to a custom database table
- add nicer frontend cards/table styling without making it bloated
If you want, I can now give you a revised and improved plugin version based on the points above.