PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.14
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.14
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 2.6.4 2.6.3 All 139 releases
metasync / wp-mcp-server / tools / class-mcp-tool-taxonomy-meta.php

class-mcp-tool-taxonomy-meta.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.14, at wp-mcp-server/tools/class-mcp-tool-taxonomy-meta.php

693 lines 27.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MCP Tools for Taxonomy Meta Operations
4 *
5 * Provides MCP tools for managing SEO metadata on categories, tags, and custom taxonomies.
6 * Enables category/tag page SEO optimization with meta titles, descriptions, and social meta.
7 *
8 * @package MetaSync
9 * @subpackage MCP_Server/Tools
10 * @since 2.8.0
11 */
12
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 require_once plugin_dir_path(dirname(__FILE__)) . 'class-mcp-tool-base.php';
18
19 // Term-level SEO plugin sync: propagate MCP-driven term meta updates into
20 // Yoast / Rank Math / AIOSEO term storage.
21 $term_sync_file = plugin_dir_path(dirname(dirname(__FILE__))) . 'includes/class-metasync-term-plugin-sync.php';
22 if (file_exists($term_sync_file)) {
23 require_once $term_sync_file;
24 } else {
25 error_log('[MetaSync] Term plugin sync class missing: ' . $term_sync_file);
26 }
27
28 /**
29 * Get Taxonomy Term Meta Tool
30 *
31 * Retrieves SEO metadata for a category, tag, or custom taxonomy term
32 */
33 class MCP_Tool_Get_Term_Meta extends MCP_Tool_Base {
34
35 public function get_name() {
36 return 'wordpress_get_term_meta';
37 }
38
39 public function get_description() {
40 return 'Get SEO metadata for a taxonomy term (category, tag, or custom taxonomy). Returns meta title, description, Open Graph, and Twitter Card data.';
41 }
42
43 public function get_input_schema() {
44 return [
45 'type' => 'object',
46 'properties' => [
47 'term_id' => [
48 'type' => 'integer',
49 'description' => 'Term ID',
50 'minimum' => 1,
51 ],
52 'taxonomy' => [
53 'type' => 'string',
54 'description' => 'Taxonomy name (optional, for validation)',
55 ],
56 ],
57 'required' => ['term_id'],
58 ];
59 }
60
61 public function execute($params) {
62 $this->validate_params($params);
63 $this->require_capability('manage_categories');
64
65 $term_id = intval($params['term_id']);
66 $taxonomy = isset($params['taxonomy']) ? sanitize_text_field($params['taxonomy']) : '';
67
68 // Get term
69 $term = get_term($term_id, $taxonomy);
70 if (is_wp_error($term) || !$term) {
71 throw new Exception(sprintf("Term not found: %d", $term_id));
72 }
73
74 // Get all meta fields
75 $seo_meta = [
76 'meta_title' => get_term_meta($term_id, '_metasync_metatitle', true),
77 'meta_description' => get_term_meta($term_id, '_metasync_metadesc', true),
78 'focus_keyword' => get_term_meta($term_id, '_metasync_focus_keyword', true),
79 'robots_index' => get_term_meta($term_id, '_metasync_robots_index', true),
80 'canonical_url' => get_term_meta($term_id, '_metasync_canonical_url', true),
81 ];
82
83 $opengraph_meta = [
84 'og_enabled' => get_term_meta($term_id, '_metasync_og_enabled', true),
85 'og_title' => get_term_meta($term_id, '_metasync_og_title', true),
86 'og_description' => get_term_meta($term_id, '_metasync_og_description', true),
87 'og_image' => get_term_meta($term_id, '_metasync_og_image', true),
88 'og_type' => get_term_meta($term_id, '_metasync_og_type', true),
89 ];
90
91 $twitter_meta = [
92 'twitter_card' => get_term_meta($term_id, '_metasync_twitter_card', true),
93 'twitter_title' => get_term_meta($term_id, '_metasync_twitter_title', true),
94 'twitter_description' => get_term_meta($term_id, '_metasync_twitter_description', true),
95 'twitter_image' => get_term_meta($term_id, '_metasync_twitter_image', true),
96 ];
97
98 // OTTO meta (if available)
99 $otto_meta = [
100 'keywords' => get_term_meta($term_id, '_metasync_otto_keywords', true),
101 'og_title' => get_term_meta($term_id, '_metasync_otto_og_title', true),
102 'og_description' => get_term_meta($term_id, '_metasync_otto_og_description', true),
103 'twitter_title' => get_term_meta($term_id, '_metasync_otto_twitter_title', true),
104 'twitter_description' => get_term_meta($term_id, '_metasync_otto_twitter_description', true),
105 'last_update' => get_term_meta($term_id, '_metasync_otto_last_update', true),
106 ];
107
108 return $this->success([
109 'term_id' => $term_id,
110 'term_name' => $term->name,
111 'term_slug' => $term->slug,
112 'taxonomy' => $term->taxonomy,
113 'description' => $term->description,
114 'count' => $term->count,
115 'parent' => $term->parent,
116 'url' => get_term_link($term),
117 'seo_meta' => $seo_meta,
118 'opengraph_meta' => $opengraph_meta,
119 'twitter_meta' => $twitter_meta,
120 'otto_meta' => $otto_meta,
121 ]);
122 }
123 }
124
125 /**
126 * Update Taxonomy Term Meta Tool
127 *
128 * Updates SEO metadata for a category, tag, or custom taxonomy term
129 */
130 class MCP_Tool_Update_Term_Meta extends MCP_Tool_Base {
131
132 public function get_name() {
133 return 'wordpress_update_term_meta';
134 }
135
136 public function get_description() {
137 return 'Update SEO metadata for a taxonomy term (category, tag, etc.). Set meta title, description, Open Graph, and Twitter Card data for archive pages.';
138 }
139
140 public function get_input_schema() {
141 return [
142 'type' => 'object',
143 'properties' => [
144 'term_id' => [
145 'type' => 'integer',
146 'description' => 'Term ID',
147 'minimum' => 1,
148 ],
149 'taxonomy' => [
150 'type' => 'string',
151 'description' => 'Taxonomy name (optional, for validation)',
152 ],
153 'meta_title' => [
154 'type' => 'string',
155 'description' => 'SEO meta title for the term archive page',
156 ],
157 'meta_description' => [
158 'type' => 'string',
159 'description' => 'SEO meta description for the term archive page',
160 ],
161 'focus_keyword' => [
162 'type' => 'string',
163 'description' => 'Primary keyword for SEO',
164 ],
165 'robots_index' => [
166 'type' => 'string',
167 'enum' => ['index', 'noindex'],
168 'description' => 'Indexing directive',
169 ],
170 'canonical_url' => [
171 'type' => 'string',
172 'description' => 'Canonical URL',
173 ],
174 'og_enabled' => [
175 'type' => 'boolean',
176 'description' => 'Enable Open Graph tags',
177 ],
178 'og_title' => [
179 'type' => 'string',
180 'description' => 'Open Graph title',
181 ],
182 'og_description' => [
183 'type' => 'string',
184 'description' => 'Open Graph description',
185 ],
186 'og_image' => [
187 'type' => 'string',
188 'description' => 'Open Graph image URL',
189 ],
190 'twitter_card' => [
191 'type' => 'string',
192 'enum' => ['summary', 'summary_large_image'],
193 'description' => 'Twitter Card type',
194 ],
195 'twitter_title' => [
196 'type' => 'string',
197 'description' => 'Twitter Card title',
198 ],
199 'twitter_description' => [
200 'type' => 'string',
201 'description' => 'Twitter Card description',
202 ],
203 'twitter_image' => [
204 'type' => 'string',
205 'description' => 'Twitter Card image URL',
206 ],
207 ],
208 'required' => ['term_id'],
209 ];
210 }
211
212 public function execute($params) {
213 $this->validate_params($params);
214 $this->require_capability('manage_categories');
215
216 $term_id = intval($params['term_id']);
217 $taxonomy = isset($params['taxonomy']) ? sanitize_text_field($params['taxonomy']) : '';
218
219 // Verify term exists
220 $term = get_term($term_id, $taxonomy);
221 if (is_wp_error($term) || !$term) {
222 throw new Exception(sprintf("Term not found: %d", $term_id));
223 }
224
225 $updated_fields = [];
226
227 // Update SEO meta
228 if (isset($params['meta_title'])) {
229 update_term_meta($term_id, '_metasync_metatitle', sanitize_text_field($params['meta_title']));
230 $updated_fields[] = 'meta_title';
231 }
232
233 if (isset($params['meta_description'])) {
234 update_term_meta($term_id, '_metasync_metadesc', sanitize_textarea_field($params['meta_description']));
235 $updated_fields[] = 'meta_description';
236 }
237
238 if (isset($params['focus_keyword'])) {
239 update_term_meta($term_id, '_metasync_focus_keyword', sanitize_text_field($params['focus_keyword']));
240 $updated_fields[] = 'focus_keyword';
241 }
242
243 if (isset($params['robots_index'])) {
244 update_term_meta($term_id, '_metasync_robots_index', sanitize_text_field($params['robots_index']));
245 $updated_fields[] = 'robots_index';
246 }
247
248 if (isset($params['canonical_url'])) {
249 update_term_meta($term_id, '_metasync_canonical_url', esc_url_raw($params['canonical_url']));
250 $updated_fields[] = 'canonical_url';
251 }
252
253 // Update Open Graph meta
254 if (isset($params['og_enabled'])) {
255 update_term_meta($term_id, '_metasync_og_enabled', $params['og_enabled'] ? '1' : '0');
256 $updated_fields[] = 'og_enabled';
257 }
258
259 if (isset($params['og_title'])) {
260 update_term_meta($term_id, '_metasync_og_title', sanitize_text_field($params['og_title']));
261 $updated_fields[] = 'og_title';
262 }
263
264 if (isset($params['og_description'])) {
265 update_term_meta($term_id, '_metasync_og_description', sanitize_textarea_field($params['og_description']));
266 $updated_fields[] = 'og_description';
267 }
268
269 if (isset($params['og_image'])) {
270 update_term_meta($term_id, '_metasync_og_image', esc_url_raw($params['og_image']));
271 $updated_fields[] = 'og_image';
272 }
273
274 // Update Twitter Card meta
275 if (isset($params['twitter_card'])) {
276 update_term_meta($term_id, '_metasync_twitter_card', sanitize_text_field($params['twitter_card']));
277 $updated_fields[] = 'twitter_card';
278 }
279
280 if (isset($params['twitter_title'])) {
281 update_term_meta($term_id, '_metasync_twitter_title', sanitize_text_field($params['twitter_title']));
282 $updated_fields[] = 'twitter_title';
283 }
284
285 if (isset($params['twitter_description'])) {
286 update_term_meta($term_id, '_metasync_twitter_description', sanitize_textarea_field($params['twitter_description']));
287 $updated_fields[] = 'twitter_description';
288 }
289
290 if (isset($params['twitter_image'])) {
291 update_term_meta($term_id, '_metasync_twitter_image', esc_url_raw($params['twitter_image']));
292 $updated_fields[] = 'twitter_image';
293 }
294
295 // Propagate MetaSync term meta into the active SEO plugins' term storage
296 // (Yoast / Rank Math / AIOSEO) so archive pages render these values.
297 if (!empty($updated_fields) && class_exists('Metasync_Term_Plugin_Sync')) {
298 $key_map = [
299 'meta_title' => 'title',
300 'meta_description' => 'desc',
301 'og_title' => 'og_title',
302 'og_description' => 'og_desc',
303 'og_image' => 'og_image',
304 'twitter_title' => 'twitter_title',
305 'twitter_description' => 'twitter_desc',
306 'canonical_url' => 'canonical',
307 'robots_index' => 'noindex',
308 ];
309 $sync_data = [];
310 foreach ($key_map as $param_key => $sync_key) {
311 if (isset($params[$param_key])) {
312 $sync_data[$sync_key] = $params[$param_key];
313 }
314 }
315 if (!empty($sync_data)) {
316 Metasync_Term_Plugin_Sync::get_instance()->sync_term(
317 $term_id,
318 $term->taxonomy,
319 $sync_data
320 );
321 }
322 }
323
324 if (empty($updated_fields)) {
325 throw new Exception('No meta fields provided to update');
326 }
327
328 return $this->success([
329 'term_id' => $term_id,
330 'term_name' => $term->name,
331 'taxonomy' => $term->taxonomy,
332 'updated_fields' => $updated_fields,
333 'fields_count' => count($updated_fields),
334 'message' => count($updated_fields) . ' meta field(s) updated successfully',
335 ]);
336 }
337 }
338
339 /**
340 * Bulk Update Taxonomy Term Meta Tool
341 *
342 * Updates SEO metadata for multiple terms at once
343 */
344 class MCP_Tool_Bulk_Update_Term_Meta extends MCP_Tool_Base {
345
346 public function get_name() {
347 return 'wordpress_bulk_update_term_meta';
348 }
349
350 public function get_description() {
351 return 'Update SEO metadata for multiple taxonomy terms at once (max 50 terms per request)';
352 }
353
354 public function get_input_schema() {
355 return [
356 'type' => 'object',
357 'properties' => [
358 'updates' => [
359 'type' => 'array',
360 'description' => 'Array of term meta updates',
361 'items' => [
362 'type' => 'object',
363 'properties' => [
364 'term_id' => [
365 'type' => 'integer',
366 'description' => 'Term ID',
367 ],
368 'meta_title' => [
369 'type' => 'string',
370 'description' => 'SEO meta title',
371 ],
372 'meta_description' => [
373 'type' => 'string',
374 'description' => 'SEO meta description',
375 ],
376 'focus_keyword' => [
377 'type' => 'string',
378 'description' => 'Focus keyword',
379 ],
380 'canonical_url' => [
381 'type' => 'string',
382 'description' => 'Canonical URL',
383 ],
384 'og_enabled' => [
385 'type' => 'boolean',
386 'description' => 'Enable Open Graph tags',
387 ],
388 'og_title' => [
389 'type' => 'string',
390 'description' => 'Open Graph title',
391 ],
392 'og_description' => [
393 'type' => 'string',
394 'description' => 'Open Graph description',
395 ],
396 'og_image' => [
397 'type' => 'string',
398 'description' => 'Open Graph image URL',
399 ],
400 'twitter_card' => [
401 'type' => 'string',
402 'enum' => ['summary', 'summary_large_image'],
403 'description' => 'Twitter Card type',
404 ],
405 'twitter_title' => [
406 'type' => 'string',
407 'description' => 'Twitter Card title',
408 ],
409 'twitter_description' => [
410 'type' => 'string',
411 'description' => 'Twitter Card description',
412 ],
413 ],
414 'required' => ['term_id'],
415 ],
416 'maxItems' => 50,
417 ],
418 ],
419 'required' => ['updates'],
420 ];
421 }
422
423 public function execute($params) {
424 $this->validate_params($params);
425 $this->require_capability('manage_categories');
426
427 if (!is_array($params['updates'])) {
428 throw new Exception('updates must be an array');
429 }
430
431 $updates = $params['updates'];
432
433 if (count($updates) > 50) {
434 throw new Exception('Maximum 50 terms can be updated at once');
435 }
436
437 $results = [
438 'success' => [],
439 'failed' => [],
440 ];
441
442 foreach ($updates as $update) {
443 try {
444 if (!isset($update['term_id'])) {
445 throw new Exception('Missing required field: term_id');
446 }
447
448 $term_id = intval($update['term_id']);
449
450 // Verify term exists
451 $term = get_term($term_id);
452 if (is_wp_error($term) || !$term) {
453 $results['failed'][] = [
454 'term_id' => $term_id,
455 'error' => 'Term not found',
456 ];
457 continue;
458 }
459
460 $updated_fields = [];
461
462 // Update meta fields
463 if (isset($update['meta_title'])) {
464 update_term_meta($term_id, '_metasync_metatitle', sanitize_text_field($update['meta_title']));
465 $updated_fields[] = 'meta_title';
466 }
467
468 if (isset($update['meta_description'])) {
469 update_term_meta($term_id, '_metasync_metadesc', sanitize_textarea_field($update['meta_description']));
470 $updated_fields[] = 'meta_description';
471 }
472
473 if (isset($update['focus_keyword'])) {
474 update_term_meta($term_id, '_metasync_focus_keyword', sanitize_text_field($update['focus_keyword']));
475 $updated_fields[] = 'focus_keyword';
476 }
477
478 if (isset($update['canonical_url'])) {
479 update_term_meta($term_id, '_metasync_canonical_url', esc_url_raw($update['canonical_url']));
480 $updated_fields[] = 'canonical_url';
481 }
482
483 if (isset($update['og_enabled'])) {
484 update_term_meta($term_id, '_metasync_og_enabled', $update['og_enabled'] ? '1' : '0');
485 $updated_fields[] = 'og_enabled';
486 }
487
488 if (isset($update['og_title'])) {
489 update_term_meta($term_id, '_metasync_og_title', sanitize_text_field($update['og_title']));
490 $updated_fields[] = 'og_title';
491 }
492
493 if (isset($update['og_description'])) {
494 update_term_meta($term_id, '_metasync_og_description', sanitize_textarea_field($update['og_description']));
495 $updated_fields[] = 'og_description';
496 }
497
498 if (isset($update['og_image'])) {
499 update_term_meta($term_id, '_metasync_og_image', esc_url_raw($update['og_image']));
500 $updated_fields[] = 'og_image';
501 }
502
503 if (isset($update['twitter_card'])) {
504 $allowed_cards = ['summary', 'summary_large_image'];
505 if (!in_array($update['twitter_card'], $allowed_cards, true)) {
506 $results['failed'][] = ['term_id' => $term_id, 'error' => 'Invalid twitter_card value: ' . sanitize_text_field($update['twitter_card'])];
507 continue;
508 }
509 update_term_meta($term_id, '_metasync_twitter_card', sanitize_text_field($update['twitter_card']));
510 $updated_fields[] = 'twitter_card';
511 }
512
513 if (isset($update['twitter_title'])) {
514 update_term_meta($term_id, '_metasync_twitter_title', sanitize_text_field($update['twitter_title']));
515 $updated_fields[] = 'twitter_title';
516 }
517
518 if (isset($update['twitter_description'])) {
519 update_term_meta($term_id, '_metasync_twitter_description', sanitize_textarea_field($update['twitter_description']));
520 $updated_fields[] = 'twitter_description';
521 }
522
523 // Propagate this term's MCP-driven updates into the active
524 // SEO plugins' term storage (Yoast / Rank Math / AIOSEO).
525 if (!empty($updated_fields) && class_exists('Metasync_Term_Plugin_Sync')) {
526 $bulk_key_map = [
527 'meta_title' => 'title',
528 'meta_description' => 'desc',
529 'og_title' => 'og_title',
530 'og_description' => 'og_desc',
531 'og_image' => 'og_image',
532 'twitter_title' => 'twitter_title',
533 'twitter_description' => 'twitter_desc',
534 'canonical_url' => 'canonical',
535 'robots_index' => 'noindex',
536 ];
537 $sync_data = [];
538 foreach ($bulk_key_map as $param_key => $sync_key) {
539 if (isset($update[$param_key])) {
540 $sync_data[$sync_key] = $update[$param_key];
541 }
542 }
543 if (!empty($sync_data)) {
544 Metasync_Term_Plugin_Sync::get_instance()->sync_term(
545 $term_id,
546 $term->taxonomy,
547 $sync_data
548 );
549 }
550 }
551
552 $results['success'][] = [
553 'term_id' => $term_id,
554 'term_name' => $term->name,
555 'taxonomy' => $term->taxonomy,
556 'updated_fields' => $updated_fields,
557 ];
558
559 } catch (Exception $e) {
560 $results['failed'][] = [
561 'term_id' => isset($update['term_id']) ? $update['term_id'] : null,
562 'error' => $e->getMessage(),
563 ];
564 }
565 }
566
567 return $this->success([
568 'total_requested' => count($updates),
569 'success_count' => count($results['success']),
570 'failed_count' => count($results['failed']),
571 'results' => $results,
572 'message' => count($results['success']) . ' term(s) updated successfully',
573 ]);
574 }
575 }
576
577 /**
578 * List Terms with SEO Meta Tool
579 *
580 * Lists all terms with their SEO metadata for a taxonomy
581 */
582 class MCP_Tool_List_Terms_With_Meta extends MCP_Tool_Base {
583
584 public function get_name() {
585 return 'wordpress_list_terms_with_meta';
586 }
587
588 public function get_description() {
589 return 'List all terms for a taxonomy with their SEO metadata. Useful for auditing category/tag SEO.';
590 }
591
592 public function get_input_schema() {
593 return [
594 'type' => 'object',
595 'properties' => [
596 'taxonomy' => [
597 'type' => 'string',
598 'description' => 'Taxonomy name (category, post_tag, or custom taxonomy)',
599 ],
600 'hide_empty' => [
601 'type' => 'boolean',
602 'description' => 'Hide terms with no posts (default: false)',
603 ],
604 'orderby' => [
605 'type' => 'string',
606 'enum' => ['name', 'slug', 'count', 'id'],
607 'description' => 'Order by field (default: name)',
608 ],
609 'order' => [
610 'type' => 'string',
611 'enum' => ['ASC', 'DESC'],
612 'description' => 'Sort order (default: ASC)',
613 ],
614 ],
615 'required' => ['taxonomy'],
616 ];
617 }
618
619 public function execute($params) {
620 $this->validate_params($params);
621 $this->require_capability('manage_categories');
622
623 $taxonomy = sanitize_text_field($params['taxonomy']);
624 $hide_empty = isset($params['hide_empty']) ? (bool)$params['hide_empty'] : false;
625 $orderby = isset($params['orderby']) ? sanitize_text_field($params['orderby']) : 'name';
626 $order = isset($params['order']) ? sanitize_text_field($params['order']) : 'ASC';
627
628 // Verify taxonomy exists
629 if (!taxonomy_exists($taxonomy)) {
630 throw new Exception(sprintf("Taxonomy not found: %s", $taxonomy));
631 }
632
633 // Get terms
634 $terms = get_terms([
635 'taxonomy' => $taxonomy,
636 'hide_empty' => $hide_empty,
637 'orderby' => $orderby,
638 'order' => $order,
639 ]);
640
641 if (is_wp_error($terms)) {
642 throw new Exception('Failed to retrieve terms: ' . $terms->get_error_message());
643 }
644
645 $terms_data = [];
646 $stats = [
647 'with_meta_title' => 0,
648 'with_meta_description' => 0,
649 'with_og_data' => 0,
650 'missing_seo' => 0,
651 ];
652
653 foreach ($terms as $term) {
654 $meta_title = get_term_meta($term->term_id, '_metasync_metatitle', true);
655 $meta_desc = get_term_meta($term->term_id, '_metasync_metadesc', true);
656 $og_enabled = get_term_meta($term->term_id, '_metasync_og_enabled', true);
657 $og_title = get_term_meta($term->term_id, '_metasync_og_title', true);
658
659 // Update stats
660 if (!empty($meta_title)) $stats['with_meta_title']++;
661 if (!empty($meta_desc)) $stats['with_meta_description']++;
662 if ($og_enabled === '1' && !empty($og_title)) $stats['with_og_data']++;
663 if (empty($meta_title) && empty($meta_desc)) $stats['missing_seo']++;
664
665 $terms_data[] = [
666 'term_id' => $term->term_id,
667 'name' => $term->name,
668 'slug' => $term->slug,
669 'count' => $term->count,
670 'parent' => $term->parent,
671 'url' => get_term_link($term),
672 'seo_status' => [
673 'has_meta_title' => !empty($meta_title),
674 'has_meta_description' => !empty($meta_desc),
675 'has_og_data' => $og_enabled === '1' && !empty($og_title),
676 'needs_attention' => empty($meta_title) && empty($meta_desc),
677 ],
678 'meta_preview' => [
679 'meta_title' => !empty($meta_title) ? mb_substr($meta_title, 0, 60) . '...' : null,
680 'meta_description' => !empty($meta_desc) ? mb_substr($meta_desc, 0, 100) . '...' : null,
681 ],
682 ];
683 }
684
685 return $this->success([
686 'taxonomy' => $taxonomy,
687 'total_terms' => count($terms_data),
688 'stats' => $stats,
689 'terms' => $terms_data,
690 ]);
691 }
692 }
693