PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / wp-mcp-server / tools / class-mcp-tool-seo-analysis.php

class-mcp-tool-seo-analysis.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at wp-mcp-server/tools/class-mcp-tool-seo-analysis.php

271 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Tool: SEO Analysis
4 *
5 * Provides tools for analyzing SEO aspects of WordPress posts.
6 *
7 * @package MetaSync
8 * @subpackage MCP_Server/Tools
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 /**
16 * Analyze SEO Tool
17 */
18 class MCP_Tool_Analyze_SEO extends MCP_Tool_Base {
19
20 public function get_name() {
21 return 'wordpress_analyze_seo';
22 }
23
24 public function get_description() {
25 return 'Analyze SEO aspects of a post (title length, description, keyword usage, content analysis)';
26 }
27
28 public function get_input_schema() {
29 return [
30 'type' => 'object',
31 'properties' => [
32 'post_id' => [
33 'type' => 'integer',
34 'description' => 'WordPress post or page ID',
35 'minimum' => 1
36 ]
37 ],
38 'required' => ['post_id']
39 ];
40 }
41
42 public function execute($params) {
43 $this->validate_params($params);
44 $this->require_capability('read');
45
46 $post_id = $this->sanitize_integer($params['post_id']);
47
48 // Verify post exists
49 $post = $this->verify_post_exists($post_id);
50
51 // Custom/LPS pages ship their own self-contained SEO inside their HTML
52 // bundle (not WP post meta), so content/keyword metrics are meaningless.
53 if (metasync_is_custom_or_lps_page($post_id)) {
54 return $this->success([
55 'notice' => 'self-managed SEO — analysis not applicable',
56 'post_info' => [
57 'id' => $post_id,
58 'title' => $post->post_title,
59 'type' => $post->post_type,
60 'status' => $post->post_status,
61 'url' => get_permalink($post_id),
62 ],
63 ]);
64 }
65
66 // Get SEO meta
67 $meta_title = get_post_meta($post_id, '_metasync_metatitle', true);
68 $meta_desc = get_post_meta($post_id, '_metasync_metadesc', true);
69 $focus_keyword = get_post_meta($post_id, '_metasync_focus_keyword', true);
70 $robots_index = get_post_meta($post_id, '_metasync_robots_index', true);
71
72 // Use post title if no meta title
73 $effective_title = !empty($meta_title) ? $meta_title : $post->post_title;
74 $title_length = mb_strlen($effective_title);
75 $desc_length = mb_strlen($meta_desc);
76
77 // Analyze content
78 $content = $post->post_content;
79 $content_text = wp_strip_all_tags($content);
80 $word_count = str_word_count($content_text);
81
82 // Check keyword usage
83 $keyword_in_title = false;
84 $keyword_in_content = false;
85 $keyword_in_description = false;
86
87 if (!empty($focus_keyword)) {
88 $keyword_lower = mb_strtolower($focus_keyword);
89 $keyword_in_title = mb_stripos($effective_title, $focus_keyword) !== false;
90 $keyword_in_content = mb_stripos($content_text, $focus_keyword) !== false;
91 $keyword_in_description = mb_stripos($meta_desc, $focus_keyword) !== false;
92 }
93
94 // Build analysis
95 $analysis = [
96 'post_info' => [
97 'id' => $post_id,
98 'title' => $post->post_title,
99 'type' => $post->post_type,
100 'status' => $post->post_status,
101 'url' => get_permalink($post_id)
102 ],
103 'meta_title' => [
104 'value' => $effective_title,
105 'length' => $title_length,
106 'status' => $this->get_title_status($title_length),
107 'recommendation' => $this->get_title_recommendation($title_length)
108 ],
109 'meta_description' => [
110 'value' => $meta_desc,
111 'length' => $desc_length,
112 'status' => $this->get_description_status($desc_length),
113 'recommendation' => $this->get_description_recommendation($desc_length)
114 ],
115 'focus_keyword' => [
116 'value' => $focus_keyword,
117 'in_title' => $keyword_in_title,
118 'in_description' => $keyword_in_description,
119 'in_content' => $keyword_in_content,
120 'status' => $this->get_keyword_status($focus_keyword, $keyword_in_title, $keyword_in_content)
121 ],
122 'content' => [
123 'word_count' => $word_count,
124 'status' => $this->get_content_status($word_count)
125 ],
126 'indexability' => [
127 'is_indexable' => $robots_index !== 'noindex',
128 'robots_setting' => $robots_index ?: 'index'
129 ],
130 'overall_score' => $this->calculate_seo_score(
131 $title_length,
132 $desc_length,
133 $focus_keyword,
134 $keyword_in_title,
135 $keyword_in_content,
136 $word_count
137 )
138 ];
139
140 return $this->success($analysis);
141 }
142
143 private function get_title_status($length) {
144 if ($length === 0) return 'missing';
145 if ($length < 30) return 'too_short';
146 if ($length > 60) return 'too_long';
147 return 'good';
148 }
149
150 private function get_title_recommendation($length) {
151 if ($length === 0) return 'Add a meta title (30-60 characters recommended)';
152 if ($length < 30) return 'Title is too short. Aim for 30-60 characters.';
153 if ($length > 60) return 'Title is too long. Keep it under 60 characters to avoid truncation in search results.';
154 return 'Title length is optimal';
155 }
156
157 private function get_description_status($length) {
158 if ($length === 0) return 'missing';
159 if ($length < 120) return 'too_short';
160 if ($length > 160) return 'too_long';
161 return 'good';
162 }
163
164 private function get_description_recommendation($length) {
165 if ($length === 0) return 'Add a meta description (120-160 characters recommended)';
166 if ($length < 120) return 'Description is too short. Aim for 120-160 characters.';
167 if ($length > 160) return 'Description is too long. Keep it under 160 characters to avoid truncation.';
168 return 'Description length is optimal';
169 }
170
171 private function get_keyword_status($keyword, $in_title, $in_content) {
172 if (empty($keyword)) return 'not_set';
173 if (!$in_title || !$in_content) return 'not_optimized';
174 return 'good';
175 }
176
177 private function get_content_status($word_count) {
178 if ($word_count === 0) return 'empty';
179 if ($word_count < 300) return 'too_short';
180 if ($word_count > 2000) return 'excellent';
181 return 'good';
182 }
183
184 private function calculate_seo_score($title_len, $desc_len, $keyword, $kw_in_title, $kw_in_content, $word_count) {
185 $score = 0;
186
187 // Title (25 points)
188 if ($title_len >= 30 && $title_len <= 60) $score += 25;
189 elseif ($title_len > 0) $score += 10;
190
191 // Description (25 points)
192 if ($desc_len >= 120 && $desc_len <= 160) $score += 25;
193 elseif ($desc_len > 0) $score += 10;
194
195 // Keyword usage (30 points)
196 if (!empty($keyword)) {
197 $score += 10; // Has keyword
198 if ($kw_in_title) $score += 10;
199 if ($kw_in_content) $score += 10;
200 }
201
202 // Content (20 points)
203 if ($word_count >= 300) $score += 20;
204 elseif ($word_count > 0) $score += 10;
205
206 return [
207 'score' => $score,
208 'max_score' => 100,
209 'percentage' => $score,
210 'rating' => $this->get_rating($score)
211 ];
212 }
213
214 private function get_rating($score) {
215 if ($score >= 80) return 'excellent';
216 if ($score >= 60) return 'good';
217 if ($score >= 40) return 'fair';
218 return 'needs_improvement';
219 }
220 }
221
222 /**
223 * Check Indexability Tool
224 */
225 class MCP_Tool_Check_Indexability extends MCP_Tool_Base {
226
227 public function get_name() {
228 return 'wordpress_check_indexability';
229 }
230
231 public function get_description() {
232 return 'Check if a post is set to be indexed by search engines';
233 }
234
235 public function get_input_schema() {
236 return [
237 'type' => 'object',
238 'properties' => [
239 'post_id' => [
240 'type' => 'integer',
241 'description' => 'WordPress post or page ID',
242 'minimum' => 1
243 ]
244 ],
245 'required' => ['post_id']
246 ];
247 }
248
249 public function execute($params) {
250 $this->validate_params($params);
251 $this->require_capability('read');
252
253 $post_id = $this->sanitize_integer($params['post_id']);
254
255 // Verify post exists
256 $post = $this->verify_post_exists($post_id);
257
258 $robots_index = get_post_meta($post_id, '_metasync_robots_index', true);
259 $is_indexable = ($robots_index !== 'noindex');
260
261 return $this->success([
262 'post_id' => $post_id,
263 'post_title' => $post->post_title,
264 'is_indexable' => $is_indexable,
265 'robots_setting' => $robots_index ?: 'index',
266 'status' => $post->post_status,
267 'url' => get_permalink($post_id)
268 ]);
269 }
270 }
271