=== Stalemender – Content Audit, Stale Post Finder & Content Decay Report ===
Contributors: emtiazzahid
Tags: content audit, stale content, seo, search console, content decay
Requires at least: 6.5
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Scores every post for staleness, thin content, orphan pages and lost search traffic, then hands you a prioritised list of what to refresh.

== Description ==

Old posts quietly lose rankings. Stalemender audits everything you have published, gives each post a **Freshness Score (0–100)** and tells you *why* it scored that way, so refreshing content becomes a weekly habit instead of a yearly panic.

= What it looks at =

* **Age** since the last update (or last editorial review)
* **Search traffic trend** from Google Search Console: clicks in the last 28 days vs the 28 before
* **Outdated mentions**: old years, "this year", "recently", prices in headings – each one quoted in context, with a per-post ignore list
* **Length** compared with the typical post of the same type
* **Internal links pointing in** (orphan detection via a full link index)
* **Structure**: H2 headings, images, featured image
* **Meta description** (Yoast, Rank Math, AIOSEO, SEOPress, or the excerpt)
* **Broken outbound links** (light HEAD checks, cached, throttled per host)
* **Duplicate titles, H1s and near-duplicate slugs**

Weights are yours to change. Signals without data (no Search Console, outbound blocked) are skipped and their weight redistributed, so a score is never punished for a missing integration.

= Then it helps you act =

* **Audit table**: server-side pagination, sort and filter by type, category, author, band, status, assignee, traffic trend or modified date; save views per user; export the current view as CSV
* **Post drawer**: full score breakdown, outdated phrases in context, who links here, notes, status (needs update, in progress, reviewed, merge, delete), assignee, "mark reviewed" which resets the age clock
* **Refresh next**: a top-N list of non-fresh posts ranked by staleness × lost clicks (or × inbound links when Search Console is not connected), on the dashboard and in a widget
* **Weekly digest** email with band counts, trend and the refresh list
* **AI refresh brief** (optional, off by default): a Markdown brief saved as a note. On WordPress 7.0 and later it can use the AI provider you connect under Settings → Connectors, through WordPress's built-in AI Client, so there is no separate key to manage; on any version you can instead bring your own OpenAI / Anthropic / Gemini key. None of your content is sent until you confirm the token estimate.

= Built to be light =

* Custom tables; the only post meta written is your per-post list of ignored phrases, automatic pruning
* Audits run in Action Scheduler batches that adapt to your server's time limit and resume where they left off
* Re-scores a post when you save it; drops it when unpublished
* Search Console via a service-account JSON key (stored encrypted); nightly sync
* WP-CLI: `wp stalemender audit run|status|cancel`, `wp stalemender gsc sync|status`, `wp stalemender export --file=`, `wp stalemender score <id> --explain`

= Hooks for developers =

`stalemender_signals`, `stalemender_signal_weights`, `stalemender_post_types`, `stalemender_exclude_post`, `stalemender_score_computed`, `stalemender_outdated_phrases`, `stalemender_gsc_row`, `stalemender_refresh_list_size`, `stalemender_after_audit`, `stalemender_post_html`, `stalemender_builder_html`, `stalemender_http_args`, `stalemender_rescore_on_save`, `stalemender_capability`.

== External services ==

Stalemender scores your content on your own server and never sends anything to the plugin author. It contacts other servers only in the four cases below. Each one is off until you turn it on.

= Google Search Console (optional, off by default) =

Used for the "search traffic trend" signal. Nothing is sent until you paste a Google service-account key, pick a property and turn on the sync.

* Endpoints: `https://oauth2.googleapis.com/token` to obtain a short-lived access token, `https://www.googleapis.com/webmasters/v3/sites` to list the properties the service account can see when you open the property picker, and `https://www.googleapis.com/webmasters/v3/sites/{property}/searchAnalytics/query` to read performance data.
* Sent: a signed authentication token created from your service-account key (the private key itself never leaves your server), the property identifier you chose, and two date ranges (the last 28 days and the 28 days before). No post content or page URLs are sent.
* Received and stored: page URLs of your own site with their clicks, impressions and average position. Totals are stored per post in the plugin's scores table for as long as the post is published; any error message returned by Google and a list of up to 50 unmatched URLs are stored in options. Everything is deleted on uninstall unless "keep data" is enabled.
* When: whenever you click "Sync now", run `wp stalemender gsc sync`, and, while the nightly sync is enabled, automatically once a day at 03:00 site time. The access token is cached for about 55 minutes.
* Scope requested: Search Console read-only (`webmasters.readonly`). The key is stored encrypted in your database and can be removed at any time from Settings → Search Console.
* Google terms: https://developers.google.com/terms – Privacy: https://policies.google.com/privacy

= AI refresh brief (optional, off by default) =

To use it you turn it on under Stalemender → Settings → AI assist and choose where requests go:

* **WordPress AI** (WordPress 7.0 and later, the default for new installs there): the request goes through WordPress's built-in AI Client to the AI provider you connected under Settings → Connectors. WordPress chooses the provider and model and keeps the API key; Stalemender stores no key for it. The connectors WordPress offers are for the providers listed below, and the terms and privacy policy of the one you connected apply.
* **Your own key**: you enter an API key for one of the providers below, and the request goes directly from your server to that provider.

None of your content is sent until you open a post, click "AI refresh brief", read the token estimate and then click "Generate brief". It never runs automatically. With WordPress AI, clicking "AI refresh brief" can make WordPress ask the connected provider which models it offers; that request carries the API key but none of your content.

* Sent to the provider: the post title, its URL, last-modified date, freshness score and band, word count, number of internal inbound links, up to 20 headings, the outdated phrases found, the current meta description, the Search Console click counts if you connected it, and the first 6,000 characters of the post text. The API key is sent in the request header (your own key, or the one WordPress holds for the connected provider). Requests made with your own key do not include your site URL apart from the post's own permalink; requests made through WordPress AI use WordPress's standard HTTP User-Agent, which includes your site's address.
* Received: a Markdown brief, saved as a note on the post in your database. The number of tokens used is logged locally; the text of the request is not.
* If you use your own key, it is stored encrypted in your database; only its last four characters are ever shown again.
* Providers, and the endpoints Stalemender calls with your own key:
* OpenAI – `https://api.openai.com/v1/chat/completions` – Terms: https://openai.com/policies/terms-of-use – Privacy: https://openai.com/policies/privacy-policy
* Anthropic – `https://api.anthropic.com/v1/messages` – Terms: https://www.anthropic.com/legal/commercial-terms – Privacy: https://www.anthropic.com/legal/privacy
* Google Gemini – `https://generativelanguage.googleapis.com/v1beta/` – Terms: https://ai.google.dev/gemini-api/terms – Privacy: https://policies.google.com/privacy
* OpenAI-compatible endpoint – any base URL you enter, including a server on your own network. The plugin does not restrict this address; the terms and privacy policy of that service apply.

= Outbound link check (optional, off by default) =

The "broken outbound links" signal checks whether the external links in your own posts still work. To do that, your server contacts the third-party websites you link to. It is off until you switch it on, and there is no fixed provider: the addresses contacted are the ones already published in your own content, so their terms and privacy policies are those of the sites you chose to link to.

* What is sent: an HTTP HEAD request to the linked URL, or a GET request limited to 20 KB if the site refuses HEAD. The request identifies your site with the user-agent `Mozilla/5.0 (compatible; Stalemender/<version>; +https://your-site/)`, the normal courtesy for automated requests. No content, cookies or visitor data are sent. Links pointing at private or local network addresses are never requested.
* When: only while an audit you started is running, or when you click "Rescan" on a post. Never on page views, on post save, or on a schedule. At most 20 links per post and 100 requests per batch; results are cached for 7 days; requests to the same host are spaced out; checks stop automatically after repeated connection failures. robots.txt is not consulted.
* How to turn it on: Settings → Signals → "Check outbound links". The setting is off when the plugin is installed, so no third-party site is contacted until you enable it. Developers can adjust request arguments with the `stalemender_http_args` filter.

= Weekly digest email (optional, off by default) =

When enabled, every Monday at 08:00 site time the plugin sends one email through your site's normal `wp_mail()`, with no third-party mail service, to the addresses you enter or to the site admin email. It contains the average score, band counts and the top-10 refresh list with post titles and edit links.

= What is stored locally =

Scores and signals per post, an internal link map, scan summaries, notes with the WordPress user ID of the editor who wrote them or generated an AI brief, an optional assignee user ID per post, and per-user saved views. The plugin adds no front-end scripts, cookies or visitor tracking, and registers a suggested paragraph under Settings → Privacy → Policy Guide.

== Privacy ==

Stalemender stores, in its own database tables, a freshness score and the signals behind it for every published post and page: publish and modified dates, word count, internal links, outbound links it checked, the headings it flagged, and, only when you connect Google Search Console, the clicks, impressions and position Google reports for that URL. It does not collect anything about your visitors and sets no cookies.

Data leaves your site only for the optional services listed under External services, each off by default: Google Search Console sync, the AI refresh brief (through the provider connected under Settings → Connectors, or with your own key), and the outbound link check that requests the URLs found in your content. The digest email goes to the addresses you enter and contains post titles and scores.

Uninstalling removes the tables, options, scheduled events and encrypted keys unless you enable "Keep data on uninstall". The plugin suggests a paragraph for your privacy policy under Settings → Privacy → Policy Guide.

== Installation ==

1. Upload the plugin to `/wp-content/plugins/stalemender` or install it from the Plugins screen.
2. Activate it.
3. Open **Stalemender** and click **Run first audit**.
4. Optional: connect Search Console under **Settings → Search Console** to rank by lost traffic.

== Frequently Asked Questions ==

= Does it need Google Search Console? =

No. Without it the traffic signal is skipped and the refresh list ranks by staleness × internal links. With it you also get click trends and "decaying" flags.

= How do I connect Search Console? =

Create a service account in Google Cloud with the Search Console API enabled, download its JSON key, paste it under Settings → Search Console, then add the service-account email as a user on your property in Search Console. Pick the property and enable the nightly sync.

= Will the audit slow down my site? =

No. Posts are scored in background batches (default 50, halved automatically if a batch runs out of time) and outbound links are checked with cached HEAD requests, at most 20 per post and 100 per batch.

= Does it work with page builders? =

Elementor, Bricks, Beaver Builder and Oxygen data is read from post meta, Divi shortcodes from post content, so word counts and links are not zero on builder pages.

= Is anything sent to an AI provider? =

None of your content is sent until you click "Generate brief" on a post and confirm the token estimate, and only if you have enabled AI assist and either connected a provider under Settings → Connectors (WordPress 7.0 and later) or added your own key. With WordPress AI, clicking "AI refresh brief" can make WordPress ask the connected provider which models it offers; that request carries the API key but none of your content. Usage is logged locally.

= Does the AI refresh brief use the WordPress AI Client? =

Yes, on WordPress 7.0 and later. Choose "WordPress AI (Settings → Connectors)" under Stalemender → Settings → AI assist (it is the default for new installs there), and requests go through WordPress's built-in AI Client to the provider you connected, with the key WordPress keeps. On older WordPress versions, or if you prefer, you can enter your own API key for OpenAI, Anthropic, Google Gemini or an OpenAI-compatible endpoint instead.

= Does the plugin send anything to its author? =

No. Stalemender has no server of its own, sends no usage statistics or telemetry, and contacts no address that is not listed under "External services" above. Every one of those is off until you turn it on.

= Does the bundled Action Scheduler conflict with other plugins? =

No. Stalemender loads Action Scheduler 3.9.3 from `lib/action-scheduler/`. Action Scheduler is built for this: each copy registers its version with `ActionScheduler_Versions` behind `function_exists()` and `class_exists()` guards, and only the newest copy on the site boots. WooCommerce and other plugins that bundle it share the same mechanism.

= Where is the JavaScript source? =

The admin app ships un-minified in `assets/src/`; the files in `assets/build/` are produced from them with `npm install && npm run build` (the `package.json` is included). The bundled Action Scheduler library in `lib/` is unmodified upstream code under its own GPL licence. Nothing else is compiled or obfuscated.

= Who can see the audit? =

Users who can edit others' posts (filterable with `stalemender_capability`). Settings need `manage_options`.

== Screenshots ==

1. Audit table with freshness scores, reasons, traffic trend, workflow status and filters.
2. Post drawer: score breakdown per signal, outdated mentions in context, inbound links and notes.
3. Dashboard: band distribution, average score trend and the "refresh next" list.
4. Settings: signal weights and thresholds.
5. Settings: Search Console connection.
6. AI refresh brief saved as a note, with the token estimate shown before sending.

== Changelog ==

= 1.1.0 =
* New: the AI refresh brief can use the WordPress AI Client and the provider connected under Settings → Connectors (WordPress 7.0 and later). Your own API key still works.

= 1.0.0 =
* Initial release.
