PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.9.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.9.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
thinkrank / includes / admin / importers / class-seopress-exporter.php

class-seopress-exporter.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.9.0, at includes/admin/importers/class-seopress-exporter.php

682 lines 29.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * SEOPress Exporter
5 *
6 * Reads SEOPress data from postmeta/termmeta/options and normalizes
7 * into the canonical snapshot format.
8 *
9 * CRITICAL: _seopress_robots_index = 'yes' means NOINDEX (inverted logic).
10 *
11 * @package ThinkRank\Admin\Importers
12 * @since 2.0.0
13 */
14
15 declare(strict_types=1);
16
17 namespace ThinkRank\Admin\Importers;
18
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * SEOPress Exporter Class
25 *
26 * @since 2.0.0
27 */
28 class SEOPress_Exporter extends Abstract_Plugin_Exporter {
29
30 /**
31 * Constructor
32 */
33 public function __construct() {
34 $this->plugin_slug = 'seopress';
35 $this->plugin_name = 'SEOPress';
36 $this->plugin_file = 'wp-seopress/seopress.php';
37 $this->meta_key_prefix = '_seopress_';
38 // SEOPress option names all carry the `_option_name` suffix.
39 $this->option_keys = ['seopress_titles_option_name', 'seopress_social_option_name', 'seopress_advanced_option_name'];
40 }
41
42 /**
43 * {@inheritDoc}
44 */
45 public function detect(): bool {
46 global $wpdb;
47
48 $count = (int) $wpdb->get_var(
49 $wpdb->prepare(
50 "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE %s LIMIT 1",
51 $wpdb->esc_like($this->meta_key_prefix) . '%'
52 )
53 );
54
55 return $count > 0;
56 }
57
58 /**
59 * {@inheritDoc}
60 */
61 public function get_available_types(): array {
62 global $wpdb;
63
64 $types = [];
65
66 $post_count = (int) $wpdb->get_var(
67 $wpdb->prepare(
68 "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key LIKE %s",
69 $wpdb->esc_like($this->meta_key_prefix) . '%'
70 )
71 );
72 if ($post_count > 0) {
73 $types['postmeta'] = $post_count;
74 }
75
76 $term_count = (int) $wpdb->get_var(
77 $wpdb->prepare(
78 "SELECT COUNT(DISTINCT term_id) FROM {$wpdb->termmeta} WHERE meta_key LIKE %s",
79 $wpdb->esc_like($this->meta_key_prefix) . '%'
80 )
81 );
82 if ($term_count > 0) {
83 $types['termmeta'] = $term_count;
84 }
85
86 // Check for redirections via seopress_404 CPT
87 $redirect_count = (int) $wpdb->get_var(
88 "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'seopress_404'"
89 );
90 if ($redirect_count > 0) {
91 $types['redirections'] = $redirect_count;
92 }
93
94 foreach (array_merge($this->option_keys, ['seopress_xml_sitemap_option_name', 'seopress_instant_indexing_option_name']) as $key) {
95 if (get_option($key, null) !== null) {
96 $types['settings'] = 1;
97 break;
98 }
99 }
100
101 return $types;
102 }
103
104 /**
105 * {@inheritDoc}
106 */
107 protected function export_postmeta_page(int $page): array {
108 $post_ids = $this->get_post_ids_with_meta($page);
109
110 if (empty($post_ids)) {
111 return [];
112 }
113
114 $records = [];
115 foreach ($post_ids as $post_id) {
116 $post_id = (int) $post_id;
117 $meta = $this->get_all_plugin_meta($post_id);
118
119 if (empty($meta)) {
120 continue;
121 }
122
123 // CRITICAL: SEOPress inverted noindex logic
124 // _seopress_robots_index = 'yes' means NOINDEX
125 $noindex = ($meta['_seopress_robots_index'] ?? '') === 'yes' ? 1 : 0;
126 $nofollow = ($meta['_seopress_robots_follow'] ?? '') === 'yes' ? 1 : 0;
127
128 // Focus keywords are one comma-separated string. The full list
129 // feeds ThinkRank's keyword array (data.focus_keywords); the first
130 // entry stays the legacy single focus keyword.
131 $focus_kw_raw = $meta['_seopress_analysis_target_kw'] ?? '';
132 $focus_keywords = array_values(array_filter(
133 array_map('trim', explode(',', $focus_kw_raw)),
134 static fn($keyword) => $keyword !== ''
135 ));
136 $primary_keyword = $focus_keywords[0] ?? '';
137 $additional_keywords = array_slice($focus_keywords, 1);
138
139 $records[] = [
140 'object_id' => $post_id,
141 'object_type' => 'post',
142 'source_plugin' => $this->plugin_slug,
143 'data' => [
144 'seo_title' => $this->convert_template_variables($meta['_seopress_titles_title'] ?? '', $post_id),
145 'meta_description' => $this->convert_template_variables($meta['_seopress_titles_desc'] ?? '', $post_id),
146 'focus_keyword' => $primary_keyword,
147 'focus_keywords' => $focus_keywords,
148 'canonical_url' => $meta['_seopress_robots_canonical'] ?? '',
149 'noindex' => $noindex,
150 'nofollow' => $nofollow,
151 'noarchive' => ($meta['_seopress_robots_archive'] ?? '') === 'yes' ? 1 : 0,
152 'noimageindex' => ($meta['_seopress_robots_imageindex'] ?? '') === 'yes' ? 1 : 0,
153 'nosnippet' => ($meta['_seopress_robots_snippet'] ?? '') === 'yes' ? 1 : 0,
154 'og_title' => $this->convert_template_variables($meta['_seopress_social_fb_title'] ?? '', $post_id),
155 'og_description' => $this->convert_template_variables($meta['_seopress_social_fb_desc'] ?? '', $post_id),
156 'og_image' => $meta['_seopress_social_fb_img'] ?? '',
157 'twitter_title' => $this->convert_template_variables($meta['_seopress_social_twitter_title'] ?? '', $post_id),
158 'twitter_description' => $this->convert_template_variables($meta['_seopress_social_twitter_desc'] ?? '', $post_id),
159 'twitter_image' => $meta['_seopress_social_twitter_img'] ?? '',
160 'primary_category' => (int) ($meta['_seopress_robots_primary_cat'] ?? 0),
161 'schema_type' => '',
162 ],
163 'extended' => [
164 'focus_keywords_additional' => $additional_keywords,
165 'redirect_enabled' => !empty($meta['_seopress_redirections_enabled']),
166 'redirect_url' => $meta['_seopress_redirections_value'] ?? '',
167 'redirect_type' => $meta['_seopress_redirections_type'] ?? '301',
168 'og_image_width' => $meta['_seopress_social_fb_img_width'] ?? '',
169 'og_image_height' => $meta['_seopress_social_fb_img_height'] ?? '',
170 'twitter_card_type' => $meta['_seopress_social_twitter_card_img_size'] ?? '',
171 ],
172 ];
173 }
174
175 return $records;
176 }
177
178 /**
179 * {@inheritDoc}
180 */
181 protected function export_termmeta_page(int $page): array {
182 $term_ids = $this->get_term_ids_with_meta($page);
183
184 if (empty($term_ids)) {
185 return [];
186 }
187
188 $records = [];
189 foreach ($term_ids as $term_id) {
190 $term_id = (int) $term_id;
191 $meta = $this->get_all_plugin_term_meta($term_id);
192
193 if (empty($meta)) {
194 continue;
195 }
196
197 $noindex = ($meta['_seopress_robots_index'] ?? '') === 'yes' ? 1 : 0;
198 $nofollow = ($meta['_seopress_robots_follow'] ?? '') === 'yes' ? 1 : 0;
199
200 $records[] = [
201 'object_id' => $term_id,
202 'object_type' => 'term',
203 'source_plugin' => $this->plugin_slug,
204 'data' => [
205 'seo_title' => $this->convert_template_variables($meta['_seopress_titles_title'] ?? ''),
206 'meta_description' => $this->convert_template_variables($meta['_seopress_titles_desc'] ?? ''),
207 'canonical_url' => $meta['_seopress_robots_canonical'] ?? '',
208 'noindex' => $noindex,
209 'nofollow' => $nofollow,
210 'og_title' => $this->convert_template_variables($meta['_seopress_social_fb_title'] ?? ''),
211 'og_description' => $this->convert_template_variables($meta['_seopress_social_fb_desc'] ?? ''),
212 ],
213 'extended' => [
214 'og_image' => $meta['_seopress_social_fb_img'] ?? '',
215 'twitter_title' => $meta['_seopress_social_twitter_title'] ?? '',
216 'twitter_description' => $meta['_seopress_social_twitter_desc'] ?? '',
217 ],
218 ];
219 }
220
221 return $records;
222 }
223
224 /**
225 * {@inheritDoc}
226 */
227 protected function export_usermeta_page(int $page): array {
228 return [];
229 }
230
231 /**
232 * {@inheritDoc}
233 */
234 protected function export_settings(): array {
235 $titles = get_option('seopress_titles_option_name', []);
236 $social = get_option('seopress_social_option_name', []);
237 $advanced = get_option('seopress_advanced_option_name', []);
238 $xml_sitemap = get_option('seopress_xml_sitemap_option_name', []);
239 $instant = get_option('seopress_instant_indexing_option_name', []);
240 $pro = get_option('seopress_pro_option_name', []);
241
242 $titles = is_array($titles) ? $titles : [];
243 $social = is_array($social) ? $social : [];
244 $advanced = is_array($advanced) ? $advanced : [];
245 $xml_sitemap = is_array($xml_sitemap) ? $xml_sitemap : [];
246 $instant = is_array($instant) ? $instant : [];
247 $pro = is_array($pro) ? $pro : [];
248
249 return [
250 [
251 'type' => 'settings',
252 'source_plugin' => $this->plugin_slug,
253 'data' => [
254 'separator' => $titles['seopress_titles_sep'] ?? '-',
255 'homepage_title' => $this->convert_template_variables($titles['seopress_titles_home_site_title'] ?? ''),
256 'homepage_description' => $this->convert_template_variables($titles['seopress_titles_home_site_desc'] ?? ''),
257 // Knowledge Graph lives in SEOPress's SOCIAL option
258 // (knowledge_type 'Organization'|'Person', name, img).
259 'organization_name' => $this->convert_template_variables($social['seopress_social_knowledge_name'] ?? ''),
260 'organization_logo' => $social['seopress_social_knowledge_img'] ?? '',
261 'knowledge_graph' => $this->extract_seopress_knowledge_graph($social),
262 'social_profiles' => [
263 'facebook' => $social['seopress_social_accounts_facebook'] ?? '',
264 'twitter' => $social['seopress_social_accounts_twitter'] ?? '',
265 'instagram' => $social['seopress_social_accounts_instagram'] ?? '',
266 'linkedin' => $social['seopress_social_accounts_linkedin'] ?? '',
267 'youtube' => $social['seopress_social_accounts_youtube'] ?? '',
268 'pinterest' => $social['seopress_social_accounts_pinterest'] ?? '',
269 ],
270 // A disabled archive is stronger than noindex — ThinkRank
271 // cannot remove date archives, so both map to noindex.
272 'noindex_archives' => [
273 'date' => !empty($titles['seopress_titles_archives_date_noindex'])
274 || !empty($titles['seopress_titles_archives_date_disable']),
275 'author' => !empty($titles['seopress_titles_archives_author_noindex'])
276 || !empty($titles['seopress_titles_archives_author_disable']),
277 ],
278 'twitter_card_type' => (string) ($social['seopress_social_twitter_card_img_size'] ?? ''),
279 'social_defaults' => [
280 'facebook_app_id' => (string) ($social['seopress_social_facebook_app_id'] ?? ''),
281 'og_default_image' => (string) ($social['seopress_social_facebook_img'] ?? ''),
282 ],
283 // SEOPress's "Bing API key" IS the IndexNow key (served at
284 // /{key}.txt) — carrying it over skips re-verification.
285 'instant_indexing' => [
286 'api_key' => (string) ($instant['seopress_instant_indexing_bing_api_key'] ?? ''),
287 ],
288 ],
289 'extended' => [
290 // Pinterest is applied (ThinkRank renders it); the rest is
291 // preserved and gates /import/cleanup.
292 'webmaster_tools' => array_filter([
293 'google' => (string) ($advanced['seopress_advanced_advanced_google'] ?? ''),
294 'bing' => (string) ($advanced['seopress_advanced_advanced_bing'] ?? ''),
295 'yandex' => (string) ($advanced['seopress_advanced_advanced_yandex'] ?? ''),
296 'baidu' => (string) ($advanced['seopress_advanced_advanced_baidu'] ?? ''),
297 'pinterest' => (string) ($advanced['seopress_advanced_advanced_pinterest'] ?? ''),
298 ]),
299 'title_formats' => $this->extract_seopress_title_formats($titles),
300 'post_type_settings' => $this->extract_seopress_post_type_settings($titles),
301 'author_archives' => $this->extract_seopress_author_archives($titles),
302 // Auto image alt is a single toggle in SEOPress (no format).
303 'image_seo' => !empty($advanced['seopress_advanced_advanced_image_auto_alt_txt'])
304 ? ['add_missing_alt' => true]
305 : [],
306 'breadcrumb_settings' => $this->extract_seopress_breadcrumbs($pro),
307 'sitemap_settings' => $this->normalize_seopress_sitemap($xml_sitemap),
308 ],
309 ],
310 ];
311 }
312
313 /**
314 * Extract SEOPress's Knowledge Graph entity (social option:
315 * knowledge_type 'Organization'|'Person' + knowledge_name).
316 *
317 * @param array $social seopress_social_option_name value
318 * @return array{type: string, name: string}
319 */
320 private function extract_seopress_knowledge_graph(array $social): array {
321 $type = strtolower((string) ($social['seopress_social_knowledge_type'] ?? ''));
322 $name = $this->convert_template_variables((string) ($social['seopress_social_knowledge_name'] ?? ''));
323
324 if (!in_array($type, ['organization', 'person'], true) || $name === '') {
325 return ['type' => '', 'name' => ''];
326 }
327
328 return ['type' => $type, 'name' => $name];
329 }
330
331 /**
332 * Map SEOPress's per-context title templates onto ThinkRank's Site
333 * Identity keys, converting %%vars%% to the identity renderer's %token%
334 * vocabulary.
335 *
336 * @param array $titles seopress_titles_option_name value
337 * @return array Map of ThinkRank title-format key => converted template
338 */
339 private function extract_seopress_title_formats(array $titles): array {
340 $single = is_array($titles['seopress_titles_single_titles'] ?? null) ? $titles['seopress_titles_single_titles'] : [];
341 $tax = is_array($titles['seopress_titles_tax_titles'] ?? null) ? $titles['seopress_titles_tax_titles'] : [];
342
343 $sources = [
344 'homepage_title' => [$titles['seopress_titles_home_site_title'] ?? '', ''],
345 'post_title' => [$single['post']['title'] ?? '', '%post_title%'],
346 'page_title' => [$single['page']['title'] ?? '', '%page_title%'],
347 'category_title' => [$tax['category']['title'] ?? '', '%category_title%'],
348 'tag_title' => [$tax['post_tag']['title'] ?? '', '%tag_title%'],
349 'search_title' => [$titles['seopress_titles_archives_search_title'] ?? '', '%search_term%'],
350 'archive_title' => [$titles['seopress_titles_archives_date_title'] ?? '', '%date%'],
351 'author_title' => [$titles['seopress_titles_archives_author_title'] ?? '', '%author_name%'],
352 ];
353
354 $formats = [];
355 foreach ($sources as $tr_key => [$raw, $context_token]) {
356 $converted = $this->convert_seopress_identity_template((string) $raw, $context_token);
357 if ($converted !== '') {
358 $formats[$tr_key] = $converted;
359 }
360 }
361
362 return $formats;
363 }
364
365 /**
366 * Convert a SEOPress title template into ThinkRank's Site Identity token
367 * vocabulary, preserving structure. Unknown %%vars%% are stripped and a
368 * dangling separator dropped.
369 *
370 * @param string $template Raw SEOPress template (%%var%% syntax)
371 * @param string $context_token Token %%post_title%%/%%term_title%% stands for (may be '')
372 * @return string ThinkRank Site Identity template
373 */
374 private function convert_seopress_identity_template(string $template, string $context_token): string {
375 if ($template === '' || strpos($template, '%%') === false) {
376 return trim($template);
377 }
378
379 $map = [
380 '%%sitetitle%%' => '%site_title%',
381 '%%sitename%%' => '%site_title%',
382 '%%tagline%%' => '%site_description%',
383 '%%sitedesc%%' => '%site_description%',
384 '%%sep%%' => '%sep%',
385 '%%search_keywords%%' => '%search_term%',
386 '%%archive_title%%' => '%archive_title%',
387 '%%archive_date%%' => '%date%',
388 '%%post_date%%' => '%date%',
389 '%%post_author%%' => '%author_name%',
390 ];
391 if ($context_token !== '') {
392 $map['%%post_title%%'] = $context_token;
393 $map['%%title%%'] = $context_token;
394 $map['%%term_title%%'] = $context_token;
395 }
396 $template = str_replace(array_keys($map), array_values($map), $template);
397
398 // Drop remaining SEOPress vars ThinkRank cannot resolve (%%page%%, …).
399 $template = (string) preg_replace('/%%[a-z0-9_-]+%%/i', '', $template);
400
401 $template = (string) preg_replace('/\s{2,}/', ' ', $template);
402 $template = trim($template);
403 $template = (string) preg_replace('/^(?:%sep%)\s*/', '', $template);
404 $template = (string) preg_replace('/\s*(?:%sep%)$/', '', $template);
405
406 return trim($template);
407 }
408
409 /**
410 * Extract SEOPress's per-post-type title/description templates and robots
411 * in the shape migrate_post_type_settings() consumes (Global SEO dialect:
412 * %title%/%sitename%/%excerpt%).
413 *
414 * @param array $titles seopress_titles_option_name value
415 * @return array Map of post_type => {title_template, description_template, custom_robots, robots}
416 */
417 private function extract_seopress_post_type_settings(array $titles): array {
418 $single = is_array($titles['seopress_titles_single_titles'] ?? null) ? $titles['seopress_titles_single_titles'] : [];
419 $settings = [];
420
421 foreach ($single as $post_type => $pt) {
422 if (!is_array($pt)) {
423 continue;
424 }
425
426 $pt_settings = [];
427
428 $title = $this->convert_seopress_global_template((string) ($pt['title'] ?? ''));
429 if ($title !== '') {
430 $pt_settings['title_template'] = $title;
431 }
432
433 $description = $this->convert_seopress_global_template((string) ($pt['description'] ?? ''));
434 if ($description !== '') {
435 $pt_settings['description_template'] = $description;
436 }
437
438 $directives = [];
439 foreach (['noindex', 'nofollow'] as $flag) {
440 if (!empty($pt[$flag])) {
441 $directives[] = $flag;
442 }
443 }
444 if (!empty($directives)) {
445 $pt_settings['custom_robots'] = true;
446 $pt_settings['robots'] = $directives;
447 }
448
449 if (!empty($pt_settings)) {
450 $settings[$post_type] = $pt_settings;
451 }
452 }
453
454 // SEOPress keeps attachment noindex as its own flag outside
455 // single_titles.
456 if (!empty($titles['seopress_titles_attachments_noindex'])) {
457 $settings['attachment']['custom_robots'] = true;
458 $settings['attachment']['robots'] = array_values(array_unique(array_merge(
459 $settings['attachment']['robots'] ?? [],
460 ['noindex']
461 )));
462 }
463
464 return $settings;
465 }
466
467 /**
468 * Convert a SEOPress template into the Global SEO Pattern_Resolver
469 * vocabulary (%title%/%sitename%/%sep%/%excerpt%).
470 *
471 * @param string $template Raw SEOPress template
472 * @return string Converted template
473 */
474 private function convert_seopress_global_template(string $template): string {
475 if ($template === '' || strpos($template, '%%') === false) {
476 return trim($template);
477 }
478
479 $map = [
480 '%%post_title%%' => '%title%',
481 '%%title%%' => '%title%',
482 '%%sitetitle%%' => '%sitename%',
483 '%%sitename%%' => '%sitename%',
484 '%%sep%%' => '%sep%',
485 '%%post_excerpt%%' => '%excerpt%',
486 '%%excerpt%%' => '%excerpt%',
487 '%%post_date%%' => '%date%',
488 '%%post_author%%' => '%author%',
489 '%%post_category%%' => '%category%',
490 ];
491 $template = str_replace(array_keys($map), array_values($map), $template);
492
493 $template = (string) preg_replace('/%%[a-z0-9_-]+%%/i', '', $template);
494 $template = (string) preg_replace('/\s+/', ' ', $template);
495
496 return trim($template);
497 }
498
499 /**
500 * Extract SEOPress's author-archive behaviour for ThinkRank's Author
501 * Archives feature. The noindex flag travels in data.noindex_archives.
502 *
503 * @param array $titles seopress_titles_option_name value
504 * @return array Author archive settings
505 */
506 private function extract_seopress_author_archives(array $titles): array {
507 $has_any = isset($titles['seopress_titles_archives_author_disable'])
508 || !empty($titles['seopress_titles_archives_author_title'])
509 || !empty($titles['seopress_titles_archives_author_desc']);
510 if (!$has_any) {
511 return [];
512 }
513
514 return [
515 'enabled' => empty($titles['seopress_titles_archives_author_disable']),
516 'title' => $this->convert_seopress_identity_template((string) ($titles['seopress_titles_archives_author_title'] ?? ''), '%author_name%'),
517 'description' => $this->convert_seopress_identity_template((string) ($titles['seopress_titles_archives_author_desc'] ?? ''), '%author_name%'),
518 ];
519 }
520
521 /**
522 * Extract SEOPress Pro's breadcrumb settings in the canonical
523 * breadcrumb_settings shape. No-op on SEOPress Free (option absent).
524 *
525 * @param array $pro seopress_pro_option_name value
526 * @return array Canonical breadcrumb_settings payload
527 */
528 private function extract_seopress_breadcrumbs(array $pro): array {
529 if (empty($pro)) {
530 return [];
531 }
532
533 $has_any = isset($pro['seopress_breadcrumbs_enable'])
534 || !empty($pro['seopress_breadcrumbs_separator'])
535 || !empty($pro['seopress_breadcrumbs_i18n_home']);
536 if (!$has_any) {
537 return [];
538 }
539
540 return [
541 'enabled' => !empty($pro['seopress_breadcrumbs_enable']),
542 'home_label' => (string) ($pro['seopress_breadcrumbs_i18n_home'] ?? ''),
543 'separator' => trim(html_entity_decode((string) ($pro['seopress_breadcrumbs_separator'] ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8')),
544 'prefix' => (string) ($pro['seopress_breadcrumbs_i18n_here'] ?? ''),
545 ];
546 }
547
548 /**
549 * Normalize SEOPress's `seopress_xml_sitemap_option` into the flat, canonical
550 * `sitemap_settings` shape the migrator's migrate_sitemap() reads.
551 *
552 * SEOPress stores a global enable flag, an image-inclusion flag, and per-post-
553 * type / per-taxonomy inclusion as { <slug>: { include: '1' } } maps. It
554 * exposes no links-per-file, featured-image or ping toggle, so those keys are
555 * omitted (the migrator skips absent keys). Returns ['has_data' => false] when
556 * there is nothing to migrate.
557 *
558 * NOTE: SEOPress is not installed in this environment; the key paths below
559 * come from SEOPress's documented options schema, not a live capture.
560 *
561 * @param array $sitemap seopress_xml_sitemap_option value
562 * @return array Canonical sitemap_settings payload
563 */
564 private function normalize_seopress_sitemap(array $sitemap): array {
565 if (empty($sitemap)) {
566 return ['has_data' => false];
567 }
568
569 $post_types = is_array($sitemap['seopress_xml_sitemap_post_types_list'] ?? null)
570 ? $sitemap['seopress_xml_sitemap_post_types_list'] : [];
571 $taxonomies = is_array($sitemap['seopress_xml_sitemap_taxonomies_list'] ?? null)
572 ? $sitemap['seopress_xml_sitemap_taxonomies_list'] : [];
573
574 return [
575 'enabled' => !empty($sitemap['seopress_xml_sitemap_general_enable']),
576 'include_images' => !empty($sitemap['seopress_xml_sitemap_img_enable']),
577 'include_posts' => !empty($post_types['post']['include']),
578 'include_pages' => !empty($post_types['page']['include']),
579 'include_categories' => !empty($taxonomies['category']['include']),
580 'include_tags' => !empty($taxonomies['post_tag']['include']),
581 'has_data' => true,
582 ];
583 }
584
585 /**
586 * {@inheritDoc}
587 */
588 protected function export_redirections_page(int $page): array {
589 $offset = ($page - 1) * $this->chunk_size;
590
591 $redirects = get_posts([
592 'post_type' => 'seopress_404',
593 'posts_per_page' => $this->chunk_size,
594 'offset' => $offset,
595 'orderby' => 'ID',
596 'order' => 'ASC',
597 'post_status' => 'any',
598 ]);
599
600 if (empty($redirects)) {
601 return [];
602 }
603
604 $records = [];
605 foreach ($redirects as $redirect) {
606 $meta = get_post_meta($redirect->ID);
607
608 $records[] = [
609 'object_type' => 'redirection',
610 'source_plugin' => $this->plugin_slug,
611 'data' => [],
612 'extended' => [
613 'source_url' => $redirect->post_title ?? '',
614 'target_url' => $meta['_seopress_redirections_value'][0] ?? '',
615 'http_code' => (int) ($meta['_seopress_redirections_type'][0] ?? 301),
616 'is_regex' => !empty($meta['_seopress_redirections_enabled_regex'][0]),
617 'enabled' => !empty($meta['_seopress_redirections_enabled'][0]),
618 ],
619 ];
620 }
621
622 return $records;
623 }
624
625 /**
626 * {@inheritDoc}
627 */
628 protected function convert_template_variables($value, ?int $post_id = null): string {
629 // Foreign data first: booleans/arrays in the source plugin's options
630 // must degrade to '' here, not fatal the migration (see abstract).
631 $value = $this->stringify_template_value($value);
632
633 if (empty($value) || strpos($value, '%%') === false) {
634 return $value;
635 }
636
637 // SEOPress uses %%variable%% syntax similar to Yoast
638 $replacements = [
639 '%%sitetitle%%' => get_bloginfo('name'),
640 '%%sitename%%' => get_bloginfo('name'),
641 '%%tagline%%' => get_bloginfo('description'),
642 '%%sitedesc%%' => get_bloginfo('description'),
643 '%%sep%%' => '-',
644 '%%currentyear%%' => gmdate('Y'),
645 '%%currentmonth%%' => gmdate('F'),
646 '%%currentday%%' => gmdate('j'),
647 '%%currentdate%%' => gmdate('Y-m-d'),
648 ];
649
650 if ($post_id) {
651 $post = get_post($post_id);
652 if ($post) {
653 $replacements['%%post_title%%'] = $post->post_title;
654 $replacements['%%title%%'] = $post->post_title;
655 $replacements['%%post_excerpt%%'] = \ThinkRank\Core\Seo_Text::trim_words(
656 $post->post_excerpt ?: \ThinkRank\Core\Seo_Text::trim_words(wp_strip_all_tags($post->post_content), 55),
657 55
658 );
659 $replacements['%%post_date%%'] = get_the_date('', $post);
660 $replacements['%%post_modified_date%%'] = get_the_modified_date('', $post);
661 $replacements['%%post_author%%'] = get_the_author_meta('display_name', (int) $post->post_author);
662
663 $post_type_obj = get_post_type_object($post->post_type);
664 $replacements['%%post_type%%'] = $post_type_obj ? $post_type_obj->labels->singular_name : '';
665
666 $categories = get_the_category($post_id);
667 $replacements['%%post_category%%'] = !empty($categories) ? $categories[0]->name : '';
668
669 $tags = get_the_tags($post_id);
670 $replacements['%%post_tag%%'] = !empty($tags) ? $tags[0]->name : '';
671 }
672 }
673
674 $value = str_replace(array_keys($replacements), array_values($replacements), $value);
675
676 // Strip remaining unknown variables
677 $value = preg_replace('/%%[a-z0-9_-]+%%/i', '', $value);
678
679 return trim($value);
680 }
681 }
682