PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.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 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / seo / class-focus-keywords.php

class-focus-keywords.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.7.0, at includes/seo/class-focus-keywords.php

204 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Focus Keywords helper.
4 *
5 * Central read/write/normalize logic for the multi-focus-keyword feature.
6 * Keywords are stored as an array in `_thinkrank_focus_keywords`, up to
7 * Focus_Keywords::MAX. The legacy single-value meta `_thinkrank_focus_keyword`
8 * is kept in sync (= the primary/first keyword) for backward compatibility with
9 * older consumers that still read a string.
10 *
11 * This plugin stores MAX keywords per post and nothing beyond. An extension
12 * that stores more (ThinkRank Pro) receives the full submitted list through
13 * `thinkrank_focus_keywords_saved` and returns it through
14 * `thinkrank_focus_keywords` (#673).
15 *
16 * @package ThinkRank\SEO
17 * @since 1.0.0
18 */
19
20 declare(strict_types=1);
21
22 namespace ThinkRank\SEO;
23
24 if (!defined('ABSPATH')) {
25 exit;
26 }
27
28 /**
29 * Focus Keywords storage + normalization helper.
30 *
31 * @since 1.0.0
32 */
33 class Focus_Keywords {
34
35 /**
36 * Array post meta key holding the keyword list.
37 */
38 public const META_KEY = '_thinkrank_focus_keywords';
39
40 /**
41 * Legacy single-string meta key (kept = primary keyword for back-compat).
42 */
43 public const LEGACY_META_KEY = '_thinkrank_focus_keyword';
44
45 /**
46 * Focus keywords stored per post.
47 */
48 public const MAX = 5;
49
50 /**
51 * Normalize arbitrary input into a clean keyword array.
52 *
53 * Accepts an array of strings or a comma-separated string. Trims and
54 * sanitizes each value, drops empties, removes case-insensitive duplicates
55 * (keeping first occurrence / original order), and caps the result at
56 * `$limit`.
57 *
58 * @param mixed $input Array of keywords or comma-separated string.
59 * @param int|null $limit Maximum keywords to return. Null (default) is MAX.
60 * Pass 0 (or negative) to return the full deduped
61 * list uncapped.
62 * @return string[] Normalized keyword list.
63 */
64 public static function normalize($input, ?int $limit = null): array {
65 if ($limit === null) {
66 $limit = self::MAX;
67 }
68
69 if (is_string($input)) {
70 $input = explode(',', $input);
71 }
72
73 if (!is_array($input)) {
74 return [];
75 }
76
77 $seen = [];
78 $keywords = [];
79
80 foreach ($input as $keyword) {
81 if (is_array($keyword)) {
82 continue;
83 }
84
85 $keyword = sanitize_text_field(trim((string) $keyword));
86 if ($keyword === '') {
87 continue;
88 }
89
90 $dedupe_key = function_exists('mb_strtolower')
91 ? mb_strtolower($keyword)
92 : strtolower($keyword);
93
94 if (isset($seen[$dedupe_key])) {
95 continue;
96 }
97
98 $seen[$dedupe_key] = true;
99 $keywords[] = $keyword;
100
101 if ($limit > 0 && count($keywords) >= $limit) {
102 break;
103 }
104 }
105
106 return $keywords;
107 }
108
109 /**
110 * Get the focus keywords for a post.
111 *
112 * Falls back to the legacy single value for back-compat.
113 *
114 * @param int $post_id Post ID.
115 * @return string[] Keyword list.
116 */
117 public static function get(int $post_id): array {
118 $keywords = self::normalize(self::read_stored($post_id));
119
120 /**
121 * Filter a post's focus keywords.
122 *
123 * This plugin stores up to Focus_Keywords::MAX. An extension that
124 * stores more returns the full list here.
125 *
126 * @since 2.6.0
127 *
128 * @param string[] $keywords Stored keywords, in order.
129 * @param int $post_id Post ID.
130 */
131 $filtered = apply_filters('thinkrank_focus_keywords', $keywords, $post_id);
132
133 return is_array($filtered) ? self::normalize($filtered, 0) : $keywords;
134 }
135
136 /**
137 * Read the stored keyword array (array meta, legacy fallback). Uncapped.
138 *
139 * @param int $post_id Post ID.
140 * @return string[] Stored keywords (deduped, uncapped).
141 */
142 private static function read_stored(int $post_id): array {
143 $stored = get_post_meta($post_id, self::META_KEY, true);
144 if (is_array($stored) && !empty($stored)) {
145 return self::normalize($stored, 0);
146 }
147
148 // Backward compatibility: convert the old single value into an array.
149 $legacy = get_post_meta($post_id, self::LEGACY_META_KEY, true);
150 if (is_string($legacy) && $legacy !== '') {
151 return self::normalize($legacy, 0);
152 }
153
154 return [];
155 }
156
157 /**
158 * Get the primary (first) focus keyword for a post.
159 *
160 * @param int $post_id Post ID.
161 * @return string Primary keyword, or '' when none set.
162 */
163 public static function get_primary(int $post_id): string {
164 $keywords = self::get($post_id);
165 return $keywords[0] ?? '';
166 }
167
168 /**
169 * Save focus keywords (metabox / inline edit / AI / import).
170 *
171 * Stores the first MAX keywords and hands the full submitted list to
172 * `thinkrank_focus_keywords_saved`.
173 *
174 * @param int $post_id Post ID.
175 * @param mixed $input Array of keywords or comma-separated string.
176 * @return string[] The post's keywords after the save, as get() reads them.
177 */
178 public static function save(int $post_id, $input): array {
179 $all = self::normalize($input, 0);
180 $kept = array_slice($all, 0, self::MAX);
181
182 if (empty($kept)) {
183 delete_post_meta($post_id, self::META_KEY);
184 delete_post_meta($post_id, self::LEGACY_META_KEY);
185 } else {
186 update_post_meta($post_id, self::META_KEY, $kept);
187 update_post_meta($post_id, self::LEGACY_META_KEY, $kept[0]);
188 }
189
190 /**
191 * Fires after a post's focus keywords are saved.
192 *
193 * @since 2.6.0
194 *
195 * @param int $post_id Post ID.
196 * @param string[] $keywords The full submitted list, including any
197 * beyond Focus_Keywords::MAX.
198 */
199 do_action('thinkrank_focus_keywords_saved', $post_id, $all);
200
201 return self::get($post_id);
202 }
203 }
204