=== AlgoRadar ===
Contributors: ceotech
Donate link: https://paypal.me/ceotechit
Author: Francesco Palmieri
Author URI: https://www.ceotech.it/
Tags: seo, google, content audit, technical seo, search console
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 0.3.11
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Estimates your site's exposure to major Google algorithm updates with a local, transparent heuristic model, at no cost.

== Description ==

AlgoRadar analyzes the content already published on your WordPress site and calculates two indicative scores:

* **Core Exposure Score** — exposure to content-quality factors typically involved in core updates (thin content, duplicates, weak internal linking, technical issues).
* **Spam Exposure Score** — exposure to patterns typical of spam updates (risky outbound links, keyword stuffing, doorway pages).

Both scores come from a **local, configurable** heuristic model, never from Google's actual algorithm: AlgoRadar is not a Google product, does not replicate its inner workings, and does not predict real penalties. Every factor that contributes to a score is shown transparently (data point, reason, weight, suggestion), so the score is never a number without an explanation.

= Built to go easy on your hosting =

Where possible, AlgoRadar reads data directly from the WordPress database (content, internal links, metadata) instead of scanning the site over HTTP. Actual HTTP requests are reserved only for the signals that genuinely need them, such as real response time. The background scan uses WP-Cron with chained batch execution, with a browser-driven fallback for hosting environments where background execution isn't reliable.

= No paid services =

AlgoRadar works exclusively with data already present on your site and, if you connect it, with the official, free Google Search Console API to compare scores against real clicks and impressions. No integration with paid SEO services.

== Installation ==

1. Upload the `algoradar` folder to `/wp-content/plugins/`, or install the zip via Plugins > Add New > Upload Plugin.
2. Activate the plugin from the WordPress Plugins menu.
3. Go to AlgoRadar > Settings to review the default values (no configuration is required to get started).
4. Go to AlgoRadar > Dashboard for your site's status.

== Frequently Asked Questions ==

= Are the scores official Google metrics? =

No. They're calculated by a local heuristic model included in the plugin, with weights you can change yourself in Settings. They don't come from internal Google documentation and don't guarantee or predict any action by Google on your site.

= Does the plugin send my data to external services? =

No, by default. The only optional external connection is to the official Google Search Console API, which you can enable voluntarily from the Settings page, and it's used only to compare scores against real organic traffic data.

= Does it need a system cron? =

No. AlgoRadar uses WP-Cron for background execution and, if your hosting doesn't run it reliably, falls back to browser-driven batch processing. If neither is reliable enough on your host, `wp algoradar scan` (requires WP-CLI) runs a scan to completion from a real system cron job, without depending on WP-Cron or a browser tab.

= How do I get support? =

Write to info@ceotech.it for questions, bug reports, or requests.

== External services ==

AlgoRadar connects to one third-party service, and only if you explicitly turn it on: the official **Google Search Console API**. It is off by default and every scoring feature works without it.

**What it's for**: on the Compare page, if you choose to connect your Google account, AlgoRadar fetches your site's real clicks and impressions per URL from Search Console so you can see them alongside the plugin's own Core/Spam Exposure Scores.

**What is sent, and when**:

* Connecting: when you click "Connect" in AlgoRadar > Settings, your browser is redirected to Google's OAuth consent screen (`https://accounts.google.com/o/oauth2/v2/auth`) to authorize read-only access to your Search Console data. No site data is sent at this step, only the standard OAuth request.
* Token exchange/refresh: after you authorize, the plugin exchanges the authorization code (and later, silently, the refresh token) for an access token by calling Google's token endpoint (`https://oauth2.googleapis.com/token`). This request includes the OAuth credentials you entered (Client ID/Secret) and the token/code itself; no content from your site is sent.
* Fetching data: when you open the Compare page with a Search Console property selected, the plugin calls the Search Console API (`https://www.googleapis.com/webmasters/v3`, read-only scope) to request clicks/impressions per URL for your own site's property, for the date range being compared. This happens only on that page load, only for a connected account, and never during the background scan itself.

No content, personal data of your visitors, or site data other than your own page URLs and the date range being queried is ever sent to Google. You can disconnect at any time from AlgoRadar > Settings, which deletes the stored tokens.

This integration is governed by Google's own terms: [Google APIs Terms of Service](https://developers.google.com/terms/) and [Google Privacy Policy](https://policies.google.com/privacy).

== Screenshots ==

1. Dashboard: Core and Spam Exposure Score at a glance, with a prioritized list of pages to check first.
2. Page detail: every factor behind the score, with its data point, reason, weight, and suggestion — never a number without an explanation.
3. Pages: every analyzed URL with its scores and priority.
4. Compare: track scores between two scans over time, and correlate them with real Search Console clicks and impressions.
5. Settings: every weight and threshold is visible and adjustable, never a hidden default.

== Changelog ==

= 0.3.11 =
* Added two new Core Exposure Score factors, both free local checks on the page's own real HTML, no paid service involved: Mobile Viewport (presence of the `<meta name="viewport">` tag) and Structured Data (presence of JSON-LD or microdata). Both are presence checks, not full validity checks, and neither can force Priority 1 on its own.

= 0.3.10 =
* Fixed a false-positive risk in Keyword Stuffing on review-style content: the factor now excludes words that already appear in the page's own title before counting repetitions, so a product or brand name repeated throughout a review (expected on this kind of content) is no longer counted as unnatural repetition.
* Renamed the "Freshness/Authorship" factor to "Freshness": it has only ever measured how long ago a page was last updated, with no authorship signal (author bio, byline) behind it, so the name promised more than the factor actually checked.

= 0.3.9 =
* Fixed a false-positive risk in the risk-category and commercial-keyword detection (External Link Risk, Commercial Density, Sponsored Disclosure): a keyword was previously matched as a plain substring anywhere in a domain or link text, so an unrelated word that happened to contain the same characters (for example inside a longer word) could be flagged. Matching now requires a real word boundary.

= 0.3.8 =
* Added Support, Donate and Write a review links to the plugin's row on the Installed Plugins screen, the same convenience links many WordPress.org plugins show (this isn't automatic: each plugin has to add it in its own code).
* Added a `wp algoradar scan` WP-CLI command that runs a scan to completion from a real system cron job, for hosting where WP-Cron isn't reliable enough and the browser-driven fallback isn't practical either (it requires a page kept open). Fully optional: WP-Cron and the browser fallback keep working exactly as before if you don't use it.

= 0.3.7 =
* Added the WordPress.org listing assets: plugin icon, banner and 4 admin screenshots.
* Re-tagged to pick up the Donate link added after 0.3.6 was tagged: the plugin page reads its readme from the Stable tag's own folder, not from trunk, so a change committed only to trunk after a tag never appears on the public page until the next tag.

= 0.3.6 =
* Added an "External services" section to this readme documenting the optional Google Search Console connection: what is sent to Google, when, and links to Google's own Terms of Service and Privacy Policy.
* Removed the pre-built translation files from the plugin package. WordPress.org generates and serves translations for approved plugins itself once volunteer translations exist, so shipping them here was redundant.

= 0.3.5 =
* Fixed the actual cause of the scan getting permanently stuck right at the end of site indexing (100% of contents read, never moving to the analysis phase) on sites with a large number of published, long-form contents: finding duplicate/near-duplicate pages compared the full set of text fingerprints of every single page against every other page's, all held in memory at once. A long article can produce hundreds or thousands of these fingerprints, and multiplied by tens of thousands of pages this could exceed the memory PHP is allowed to use, crashing the request outright with no valid response, exactly where this kept happening. Each page's fingerprint is now reduced to a small fixed-size sample (a standard technique for comparing large numbers of documents cheaply) that still reliably identifies near-duplicate pages, cutting the memory this step needs by roughly 80-90x. As a safety net on top, this step now also recovers instead of leaving the scan stuck if something about it still fails unexpectedly, and a runaway number of pages to compare against each other is capped rather than left unbounded.
* A scan already stuck at this exact point (100% indexed, stuck) cannot recover on its own even after updating, because the comparison data it already collected was captured before this fix in the old, unbounded format: start a new scan after updating.

= 0.3.4 =
* Fixed the same "error during the scan" from 0.3.3 still appearing on very large sites, this time during site indexing itself rather than scoring: reading and analyzing 200 contents in one batch (no HTTP requests involved, just database reads and text processing) can still take longer than expected on some hosting/content combinations, and a batch that ran long could hit the same server execution-time wall the 0.3.3 fix addressed only for the scoring phase. Indexing batches now also stop and hand off the rest of their work if they run long, the same safeguard already applied to scoring in 0.3.3.

= 0.3.3 =
* Fixed the actual cause of "An error occurred during the scan" appearing partway through, on sites with a large number of published contents: the scoring phase claimed a full batch of pages upfront and analyzed every one of them (including a real HTTP check per page) before answering, with no limit on how long that could take; a batch that included several slow or unresponsive pages could run past the server's execution time limit, killing the request before it could reply, which the browser then reported as an error instead of quietly continuing. Batches now also stop and hand off the rest of their work if they run long, regardless of how many pages were assigned to them.
* Fixed the same kind of issue at the point site indexing finishes: finishing the index used to write its duplicate-content findings one page at a time, meaning a site with tens of thousands of contents made just as many individual database updates in that single moment. This is now done in bulk, a page's write is no longer lost after a slow batch (it's picked up again immediately, not after the usual 10-minute safety window), and a batch that still fails to reply now gets a few automatic, increasingly-spaced retries before showing an error, instead of stopping at the very first blip.

= 0.3.2 =
* Fixed the resumed background execution added in 0.3.1: opening (or reloading) the Scan page while a scan was already running only read and displayed its status, without also driving it forward, so on hosting where WP-Cron isn't reliable enough for a scan that needs many sequential batches, progress could stall after the very first batch. The page now also resumes actively processing batches in that case, exactly like right after clicking "Start", instead of only showing numbers that don't move.
* Fixed the progress display mixing counts from different phases: the number of contents read during site indexing and the number of pages scored are two different things, and adding them together could show a confusing, occasionally over-100% progress. The bar and text now always reflect the current phase's own count.

= 0.3.1 =
* Fixed a scan that could never complete on sites with a very large number of published contents: creating the scan inserted one database row per content and read every content's text in a single request to build the internal-link/duplicate-content index, which on a large site could exceed the server's execution time (or a proxy's request timeout) before the scan was even handed off to background processing, leaving it stuck at 0% forever. Row creation is now done in bulk instead of one query per content, and the site index is now built in the same small background batches already used for scoring, so a scan never does more than a few seconds of work in a single request regardless of site size.
* Added a safeguard for the rare case a scan still gets interrupted mid-run (e.g. a genuine server crash): if a scan shows no progress at all for more than 10 minutes, the Scan page now offers to start a new one instead of staying stuck indefinitely.
* The Scan page now shows live progress (including during the initial site-indexing step) without needing a manual reload, when a scan is already running when the page is opened or reloaded.

= 0.3.0 =
* Fixed a race condition in background batch processing: the immediate loopback trigger and its wp_cron safety net could, if close enough together, both pick up the same pending rows and analyze them twice. Batches are now claimed atomically with a per-call token, and rows stuck mid-batch by a server timeout are automatically reclaimed after 10 minutes instead of stalling the scan forever.
* Removed a redundant database query per duplicate page: cluster size is now computed once during site indexing and stored on the row, instead of being recalculated on every scoring pass.
* Noindex and canonical are now also read from HTTP response headers (X-Robots-Tag, Link), not only from the page's HTML: some hosting setups and security plugins set these only at the header level.
* Added an optional Page Experience factor using real Core Web Vitals (LCP, CLS, INP) from the Chrome UX Report API: off by default, requires your own free API key, and simply skips pages without enough real-user data instead of scoring them as if there were no issue.
* Added a CSV export button on the Pages screen (priority, scores, top suggestion for every analyzed URL), useful for handing the list to an editorial team without opening each page's detail individually.
* Made the external-link risk keyword lists (gambling, loans, adult, pharma, replica, commercial keywords) extensible via the `algoradar_risk_categories` and `algoradar_commercial_keywords` filters, and documented in the Guide that they're Italian-language and tuned for Italian-language sites.

= 0.2.9 =
* Removed the Plugin URI header: it was identical to the Author URI, which the Plugin Directory submission form requires to be different. The plugin doesn't have a dedicated landing page of its own, so Author URI (https://www.ceotech.it/) is kept as the single point of contact.

= 0.2.8 =
* Added the WordPress.org listing assets: plugin banner and admin screenshots.

= 0.2.7 =
* Added an en_US translation (identical in substance to the existing en_GB one, no British-only spellings needed changing). Without it, the large majority of English-language WordPress installs, which default to en_US, would have fallen back to the plugin's Italian source strings instead of showing English.

= 0.2.6 =
* The fix in 0.2.5 removed the query's dynamic placeholder count from being visible to static analysis, which flagged it again for a different, unavoidable reason: PHPCS cannot verify what a variable holding a full SQL string contains before it reaches $wpdb->prepare(). Documented with an inline justification instead of restructuring further, since the value only ever comes from a fixed literal, WordPress's own $wpdb->posts, and dynamic %s/%d placeholders.

= 0.2.5 =
* Fixed a query that chained two $wpdb->prepare() calls into each other (one for the LIMIT clause, one for the IN() values): harmless in practice, but confusing for static analysis and for anyone reading the code. Now built as a single prepare() call with every value in one array.

= 0.2.4 =
* Fixed a missing translators comment on a string with ordered placeholders, and a prepared-statement call that PHPCS couldn't verify as safe because it was split across two statements: both are now resolved without changing behavior.
* Sanitized several $_GET/$_POST reads that were missing wp_unslash() before validation (Search Console OAuth routing, AJAX scan engine parameter, settings time window and duplicate-similarity threshold).
* Documented, with inline justification, the direct database queries against the plugin's own custom tables, the SQL table-name interpolation (never built from user input), and the local-variable naming in admin view files: patterns the WordPress.org review team already expects from plugins using custom tables and included templates.
* Removed load_plugin_textdomain(), unnecessary since WordPress 4.6 for a plugin declaring a Domain Path.
* Bumped "Tested up to" to the current WordPress version and set the real WordPress.org username in Contributors.
* Translated this readme into English, the language required for the main file in the Plugin Directory; the plugin's admin interface keeps its existing translations (Italian, English, French, German, Spanish).

= 0.2.3 =
* Fixed three factors marked as "severe" (able to force Priority 1 on their own) whose formula could never actually reach the required threshold: Canonical Tag (a canonical pointing to a completely different domain is now worth 95, not 80), Duplicate Content (risk now scales with the actual cluster size: 55 for a pair of similar pages, up to 95 for a cluster of 5+ near-identical pages), and Duplicate Pattern (added a 95 threshold for clusters of 6+ pages, typical of mass-generated content).
* Lowered the maximum Internal Linking risk from 100 to 75: the factor has not forced priority for a while, but a lower ceiling prevents a simple new or not-yet-linked page (the most common case on a site that publishes regularly) from still pulling the average score too high.
* Softened the Commercial Density multiplier (from 40 to 30): a high density of commercial links is normal on legitimate review/affiliate sites, so the maximum now requires a more pronounced density to be reached.

= 0.2.2 =
* Fixed a calibration bug: an orphan page (0 inbound internal links) or a factor disabled with weight 0 in Settings could on their own force Priority 1, even with no real issue present. Every factor is now marked either "severe" (a direct, confirmed problem: HTTP errors, noindex, exact duplicates, risky links) or "soft" (an indirect or gradual signal): only "severe" factors with a weight greater than zero can force maximum urgency; all factors still contribute normally to the weighted average.
* Optimized content reading during scanning: a single DOM parse per page instead of three, and a lighter read during site indexing that only computes the data it actually needs. Cuts per-page processing time by roughly 55-60%, especially useful on sites with a lot of content.

= 0.2.1 =
* New "Minimal Pro" design for the admin area's cards: circular gauges for scores, rounded corners, subtle shadows, more polished pills and badges, more readable tables. No functional changes, styling only.

= 0.2.0 =
* Added 5 new factors to the scoring model: Duplicate Title, Duplicate Meta Description, Heading Structure, Canonical Tag (Core Exposure Score) and Sponsored Disclosure (Spam Exposure Score).
* Recalibrated all default weights, with documented reasoning in the code for each value.
* Removed the "Maximum depth" setting, which was never actually connected to any real logic (the plugin reads published content from the database, it doesn't follow a depth-based crawl).
* Added a safety ceiling (200) to "URLs per batch" to avoid server timeouts on batches that are too large; it doesn't affect the total number of pages that can be analyzed, which stays unlimited by default.
* Added pagination to the Pages page (it used to show at most 200 results with no way to see the rest on larger sites).
* Fixed a bug where links with href="javascript:..." were counted as internal links.
* Added multilingual support: .pot file and complete translations in English, French, German and Spanish. More EU languages planned for future versions.

= 0.1.3 =
* Set Plugin URI to https://www.ceotech.it/.
* Created the plugin icon (an eye with a radar detail) for the WordPress.org listing, in assets/icon.svg, icon-128x128.png and icon-256x256.png.

= 0.1.2 =
* Updated Plugin URI to an unresolvable placeholder domain (no longer points to a real site).
* Added the "Guide" page with step-by-step instructions for first-time users.
* Added a support contact (info@ceotech.it) to the Guide page and the readme.
* Corrected the description of background execution: it uses WP-Cron with chained batch execution, not the Action Scheduler library.

= 0.1.1 =
* Fixed a missing database schema update when the plugin is replaced on an already-active installation (could cause scans to get stuck at 0 URLs).

= 0.1.0 =
* First development version: plugin structure, data tables, admin pages, settings. The scanning and scoring engine will be added in later versions.
