PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / ai / content-planner / infrastructure / recent-content / recent-content-collector.php

recent-content-collector.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI trunk, at src/ai/content-planner/infrastructure/recent-content/recent-content-collector.php

167 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
4
5 namespace Yoast\WP\SEO\AI\Content_Planner\Infrastructure\Recent_Content;
6
7 use WP_Term;
8 use WPSEO_Meta;
9 use Yoast\WP\SEO\AI\Content_Planner\Domain\Category;
10 use Yoast\WP\SEO\AI\Content_Planner\Domain\Post;
11 use Yoast\WP\SEO\AI\Content_Planner\Domain\Post_List;
12 use Yoast\WP\SEO\Models\Indexable;
13 use Yoast\WP\SEO\Repositories\Indexable_Repository;
14 use Yoast\WP\SEO\Repositories\Primary_Term_Repository;
15
16 /**
17 * Collects the most recent published posts from indexables.
18 */
19 class Recent_Content_Collector {
20
21 /**
22 * The maximum number of posts to collect.
23 */
24 private const LIMIT = 100;
25 /**
26 * The valid schema types that we want to send to the API. Otherwise this field is omitted.
27 */
28 public const VALID_SCHEMA_TYPES = [
29 'SocialMediaPosting',
30 'NewsArticle',
31 'AdvertiserContentArticle',
32 'SatiricalArticle',
33 'ScholarlyArticle',
34 'TechArticle',
35 'Report',
36 ];
37
38 /**
39 * The indexable repository.
40 *
41 * @var Indexable_Repository
42 */
43 private $indexable_repository;
44
45 /**
46 * The primary term repository.
47 *
48 * @var Primary_Term_Repository
49 */
50 private $primary_term_repository;
51
52 /**
53 * The constructor.
54 *
55 * @param Indexable_Repository $indexable_repository The indexable repository.
56 * @param Primary_Term_Repository $primary_term_repository The primary term repository.
57 */
58 public function __construct(
59 Indexable_Repository $indexable_repository,
60 Primary_Term_Repository $primary_term_repository
61 ) {
62 $this->indexable_repository = $indexable_repository;
63 $this->primary_term_repository = $primary_term_repository;
64 }
65
66 /**
67 * Collects the 100 most recent published, indexed posts.
68 *
69 * @param string $post_type The post type to collect.
70 *
71 * @return Post_List The list of recent posts.
72 */
73 public function collect( string $post_type ): Post_List {
74 $results = $this->indexable_repository->get_recently_modified_posts( $post_type, self::LIMIT, false );
75
76 return $this->map_to_post_list( $results );
77 }
78
79 /**
80 * Collects the most recent indexed about page.
81 *
82 * @param string $post_type The post type to collect.
83 *
84 * @return array<string>|false The about page needed information.
85 */
86 public function collect_about_page( string $post_type ) {
87 $indexable = $this->indexable_repository->get_most_recent_about_page( $post_type );
88
89 if ( $indexable ) {
90 return [
91 'title' => ( $indexable->breadcrumb_title ?? '' ),
92 'description' => ( $indexable->description ?? '' ),
93 ];
94 }
95
96 return false;
97 }
98
99 /**
100 * Maps indexable results to a Post_List.
101 *
102 * @param Indexable[] $indexables The indexable results.
103 *
104 * @return Post_List The mapped post list.
105 */
106 private function map_to_post_list( array $indexables ): Post_List {
107 $post_list = new Post_List();
108
109 foreach ( $indexables as $indexable ) {
110 $post_list->add(
111 new Post(
112 ( $indexable->breadcrumb_title ?? '' ),
113 ( $indexable->description ?? '' ),
114 $this->get_primary_category( $indexable->object_id ?? 0 ),
115 ( $indexable->primary_focus_keyword ?? '' ),
116 ( $indexable->is_cornerstone ?? false ),
117 ( $indexable->object_last_modified ?? '' ),
118 ( \in_array( $indexable->schema_article_type, self::VALID_SCHEMA_TYPES, true ) ? $indexable->schema_article_type : null ),
119 ),
120 );
121 }
122
123 return $post_list;
124 }
125
126 /**
127 * Resolves the post's primary category into a Category value object.
128 *
129 * Looks up Yoast's explicit primary term first, then the `_yoast_wpseo_primary_category` post meta,
130 * and finally falls back to the first category WordPress has assigned to the post.
131 *
132 * @param int $post_id The post id.
133 *
134 * @return Category|null The primary category, or null when the post has no category at all.
135 */
136 private function get_primary_category( int $post_id ): ?Category {
137 // We try to find the primary term using the repository first, which uses indexables.
138 $primary_term = $this->primary_term_repository->find_by_post_id_and_taxonomy( $post_id, 'category', false );
139
140 if ( $primary_term ) {
141 $term_id = $primary_term->term_id;
142 }
143 // If the repository did not return a primary term, we try to find it using the post meta, which is how older versions of Yoast SEO stored the primary category.
144 else {
145 $term_id = \get_post_meta( $post_id, WPSEO_Meta::$meta_prefix . 'primary_category', true );
146 }
147
148 // No primary term has been set in Yoast SEO, so we fall back to the first category WordPress has assigned to the post, if any.
149 if ( empty( $term_id ) ) {
150 $category_ids = \wp_get_post_categories( $post_id );
151 $term_id = ( $category_ids[0] ?? 0 );
152 }
153
154 // If there are no categories at all, we return null.
155 if ( empty( $term_id ) ) {
156 return null;
157 }
158
159 $term = \get_term( (int) $term_id, 'category' );
160 if ( ! $term instanceof WP_Term ) {
161 return null;
162 }
163
164 return new Category( $term->name, $term->term_id );
165 }
166 }
167