=== Tidy Broken Link Scan – Broken Link & Image Checker ===
Contributors: pierreferrolliet
Tags: broken links, link checker, 404, dead links, images
Requires at least: 6.0
Tested up to: 7.0
Requires PHP: 8.2
Stable tag: 2.5.5
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Scan your posts, pages and custom post types for broken links and images — href, src, srcset included. No external service required.

== Description ==

Tidy Broken Link Scan checks every published post, page and public custom post type on your WordPress site for broken links and images. It checks `<a href>`, `<img src>`, `<img srcset>`, and `<source srcset>` — all natively, using the WordPress HTTP API. No SaaS, no API key, no external dependency.

**Features:**

* Detects broken links (4xx, 5xx, timeouts)
* Checks images including `srcset` and `<picture>` elements
* Automatic daily scan via WP-Cron
* Instant re-scan when you save a post
* Admin dashboard with filtering (all / broken / ok)
* Direct link to edit the source post
* Works on any WordPress hosting

**Need a full-site crawl?** This plugin checks the links and images inside your published posts, pages and custom post types. For a complete crawl of your whole site — every URL discovered through your sitemap, plus SSL and redirect-chain checks — you can run a free scan with our companion cloud scanner at [brokenlinkscan.com](https://brokenlinkscan.com). It runs in your browser, needs no installation, and is made by the same author.

= More from Tidy Plugins =

If you like this plugin, take a look at the rest of the Tidy suite:

* [Tidy Table of Contents](https://wordpress.org/plugins/tidy-table-of-contents/) — automatic, accessible table of contents
* [Tidy Draft Share](https://wordpress.org/plugins/tidy-draft-share/) — share drafts via secure, expiring preview links
* [Tidy Author Box](https://wordpress.org/plugins/tidy-author-box/) — author bio box with avatar and social links


== Installation ==

1. Upload the `tidy-broken-link-scan` folder to `/wp-content/plugins/`
2. Activate the plugin in **Plugins → Installed Plugins**
3. Go to **Liens cassés** in the admin menu
4. Click **Scanner tout le site** to run your first scan

== Frequently Asked Questions ==

= Will it slow down my site? =
No. Scans run in the background via WP-Cron and are never triggered by front-end visitors.

= Does it check images? =
Yes — `<img src>`, `<img srcset>`, and `<source srcset>` are all parsed and checked.

= How often does it scan? =
Once per day automatically (at 03:00 server time). You can also trigger a full scan manually or re-scan individual posts on save.

= Can I scan a single post? =
Posts are automatically re-scanned every time you publish or update them.

= A link works in my browser but is reported as broken. Why? =

Some servers refuse automated requests, or answer a HEAD request differently from a real visit. Open the link, confirm it is fine, then use Ignore on that row: it stays out of your broken count without being deleted.

= What does the health score mean? =

It is the share of checked links that are broken. Above 3% the grade drops sharply, because at that level readers and search engines hit dead ends regularly.

= Does it follow redirects? =

Yes, and a link that ends on a working page is not reported as broken. The Fix redirect action rewrites the link to its final destination so readers skip the extra hop.

= Can I exclude a domain that always fails? =

Yes. Settings has a domain exclusion list, plus a URL exclusion list for one-off cases. Social networks that systematically block crawlers are skipped by default.

= What happens when I use Fix? =

You edit the URL from the results table and the plugin rewrites it everywhere it appears in your content, so you do not have to open each post.

= Does it send my URLs to an external service? =

No. Every check is made from your own server with the WordPress HTTP API. The optional cloud scanner is a separate tool you visit yourself.

== External services ==

This plugin sends HTTP requests to third-party URLs as its core functionality (broken link checking). The list of contacted URLs is dynamic: it consists of the external links and image sources found in your own published posts, pages, and custom post types.

* What is sent: HTTP HEAD (with GET fallback) requests using the WordPress HTTP API. The plugin does not transmit any user-identifying data; only the URL itself is requested.
* When: requests are issued during a scan, which is triggered manually from the admin dashboard, automatically once per day via WP-Cron, or when you save a post (re-scan of that post only).
* Why: to determine whether each link returns a 2xx/3xx response (OK) or a 4xx/5xx/timeout response (broken).

Because the contacted URLs are entirely determined by the content of your own site, the plugin does not rely on any single fixed third-party service. A small allow-list of well-known domains (Gravatar, gstatic, wordpress.com, Cloudflare, etc.) is skipped from scanning to avoid noise; no data is sent to those domains by the plugin itself.

== Upgrade Notice ==

= 2.5.5 =
Fixes a bug where "Fix redirect" could corrupt other URLs in your content. If you used that button, search your posts for "//" after the domain name.

= 2.4.1 =
Code cleanup: all licence checks and premium-feature gating removed from the free plugin.

= 2.4.0 =
New features: broken anchor detection, redirect chain tracking, Slack notifications, scan diff, and health score dashboard.

= A link works in my browser but is reported as broken. Why? =

Some servers refuse automated requests, or answer differently to a HEAD request than to a real visit. Open the link, confirm it is fine, and use Ignore on that row — it will stay out of your broken count without being deleted.

= What does the health score mean? =

It is the share of checked links that are broken. Above 3% the grade drops sharply, because at that level readers and search engines are hitting dead ends regularly. The aim is to keep it at A.

= Does it follow redirects? =

Yes, and a link that ends on a working page is not reported as broken. The Fix redirect action lets you rewrite the link to its final destination so readers skip the extra hop.

= Can I exclude a domain that always fails? =

Yes. Settings has a domain exclusion list, plus a URL exclusion list for one-off cases. Social networks that systematically block crawlers are skipped by default.

= What happens when I use Fix? =

You edit the URL directly from the results table and the plugin rewrites it everywhere it appears in your content. There is no need to open each post.

= Does it send my URLs to an external service? =

No. Every check is made from your own server with the WordPress HTTP API. The optional cloud scanner mentioned above is a separate tool you have to visit yourself.

== Screenshots ==

1. Dead external links, filtered — dead domains and 404s, with fix, rescan and ignore on every row
2. Scan dashboard — health score, broken links and missing images at a glance
3. Scan history — the week-to-week trend and what changed since the last run
4. Settings — caching, image and menu scanning, domain and URL exclusions

== Changelog ==

= 2.5.5 =
* Fixed: "Fix redirect" rewrote every URL that merely started with the one being corrected. Changing /page to /page/ also turned an existing /page/ into /page// and /page-two into /page/-two, breaking links that were fine. Only a whole URL is replaced now.
* Fixed: "Fix redirect" was offered on healthy links. WordPress redirects /page to /page/, and that alone was enough to show the button on a link returning 200. It now appears only when the destination is a genuinely different page.
* Fixed: the confirmation box ran its own question mark into the proposed URL, which read as if a "?" was about to be added to it.

= 2.5.4 =
* Fixed: results left behind by a post that is no longer scanned (unpublished, trashed) were never re-checked nor removed, so they kept counting as broken on the dashboard forever. A full scan now clears them.
* Fixed: an image or link whose URL contains an accent or a symbol (é, ©) was reported as a 404. Absolute URLs were being detected with `FILTER_VALIDATE_URL`, which is ASCII-only, so those URLs were treated as relative and the site origin was prepended a second time.

= 2.5.3 =
* Fixed: links pointing to a domain that no longer resolves are now reported. They were being marked healthy, which hid the most common kind of broken link.
* Fixed: the retry counter is no longer reset at the start of every scan, so a link that keeps failing actually reaches the three-strike threshold and gets reported.
* Fixed: the category filters on the dashboard showed raw HTML instead of their icons.

= 2.5.2 =
* The review prompt now waits until the plugin has actually run two scans, appears only on the plugin's own screens, and states what your last scan checked instead of just counting days since install.

= 2.5.1 =
* The dashboard cloud-scanner tip now prefills your site URL, so the companion full-site scan on brokenlinkscan.com starts one click away.

= 2.5.0 =
* Scans now cover every public post type (posts, pages and custom post types) instead of only posts and pages — full scans, the daily auto-scan, the on-save re-scan, the post editor meta box and WP-CLI all use the same scope. Added a `blsc_scannable_post_types` filter to customise the list.

= 2.4.9 =
* Scan history now shows every recorded entry (up to the 30 the plugin stores) instead of being capped.
* Domain and URL exclusions are now fully configurable from the Settings screen — enter the domains or URLs to skip during scans.

= 2.4.8 =
* Replaced all direct cURL calls with the WordPress HTTP API (`wp_remote_*`) for link and image checking, including the batch scanner. Redirects are now followed via the HTTP API while still reporting redirect chains and loops.
* WP-CLI `blc export` now writes CSV to STDOUT only (use shell redirection, e.g. `wp blc export > report.csv`); the arbitrary `--file` path option was removed.

= 2.4.7 =
* WordPress.org coding-standards hardening: explicit output escaping in the admin dashboard and post meta box, documented safe `$wpdb` queries, and translators comments for all i18n placeholders (Plugin Check compliance).

= 2.4.6 =
* Added a dismissible tip on the dashboard linking to the companion cloud scanner (brokenlinkscan.com) for a full-site crawl beyond posts and pages.

= 2.4.5 =
* Replaced PHP short echo tags (`<?=`) with full `<?php echo` in the post meta box (WordPress.org coding-standards compliance).
* Bumped "Tested up to" to WordPress 7.0 and aligned the readme plugin name with the plugin header.

= 2.4.4 =
* Hardened admin output escaping: replaced `phpcs:ignore` suppressions with explicit `wp_kses()`, `esc_attr()` and `(int)` casts in the dashboard view (WordPress.org coding-standards compliance).

= 2.4.3 =
* Document outbound HTTP scanning behavior under the new "External services" section of the readme (WordPress.org guideline 6 compliance).

= 2.4.2 =
* Removed the ACF field scanner from the free plugin (the code only ran when the Pro add-on enabled the related setting). ACF scanning is now provided entirely by the companion Pro plugin via the new `blsc_extra_links` filter.

= 2.4.1 =
* Removed all licence checks and premium-feature gating from the free plugin code (premium features — auto-scan cron, email/Slack notifications, ACF scanning, exclusions, CSV/XLSX export — are now distributed via a separate companion plugin).
* Removed the now-unnecessary `load_plugin_textdomain()` call (handled by WordPress.org translation system since WP 4.6).
* Added extension hooks for add-on plugins (settings defaults/sanitisation, scan-complete notification, scan/exclusion filters, admin UI).

= 2.4.0 =
* Added: Broken anchor detection (missing #id targets)
* Added: Redirect chain tracking and reporting
* Added: Slack notifications for scan results
* Added: Scan diff — compare results between consecutive scans
* Added: Health score dashboard with summary cards

= 2.2.0 =
* Fixed: Plugin header version now matches internal constant
* Fixed: SSL_VERIFYHOST set to 2 (was incorrectly disabled, MITM vulnerability)
* Fixed: Plugin action links basename corrected (Settings link now works on Plugins page)
* Fixed: Double scan race condition — pending state now blocks duplicate starts
* Fixed: Raw $_POST passed to settings save without wp_unslash()
* Fixed: save_scan_history() query now uses $wpdb->prepare()
* Improved: Nonce caching in list table rows (performance)
* Improved: Hardcoded French strings in admin.js replaced with i18n keys
* Removed: Dead code (page_by_post method, by-post.php template, unused CSS, blsc_manual_ok_urls option)
* Added: Missing admin notice keys (settings_saved, export_empty)

= 2.1.0 =
* Parallel HTTP checks via curl_multi
* Email alerts for new broken links
* Scan history and delta reporting
* XLSX export support
* Ignore list with persistent overrides
* By-post view integrated into dashboard

= 1.0.0 =
* Initial release
