PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.0
2.10.0 2.9.0 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 All 51 releases
← All changes | includes/core/class-plan-config.php +145 -175 1.30.0 → 2.10.0 View file →
@@ -8,11 +8,10 @@
8 8 * reads from here. No feature code should check `defined('THINKRANK_PRO_VERSION')`
9 9 * or `apply_filters('thinkrank_is_pro_active', ...)` directly — call these methods
10 10 * instead so a single override flips behavior everywhere.
11 11 *
12 - * The Pro plugin attaches by filtering `thinkrank_email_report_capabilities`
13 - * (or the analogous filter for other features). It never needs to fork or
14 - * monkey-patch this file.
12 + * The Pro plugin attaches by filtering each feature's capability map. It never
13 + * needs to fork or monkey-patch this file.
15 14 *
16 15 * @package ThinkRank\Core
17 16 * @since 1.9.0
18 17 */
@@ -28,10 +27,10 @@
28 27 /**
29 28 * Plan_Config — capability registry for free/pro feature gating.
30 29 *
31 30 * Usage:
32 - * if (Plan_Config::can('custom_subject', 'email_report')) { ... }
33 - * $caps = Plan_Config::email_report();
31 + * if (Plan_Config::can('usage_policy', 'llms_txt')) { ... }
32 + * $caps = Plan_Config::llms_txt();
34 33 *
35 34 * @since 1.9.0
36 35 */
37 36 final class Plan_Config {
@@ -50,182 +49,190 @@
50 49 );
51 50 }
52 51
53 52 /**
54 - * Capability map for the Email Reporting feature.
53 + * Capability map for the llms.txt feature.
55 54 *
56 - * Returns a flat array of capability_key => bool|int|array describing
57 - * what the current plan can do. The Pro plugin filters this to enable
58 - * its capabilities.
55 + * Generating and editing llms.txt is FREE and stays free. What Pro adds is
56 + * the usage policy: the `Training:` / `Summarization:` / `Embedding:` /
57 + * `Require-Attribution:` directives that state what agents may do with the
58 + * content, rather than describing the content itself (#160 in Pro).
59 59 *
60 - * Schema (keep in sync with src/admin/config/email-report-plan.js):
61 - * - allowed_frequencies int[] Days between sends user may pick.
62 - * - max_recipients int Max addresses on the recipients field.
63 - * - recipients_locked_to string|null 'admin_email' = pre-filled & read-only.
64 - * - custom_subject bool May edit subject template.
65 - * - custom_logo bool May upload a header logo.
66 - * - logo_link bool May set a click-through URL on the logo.
67 - * - header_background bool May set a custom header background CSS.
68 - * - link_to_full_report bool May toggle the dashboard CTA at the foot.
69 - * - intro_text bool May set a custom intro paragraph.
70 - * - sections_configurable bool May enable/disable individual sections.
71 - * - footer_text bool May set a custom footer paragraph.
72 - * - additional_css bool May inject additional CSS into the email.
73 - * - ai_highlights bool May render the AI Highlights section.
60 + * Schema:
61 + * usage_policy bool May publish usage-policy directives in llms.txt.
62 + * full_document bool May publish llms-full.txt, the full-content
63 + * companion document (thinkrank-pro#171).
64 + *
65 + * @since 2.5.0
66 + *
67 + * @return array Capability map.
74 68 */
75 - public static function email_report(): array {
69 + public static function llms_txt(): array {
76 70 $defaults = [
77 - 'allowed_frequencies' => [30],
78 - 'max_recipients' => 1,
79 - 'recipients_locked_to' => 'admin_email',
80 - 'custom_subject' => false,
81 - 'custom_logo' => false,
82 - 'logo_link' => false,
83 - 'header_background' => false,
84 - 'link_to_full_report' => false,
85 - 'intro_text' => false,
86 - 'sections_configurable' => false,
87 - 'footer_text' => false,
88 - 'additional_css' => false,
89 - 'ai_highlights' => false,
71 + 'usage_policy' => false,
72 + 'full_document' => false,
90 73 ];
91 74
92 - if (self::is_pro()) {
93 - $defaults = array_merge($defaults, [
94 - 'allowed_frequencies' => [7, 15, 30],
95 - 'max_recipients' => 50,
96 - 'recipients_locked_to' => null,
97 - 'custom_subject' => true,
98 - 'custom_logo' => true,
99 - 'logo_link' => true,
100 - 'header_background' => true,
101 - 'link_to_full_report' => true,
102 - 'intro_text' => true,
103 - 'sections_configurable' => true,
104 - 'footer_text' => true,
105 - 'additional_css' => true,
106 - // ai_highlights stays false by default — it's a separate sub-feature
107 - // the Pro plugin opts in to once the AI summary integration ships.
108 - ]);
109 - }
75 + /**
76 + * Filter the llms.txt capability map.
77 + *
78 + * ThinkRank Pro sets `usage_policy` to true; nothing in the free
79 + * plugin ever does, so the directives simply never render without it.
80 + *
81 + * @since 2.5.0
82 + *
83 + * @param array $defaults Capability map (see schema above).
84 + */
85 + $caps = apply_filters('thinkrank_llms_txt_capabilities', $defaults);
110 86
87 + return array_merge($defaults, is_array($caps) ? $caps : []);
88 + }
89 +
90 + /**
91 + * Capability map for the Site SEO Analyzer.
92 + *
93 + * Running the audit and seeing the score is FREE and stays free. What Pro
94 + * adds is memory: dated snapshots of each run, a score trend, and a
95 + * side-by-side comparison of two runs (#161 in Pro). Free keeps exactly
96 + * what it has today — the most recent run, cached for an hour.
97 + *
98 + * Schema:
99 + * history bool May persist and read audit snapshots.
100 + * history_runs int Snapshots retained per site (0 = unlimited).
101 + *
102 + * @since 2.5.0
103 + *
104 + * @return array Capability map.
105 + */
106 + public static function seo_analyzer(): array {
107 + $defaults = [
108 + 'history' => false,
109 + 'history_runs' => 0,
110 + ];
111 +
111 112 /**
112 - * Filter the Email Reporting capability map.
113 + * Filter the SEO Analyzer capability map.
113 114 *
114 - * The Pro plugin uses this filter (and only this filter) to enable
115 - * pro capabilities. Returning a partial array is fine; missing keys
116 - * fall back to the values above.
115 + * ThinkRank Pro sets `history` to true and declares how many runs it
116 + * retains. Nothing in the free plugin ever does, so no snapshot is
117 + * ever written without Pro.
117 118 *
118 - * @since 1.9.0
119 + * @since 2.5.0
119 120 *
120 121 * @param array $defaults Capability map (see schema above).
121 122 */
122 - $caps = apply_filters('thinkrank_email_report_capabilities', $defaults);
123 + $caps = apply_filters('thinkrank_seo_analyzer_capabilities', $defaults);
123 124
124 - // Defensive merge: never let a filter drop required keys.
125 125 return array_merge($defaults, is_array($caps) ? $caps : []);
126 126 }
127 127
128 128 /**
129 - * Capability map for the Focus Keywords feature.
129 + * Capability map for Focus Pages.
130 130 *
131 - * Free allows up to 5 focus keywords; ThinkRank Pro lifts the cap. The 6th
132 - * and subsequent keywords are stored but only become usable (analyzed,
133 - * output, editable) once Pro raises the limit.
131 + * Focus Pages is Pro: it is bulk (many pages in one run), historical, and
132 + * depends on Pro-only rank data (thinkrank-pro#164). Free has no entry
133 + * point in v1 — the per-post SEO score panel already answers the single-page
134 + * on-page question.
134 135 *
135 - * Localized to the metabox as `thinkrankMetabox.focusKeywords`.
136 136 * Schema:
137 - * - max_keywords int Usable focus keywords. 0 = unlimited (Pro).
137 + * enabled bool May curate and diagnose focus pages.
138 + * max_pages int Pages that may be curated (0 = none).
138 139 *
139 - * @since 2.0.0
140 + * @since 2.5.0
140 141 *
141 142 * @return array Capability map.
142 143 */
143 - public static function focus_keywords(): array {
144 - // Free default. ThinkRank Pro lifts the cap by filtering the map below
145 - // (Pro owns its own limit rather than the free plugin hard-coding it).
144 + public static function focus_pages(): array {
146 145 $defaults = [
147 - 'max_keywords' => 5,
146 + 'enabled' => false,
147 + 'max_pages' => 0,
148 148 ];
149 149
150 150 /**
151 - * Filter the Focus Keywords capability map.
151 + * Filter the Focus Pages capability map.
152 152 *
153 - * The Pro plugin uses this filter to lift the free cap — set
154 - * `max_keywords` to 0 for unlimited, or a finite number.
153 + * ThinkRank Pro enables the feature and declares its own page cap,
154 + * which bounds the external API cost of a refresh.
155 155 *
156 - * @since 2.0.0
156 + * @since 2.5.0
157 157 *
158 158 * @param array $defaults Capability map (see schema above).
159 159 */
160 - $caps = apply_filters('thinkrank_focus_keywords_capabilities', $defaults);
160 + $caps = apply_filters('thinkrank_focus_pages_capabilities', $defaults);
161 161
162 162 return array_merge($defaults, is_array($caps) ? $caps : []);
163 163 }
164 164
165 165 /**
166 - * Capability map for the AI Insights trio.
166 + * Capability map for sitewide duplicate snippet detection.
167 167 *
168 - * Deliberately NOT gated on "needs an AI key" — the user pays their
169 - * provider either way, so that is not what separates free from Pro. The
170 - * split is acquisition vs. recurring depth:
168 + * The check itself is Free: finding out that two of your pages carry one
169 + * title is a read, it runs when the user asks for it, and it is exactly the
170 + * kind of thing that should work on a first install (#564). What Pro adds
171 + * is the unattended half — a recheck that runs on a schedule and tells you
172 + * when a new duplicate appears, rather than when you next open the screen.
171 173 *
172 - * - AI Traffic analytics stays FREE and ungated. It costs nothing to run
173 - * (referrer classification, no AI call) and is the feature that shows
174 - * value on day one. No capability key exists for it on purpose.
175 - * - Brand Visibility is FREEMIUM: free runs a couple of queries by hand
176 - * and keeps a short history; Pro lifts the query cap, keeps full
177 - * history, and unlocks scheduled (unattended) checks.
178 - * - Auto AI metadata is PRO: unattended automation is the clearest Pro
179 - * trait in the lineup.
174 + * Nothing in the free plugin schedules anything, so `scheduled` only ever
175 + * reports what Pro has arranged; the report reads it to say whether it is
176 + * being watched or only answered on demand.
180 177 *
181 - * Unlike email_report, this map has no JS mirror in src/admin/config/ on
182 - * purpose: the admin UI reads the resolved values off the AI Insights REST
183 - * responses (`plan`, `is_pro`, `available`) rather than re-declaring them
184 - * client-side, so there is nothing here that can drift out of sync.
178 + * Schema:
179 + * scheduled bool Duplicates are rechecked without the user asking.
185 180 *
181 + * @since 2.10.0
182 + *
183 + * @return array Capability map.
184 + */
185 + public static function duplicate_snippets(): array {
186 + $defaults = [
187 + 'scheduled' => false,
188 + ];
189 +
190 + /**
191 + * Filter the duplicate snippets capability map.
192 + *
193 + * @since 2.10.0
194 + *
195 + * @param array $defaults Capability map (see schema above).
196 + */
197 + $caps = apply_filters('thinkrank_duplicate_snippets_capabilities', $defaults);
198 +
199 + return array_merge($defaults, is_array($caps) ? $caps : []);
200 + }
201 +
202 + /**
203 + * Capability map for the thin content report.
204 + *
205 + * Finding out which of your pages are thin is a read, it runs when the user
206 + * asks for it, and it is exactly what should work on a first install, so
207 + * the report is Free (#565). What Pro adds is the unattended half: an alert
208 + * when content that was fine becomes thin, or when something thin is
209 + * published, which is the decay framing Content Refresh Radar already uses.
210 + *
211 + * Nothing in the free plugin schedules anything, so `scheduled_alerts` only
212 + * ever reports what Pro has arranged; the report reads it to say whether it
213 + * is being watched or only answered on demand.
214 + *
186 215 * Schema:
187 - * brand_max_queries int Saved brand queries allowed (0 = unlimited).
188 - * brand_history_limit int History rows returned (0 = unlimited).
189 - * brand_scheduled bool Unattended scheduled brand checks.
190 - * auto_ai_meta bool Auto-generate metadata on first publish.
216 + * scheduled_alerts bool Newly-thin content is reported without being asked.
191 217 *
192 - * @since 1.28.0
218 + * @since 2.10.0
193 219 *
194 220 * @return array Capability map.
195 221 */
196 - public static function ai_visibility(): array {
222 + public static function thin_content(): array {
197 223 $defaults = [
198 - 'brand_max_queries' => 2,
199 - 'brand_history_limit' => 10,
200 - 'brand_scheduled' => false,
201 - 'auto_ai_meta' => false,
202 -
203 - // Brand Visibility v2. Free keeps a usable "quick check" — a
204 - // couple of questions on one platform, single sample — which is
205 - // enough to see the feature work and understand what Pro measures.
206 - // Everything that turns a probe into a MEASUREMENT (sampling,
207 - // competitors, multi-platform, trends) is Pro.
208 - 'brand_wizard' => false,
209 - 'brand_competitors' => 0, // max competitors; 0 = none
210 - 'brand_max_platforms' => 1,
211 - 'brand_max_samples' => 1,
212 - 'brand_sentiment' => false,
213 - 'brand_history_runs' => 1, // runs kept for the trend chart
224 + 'scheduled_alerts' => false,
214 225 ];
215 226
216 227 /**
217 - * Filter the AI Insights capability map.
228 + * Filter the thin content capability map.
218 229 *
219 - * ThinkRank Pro sets `brand_max_queries` to 0 (unlimited, bounded
220 - * only by what the run request itself asks for), enables
221 - * `brand_scheduled` and `auto_ai_meta`, and lifts the history limit.
230 + * @since 2.10.0
222 231 *
223 - * @since 1.28.0
224 - *
225 232 * @param array $defaults Capability map (see schema above).
226 233 */
227 - $caps = apply_filters('thinkrank_ai_visibility_capabilities', $defaults);
234 + $caps = apply_filters('thinkrank_thin_content_capabilities', $defaults);
228 235
229 236 return array_merge($defaults, is_array($caps) ? $caps : []);
230 237 }
231 238
@@ -231,16 +238,15 @@
231 238
232 239 /**
233 240 * Check a single capability for a given feature.
234 241 *
235 - * Currently only the `email_report` feature is registered. Adding more
236 - * features means adding a switch case here that delegates to its own
237 - * capability builder method.
242 + * Adding a feature means adding a switch case to capabilities_for() that
243 + * delegates to its own capability builder method.
238 244 *
239 - * @param string $capability Capability key (e.g. 'custom_subject').
240 - * @param string $feature Feature scope (default 'email_report').
245 + * @param string $capability Capability key (e.g. 'usage_policy').
246 + * @param string $feature Feature scope (e.g. 'llms_txt').
241 247 */
242 - public static function can(string $capability, string $feature = 'email_report'): bool {
248 + public static function can(string $capability, string $feature): bool {
243 249 $caps = self::capabilities_for($feature);
244 250 return ! empty($caps[$capability]);
245 251 }
246 252
@@ -251,55 +257,19 @@
251 257 * @return array
252 258 */
253 259 public static function capabilities_for(string $feature): array {
254 260 switch ($feature) {
255 - case 'email_report':
256 - return self::email_report();
257 - case 'focus_keywords':
258 - return self::focus_keywords();
259 - case 'ai_visibility':
260 - return self::ai_visibility();
261 + case 'llms_txt':
262 + return self::llms_txt();
263 + case 'seo_analyzer':
264 + return self::seo_analyzer();
265 + case 'focus_pages':
266 + return self::focus_pages();
267 + case 'duplicate_snippets':
268 + return self::duplicate_snippets();
269 + case 'thin_content':
270 + return self::thin_content();
261 271 default:
262 272 return [];
263 273 }
264 - }
265 -
266 - /**
267 - * Clamp a frequency value to one the current plan allows.
268 - *
269 - * Free plans always end up at 30. Pro plans accept 7, 15, or 30.
270 - * Anything else falls back to the highest allowed value (most permissive
271 - * default that still respects the cap).
272 - *
273 - * @param int $requested Requested frequency in days.
274 - * @return int Clamped frequency.
275 - */
276 - public static function clamp_email_report_frequency(int $requested): int {
277 - $allowed = self::email_report()['allowed_frequencies'];
278 - if (in_array($requested, $allowed, true)) {
279 - return $requested;
280 - }
281 - return (int) max($allowed);
282 - }
283 -
284 - /**
285 - * Truncate a list of recipients to the plan-allowed maximum.
286 - *
287 - * Used at save and at render time. The save-time call gives the user
288 - * feedback; the render-time call is a defense in depth so a downgrade
289 - * never accidentally fans a report out to a list the user no longer
290 - * has the plan for.
291 - *
292 - * @param string[] $recipients Recipient email addresses.
293 - * @return string[] Truncated, de-duplicated recipients.
294 - */
295 - public static function clamp_email_report_recipients(array $recipients): array {
296 - $caps = self::email_report();
297 - $unique = array_values(array_unique(array_filter(array_map('trim', $recipients))));
298 -
299 - if ('admin_email' === $caps['recipients_locked_to']) {
300 - return [(string) get_option('admin_email')];
301 - }
302 -
303 - return array_slice($unique, 0, (int) $caps['max_recipients']);
304 274 }
305 275 }