PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.10.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 / abilities / content / class-get-duplicate-snippets.php

class-get-duplicate-snippets.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.10.0, at includes/abilities/content/class-get-duplicate-snippets.php

179 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Get duplicate snippets ability.
4 *
5 * @package ThinkRank\Abilities\Content
6 */
7
8 declare(strict_types=1);
9
10 namespace ThinkRank\Abilities\Content;
11
12 use ThinkRank\Abilities\Ability_Base;
13 use ThinkRank\SEO\Duplicate_Snippets;
14 use ThinkRank\SEO\Snippet_Issues;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * Reports published pages that render the same SEO title, or the same meta
22 * description, as another page anywhere on the site (#564).
23 *
24 * Distinct from list-snippet-issues, which answers "what is wrong with this
25 * post type's snippets" one post type at a time. A duplicate is a relationship
26 * between two pages that are often different post types, so it needs a report
27 * whose scope is the whole site and whose unit is the repeated value rather
28 * than the post.
29 */
30 class Get_Duplicate_Snippets extends Ability_Base {
31 /**
32 * Constructor.
33 */
34 public function __construct() {
35 $this->id = 'thinkrank/get-duplicate-snippets';
36 $this->label = __( 'Get ThinkRank Duplicate Snippets', 'thinkrank' );
37 $this->description = __( 'Find published pages across every post type that render the same search title, or the same meta description, as another page. Groups them by the repeated value, largest group first, so one bad post type template shows up as a single finding rather than fifty. Covers published content only, since a draft cannot compete in search. Returns the value, how many pages carry it, and which pages they are. Fix one with update-post-seo.', 'thinkrank' );
38 }
39
40 /**
41 * {@inheritDoc}
42 *
43 * @return array<string, bool|float|string>
44 */
45 public function get_annotations() {
46 return [
47 'readonly' => true,
48 'destructive' => false,
49 'idempotent' => true,
50 'priority' => 1.0,
51 'openWorldHint' => false,
52 ];
53 }
54
55 /**
56 * {@inheritDoc}
57 *
58 * @return array<string, mixed>
59 */
60 public function get_input_schema() {
61 return [
62 'type' => 'object',
63 'additionalProperties' => false,
64 'properties' => [
65 'issue' => [
66 'type' => 'string',
67 'enum' => array_merge( [ '' ], Snippet_Issues::grouped() ),
68 'default' => '',
69 'description' => __( 'Limit the groups to one kind. Empty returns both. Counts are always returned for both.', 'thinkrank' ),
70 ],
71 'refresh' => [
72 'type' => 'boolean',
73 'default' => false,
74 'description' => __( 'Discard the index and rescan every published page. The report already updates whenever a title, description or template changes, so this is only needed when it looks stale. The rescan runs in batches: pending rises, so call again without refresh until pending is zero.', 'thinkrank' ),
75 ],
76 ],
77 ];
78 }
79
80 /**
81 * {@inheritDoc}
82 *
83 * @return array<string, mixed>
84 */
85 public function get_output_schema() {
86 return [
87 'type' => 'object',
88 'properties' => [
89 'pending' => [
90 'type' => 'integer',
91 'description' => __( 'Pages not scanned yet. When above zero the counts are a floor, not the answer; call again to continue.', 'thinkrank' ),
92 ],
93 'scanned' => [
94 'type' => 'integer',
95 'description' => __( 'Published pages the report covers.', 'thinkrank' ),
96 ],
97 'counts' => [
98 'type' => 'object',
99 'description' => __( 'Pages caught up in a duplicate, per issue.', 'thinkrank' ),
100 ],
101 'group_counts' => [
102 'type' => 'object',
103 'description' => __( 'Distinct repeated values, per issue.', 'thinkrank' ),
104 ],
105 'truncated' => [
106 'type' => 'object',
107 'description' => __( 'Per issue, whether there are more repeated values than the groups listed.', 'thinkrank' ),
108 ],
109 'groups' => [
110 'type' => 'object',
111 'description' => __( 'Repeated values per issue, largest group first.', 'thinkrank' ),
112 ],
113 ],
114 ];
115 }
116
117 /**
118 * Execute ability.
119 *
120 * @param array<string, mixed> $input Ability input payload.
121 * @return array<string, mixed>|\WP_Error
122 */
123 public function execute( $input ) {
124 $issue = isset( $input['issue'] ) ? sanitize_key( (string) $input['issue'] ) : '';
125 $refresh = ! empty( $input['refresh'] );
126
127 if ( '' !== $issue && ! in_array( $issue, Snippet_Issues::grouped(), true ) ) {
128 return new \WP_Error(
129 'thinkrank_invalid_issue',
130 __( 'Unknown issue. Duplicates are reported for duplicate_title and duplicate_description.', 'thinkrank' ),
131 [ 'status' => 400 ]
132 );
133 }
134
135 $report = Duplicate_Snippets::report( $refresh );
136
137 $groups = [];
138 foreach ( $report['groups'] as $key => $found ) {
139 if ( '' !== $issue && $key !== $issue ) {
140 continue;
141 }
142
143 $groups[ $key ] = array_map(
144 static function ( array $group ): array {
145 // The grouping hash is an implementation detail of the
146 // index; the value and the pages are the finding.
147 return [
148 'value' => $group['value'],
149 'total' => $group['total'],
150 'pages' => array_map(
151 static function ( array $post ): array {
152 return [
153 'post_id' => $post['post_id'],
154 'post_title' => $post['post_title'],
155 'post_type' => $post['post_type'],
156 'permalink' => $post['permalink'],
157 ];
158 },
159 $group['posts']
160 ),
161 // True when the group has more pages than are named.
162 'partial' => $group['partial'],
163 ];
164 },
165 $found
166 );
167 }
168
169 return [
170 'pending' => $report['pending'],
171 'scanned' => $report['scanned'],
172 'counts' => $report['counts'],
173 'group_counts' => $report['group_counts'],
174 'truncated' => $report['truncated'],
175 'groups' => $groups,
176 ];
177 }
178 }
179