PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.2
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
thinkrank / includes / ai / prompts / class-seo-prompts.php

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

391 lines 15.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SEO Prompts Class
4 *
5 * Handles SEO-related AI prompts including metadata generation and content analysis.
6 * Preserves exact functionality from original OpenAI and Claude client implementations.
7 *
8 * @package ThinkRank
9 * @subpackage AI\Prompts
10 * @since 1.0.0
11 */
12
13 declare(strict_types=1);
14
15 namespace ThinkRank\AI\Prompts;
16
17 // Prevent direct access
18 if (!defined('ABSPATH')) {
19 exit;
20 }
21
22 /**
23 * SEO Prompts Class
24 *
25 * Centralized SEO prompt building for consistent AI optimization across all providers.
26 * Handles SEO metadata generation and content analysis prompts.
27 *
28 * @since 1.0.0
29 */
30 class SEO_Prompts {
31
32 /**
33 * Build the language directive block for a prompt.
34 *
35 * Injected near the top of every generation prompt so the model writes in
36 * the site/post language instead of defaulting to English — the fix for
37 * English/mixed output on non-English sites (issue #234). Empty when no
38 * language is resolved, preserving the legacy infer-from-content behavior.
39 *
40 * @since 1.27.0
41 *
42 * @param string $language Human-readable target language (may be empty).
43 * @return string Directive block, or '' when no language is given.
44 */
45 private function language_directive(string $language): string {
46 if ($language === '') {
47 return '';
48 }
49
50 return "\n\nLANGUAGE (critical): Write ALL output — every title, description, keyword and any other text — in {$language}. Match the language of the source content. Do NOT translate to English or switch languages.";
51 }
52
53 /**
54 * Build SEO metadata optimization prompt
55 *
56 * @since 1.0.0
57 *
58 * @param string $content Content to optimize
59 * @param string $target_keyword Target keyword
60 * @param string $content_type Type of content
61 * @param string $tone Desired tone
62 * @param string $provider AI provider
63 * @param string $language Target output language (empty = infer from content)
64 * @return string Formatted prompt
65 */
66 public function build_seo_prompt(string $content, string $target_keyword, string $content_type, string $tone, string $provider = 'openai', string $language = ''): string {
67 $keyword_instruction = $target_keyword ? "Focus on the keyword: \"{$target_keyword}\"" : "Identify the main topic";
68
69 return "You are an expert SEO copywriter. Analyze the following {$content_type} content and generate optimized SEO metadata.{$this->language_directive($language)}
70
71 {$keyword_instruction}
72
73 Content:
74 {$content}
75
76 Generate:
77 1. SEO Title (50-60 characters, compelling and keyword-optimized)
78 2. Meta Description (150-160 characters, engaging with call-to-action)
79 3. Focus Keyword (main keyword for this content)
80
81 Requirements:
82 - Use {$tone} tone
83 - Include target keyword naturally
84 - Make titles compelling for clicks
85 - Write descriptions that encourage clicks
86 - Count characters (including spaces and punctuation) before answering. The title MUST NOT exceed 60 characters and the description MUST NOT exceed 160 characters — these are hard maximums, not targets. Rewrite shorter if you go over.
87
88 Format your response as JSON:
89 {
90 \"title\": \"Your SEO title here\",
91 \"description\": \"Your meta description here\",
92 \"focus_keyword\": \"main keyword\",
93 \"suggestions\": [\"improvement suggestion 1\", \"improvement suggestion 2\"]
94 }";
95 }
96
97 /**
98 * Build a focused SEO title improvement prompt.
99 *
100 * Generates a single improved SEO title that addresses a specific scoring
101 * suggestion while following SEO best practices (length, keyword placement,
102 * emotional/power words). Returns JSON so every provider parses consistently.
103 *
104 * @since 1.14.0
105 *
106 * @param string $content Post content for context.
107 * @param string $current_title The current SEO title (may be empty).
108 * @param string $target_keyword Target/focus keyword (may be empty).
109 * @param string $content_type Type of content (blog_post, page, …).
110 * @param string $tone Desired tone.
111 * @param string $suggestion The suggestion the new title must address.
112 * @param string $provider AI provider.
113 * @param array $sentiment_words Emotion/sentiment words the title check rewards.
114 * @param array $power_words Power words the title check rewards.
115 * @return string Formatted prompt.
116 */
117 public function build_title_improvement_prompt(string $content, string $current_title, string $target_keyword, string $content_type, string $tone, string $suggestion, string $provider = 'openai', array $sentiment_words = [], array $power_words = [], string $language = ''): string {
118 $keyword_instruction = $target_keyword
119 ? "Naturally include the focus keyword: \"{$target_keyword}\"."
120 : 'Identify the main topic from the content and lead with it.';
121 $current_title_line = $current_title !== ''
122 ? "Current SEO title: \"{$current_title}\""
123 : 'There is no SEO title yet.';
124 $suggestion_line = $suggestion !== ''
125 ? "Specifically fix this issue: \"{$suggestion}\"."
126 : 'Improve the overall SEO effectiveness of the title.';
127
128 // Bake in the exact word lists ThinkRank validates against, so the
129 // generated title actually passes the emotion/sentiment and power-word
130 // checks instead of using a synonym we do not recognise.
131 $sentiment_line = '';
132 if (!empty($sentiment_words)) {
133 $list = implode(', ', $sentiment_words);
134 $sentiment_line = "\n- MUST contain at least ONE of these emotion/sentiment words verbatim (case-insensitive, may appear inside a larger word): {$list}.";
135 }
136 $power_line = '';
137 if (!empty($power_words)) {
138 $list = implode(', ', $power_words);
139 $power_line = "\n- Where natural, also include a number or ONE of these power words verbatim: {$list}.";
140 }
141
142 return "You are an expert SEO copywriter. Rewrite the SEO title for the following {$content_type} so it ranks and earns clicks.{$this->language_directive($language)}
143
144 {$current_title_line}
145 {$suggestion_line}
146
147 Content (for context):
148 {$content}
149
150 Requirements for the new SEO title:
151 - MUST be between 35 and 60 characters (60 is the hard maximum — never exceed it).
152 - {$keyword_instruction}
153 - Use a {$tone} tone and make it compelling and click-worthy.{$sentiment_line}{$power_line}
154 - Front-load the most important words; do not use clickbait or ALL CAPS.
155 - Return the title only, with no surrounding quotes.
156
157 Format your response as JSON:
158 {
159 \"title\": \"Your improved SEO title here\"
160 }";
161 }
162
163 /**
164 * Build a focused meta-description improvement prompt.
165 *
166 * Generates a single meta description that addresses a specific scoring
167 * suggestion while passing ThinkRank's checks (120-160 characters, focus
168 * keyword present). Returns JSON for consistent cross-provider parsing.
169 *
170 * @since 1.14.0
171 *
172 * @param string $content Post content for context.
173 * @param string $current_desc The current meta description (may be empty).
174 * @param string $target_keyword Target/focus keyword (may be empty).
175 * @param string $content_type Type of content.
176 * @param string $tone Desired tone.
177 * @param string $suggestion The suggestion the description must address.
178 * @param string $provider AI provider.
179 * @return string Formatted prompt.
180 */
181 public function build_meta_description_improvement_prompt(string $content, string $current_desc, string $target_keyword, string $content_type, string $tone, string $suggestion, string $provider = 'openai', string $language = ''): string {
182 $keyword_instruction = $target_keyword
183 ? "MUST naturally include the focus keyword: \"{$target_keyword}\"."
184 : 'Lead with the main topic from the content.';
185 $current_desc_line = $current_desc !== ''
186 ? "Current meta description: \"{$current_desc}\""
187 : 'There is no meta description yet.';
188 $suggestion_line = $suggestion !== ''
189 ? "Specifically fix this issue: \"{$suggestion}\"."
190 : 'Improve the overall SEO effectiveness of the meta description.';
191
192 return "You are an expert SEO copywriter. Write the meta description for the following {$content_type} so it earns clicks from search results.{$this->language_directive($language)}
193
194 {$current_desc_line}
195 {$suggestion_line}
196
197 Content (for context):
198 {$content}
199
200 Requirements for the new meta description:
201 - MUST be between 120 and 160 characters (this length range is mandatory — count characters carefully and never go under 120 or over 160).
202 - {$keyword_instruction}
203 - Use a {$tone} tone, summarize the page accurately, and end with a clear call to action.
204 - Write a single sentence or two of plain text — no quotes, no markup, no line breaks.
205
206 Format your response as JSON:
207 {
208 \"description\": \"Your meta description here\"
209 }";
210 }
211
212 /**
213 * Build a prompt that explains, in plain language, why a specific SEO score
214 * suggestion matters for this particular post and how to resolve it.
215 *
216 * Powers the "Explain with AI" affordance on each suggestion: instead of the
217 * generic, rule-based guidance, the model reads the actual post context and
218 * returns a short, encouraging explanation grounded in this content.
219 *
220 * @since 1.18.0
221 *
222 * @param string $suggestion The SEO suggestion to explain.
223 * @param string $content Post content for context.
224 * @param string $title Post/SEO title for context.
225 * @param string $target_keyword Focus keyword (may be empty).
226 * @param string $content_type Type of content.
227 * @param string $provider AI provider.
228 * @return string Formatted prompt.
229 */
230 public function build_suggestion_explanation_prompt(string $suggestion, string $content, string $title, string $target_keyword, string $content_type, string $provider = 'openai'): string {
231 $title_line = $title !== ''
232 ? "Post title: \"{$title}\""
233 : 'The post has no title yet.';
234 $keyword_line = $target_keyword !== ''
235 ? "Focus keyword: \"{$target_keyword}\""
236 : 'No focus keyword has been set.';
237
238 return "You are a friendly, expert SEO coach helping a WordPress author improve one {$content_type}.
239
240 {$title_line}
241 {$keyword_line}
242
243 The author sees this SEO suggestion:
244 \"{$suggestion}\"
245
246 Content (for context):
247 {$content}
248
249 Explain this suggestion for THIS specific post. In 2-3 short sentences of plain, encouraging language:
250 1. Why it matters for search rankings or readers (the benefit of fixing it).
251 2. Concretely what the author should change in this post to resolve it.
252
253 Be specific to the content above — reference the actual topic where helpful. Do not restate the suggestion verbatim, do not use jargon without explaining it, and do not use markup, headings, bullet points, or line breaks.
254
255 Return JSON only:
256 {
257 \"explanation\": \"Your plain-language explanation here\"
258 }";
259 }
260
261 /**
262 * Build a prompt that proposes ONE authoritative external source to cite,
263 * so the page gains a dofollow outbound link to a reputable domain.
264 *
265 * @since 1.14.0
266 *
267 * @param string $content Post content for context.
268 * @param string $target_keyword Focus keyword (may be empty).
269 * @param string $content_type Type of content.
270 * @param string $provider AI provider.
271 * @return string Formatted prompt.
272 */
273 public function build_dofollow_link_prompt(string $content, string $target_keyword, string $content_type, string $provider = 'openai'): string {
274 $topic_line = $target_keyword !== ''
275 ? "The {$content_type} focuses on: \"{$target_keyword}\"."
276 : "Infer the main topic from the content.";
277
278 return "You are an SEO editor adding ONE authoritative outbound citation to a {$content_type}.
279
280 {$topic_line}
281
282 Content (for context):
283 {$content}
284
285 Choose ONE real, widely-known, authoritative external source that is genuinely relevant to the topic. Prefer stable URLs on reputable domains that are very unlikely to 404 — for example Wikipedia article pages, official government (.gov) or education (.edu) pages, standards bodies, or the well-known official site of a major organisation. Do NOT invent URLs or use deep/obscure paths.
286
287 Return JSON only:
288 {
289 \"url\": \"https://...\",
290 \"anchor\": \"2-5 word anchor text\",
291 \"sentence\": \"One natural sentence (max 180 characters) that cites the source for further reading. Include the anchor text words somewhere in the sentence.\"
292 }";
293 }
294
295 /**
296 * Build a prompt that writes a short, relevant paragraph naturally using the
297 * focus keyword a given number of times, to raise keyword density into band.
298 *
299 * @since 1.14.0
300 *
301 * @param string $content Post content for context.
302 * @param string $target_keyword Focus keyword.
303 * @param string $content_type Type of content.
304 * @param string $tone Desired tone.
305 * @param int $keyword_mentions How many times to use the keyword.
306 * @param string $provider AI provider.
307 * @return string Formatted prompt.
308 */
309 public function build_keyword_paragraph_prompt(string $content, string $target_keyword, string $content_type, string $tone, int $keyword_mentions, string $provider = 'openai', int $word_target = 90): string {
310 $low = max(60, $word_target - 20);
311 $high = $word_target + 20;
312
313 return "You are an expert SEO writer adding a brief closing section to a {$content_type}.
314
315 Focus keyword: \"{$target_keyword}\"
316
317 Existing content (for context and style):
318 {$content}
319
320 Write a new closing section of about {$low}-{$high} words (one or two short paragraphs) that adds genuine value as a concluding takeaway and flows naturally from the existing content. Use a {$tone} tone. You MUST use the exact focus keyword phrase \"{$target_keyword}\" {$keyword_mentions} times, worked in as naturally as possible. Do not repeat sentences already in the content.
321
322 Return JSON only:
323 {
324 \"paragraph\": \"Your new closing section here\"
325 }";
326 }
327
328 /**
329 * Build content analysis prompt
330 *
331 * @since 1.0.0
332 *
333 * @param string $content Content to analyze
334 * @param array $metadata Existing metadata
335 * @param string $provider AI provider
336 * @return string Formatted prompt
337 */
338 public function build_analysis_prompt(string $content, array $metadata, string $provider = 'openai'): string {
339 $title = $metadata['title'] ?? '';
340 $description = $metadata['description'] ?? '';
341 $focus_keyword = $metadata['focus_keyword'] ?? '';
342
343 $metadata_section = '';
344 if (!empty($title) || !empty($description) || !empty($focus_keyword)) {
345 $metadata_section = "
346 Current SEO Metadata:
347 - Title: {$title}
348 - Description: {$description}
349 - Focus Keyword: {$focus_keyword}
350 ";
351 }
352
353 return "You are an expert SEO analyst. Analyze the following content for SEO optimization opportunities.
354
355 Content:
356 {$content}
357 {$metadata_section}
358
359 Provide a comprehensive SEO analysis including:
360 1. SEO Score (1-100 based on current optimization)
361 2. Content quality assessment
362 3. Keyword optimization analysis
363 4. Readability assessment
364 5. Specific improvement suggestions
365
366 Format your response as JSON:
367 {
368 \"seo_score\": 75,
369 \"content_analysis\": {
370 \"word_count\": 500,
371 \"readability\": \"good\",
372 \"keyword_density\": \"optimal\",
373 \"structure\": \"needs improvement\"
374 },
375 \"suggestions\": [
376 \"Add more subheadings to improve structure\",
377 \"Include the focus keyword in the first paragraph\",
378 \"Optimize meta description length\"
379 ],
380 \"strengths\": [
381 \"Good content length\",
382 \"Clear writing style\"
383 ],
384 \"weaknesses\": [
385 \"Missing focus keyword in title\",
386 \"No internal links\"
387 ]
388 }";
389 }
390 }
391