PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.4
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.4
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 / schema-aggregator / application / enhancement / article-schema-enhancer.php

article-schema-enhancer.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.4, at src/schema-aggregator/application/enhancement/article-schema-enhancer.php

225 lines 6.3 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 namespace Yoast\WP\SEO\Schema_Aggregator\Application\Enhancement;
4
5 use Exception;
6 use Yoast\WP\SEO\Models\Indexable;
7 use Yoast\WP\SEO\Schema_Aggregator\Domain\Enhancement\Schema_Enhancement_Interface;
8 use Yoast\WP\SEO\Schema_Aggregator\Domain\Schema_Piece;
9 use Yoast\WP\SEO\Schema_Aggregator\Infrastructure\Enhancement\Article_Config;
10
11 /**
12 * The Article schema pieces enhancer.
13 */
14 class Article_Schema_Enhancer extends Abstract_Schema_Enhancer implements Schema_Enhancement_Interface {
15
16 /**
17 * The config.
18 *
19 * @var Article_Config
20 */
21 private $config;
22
23 /**
24 * Sets the Article_Config instance.
25 *
26 * @required
27 *
28 * @param Article_Config $config The Article_Config instance.
29 *
30 * @return void
31 */
32 public function set_article_config( Article_Config $config ) {
33 $this->config = $config;
34 }
35
36 /**
37 * Enhances specific Article schema pieces.
38 *
39 * @param Schema_Piece $schema_piece The schema piece to enhance.
40 * @param Indexable $indexable The indexable object that is the source of the schema piece.
41 *
42 * @return Schema_Piece The enhanced schema piece.
43 */
44 public function enhance( Schema_Piece $schema_piece, Indexable $indexable ): Schema_Piece {
45 $schema_data = $schema_piece->get_data();
46 if ( ! isset( $schema_data['@type'] ) ) {
47 return $schema_piece;
48 }
49 if (
50 \in_array(
51 $schema_data['@type'],
52 [
53 'Article',
54 'NewsArticle',
55 'BlogPosting',
56 ],
57 true,
58 ) ) {
59 $schema_data = $this->enhance_schema_piece( $schema_data, $indexable );
60 }
61
62 if (
63 \is_array( $schema_data['@type'] ) && \in_array( 'Article', $schema_data['@type'], true ) ) {
64 $schema_data = $this->enhance_schema_piece( $schema_data, $indexable );
65 }
66
67 return new Schema_Piece( $schema_data, $schema_piece->get_type() );
68 }
69
70 /**
71 * Enhance a single schema piece
72 *
73 * @param array<string> $schema_data The schema data to enhance.
74 * @param Indexable $indexable The indexable object that is the source of the schema piece.
75 *
76 * @return array<string> The enhanced schema data.
77 */
78 protected function enhance_schema_piece( array $schema_data, Indexable $indexable ): array {
79 try {
80 $has_excerpt = false;
81
82 if ( $this->config->is_enhancement_enabled( 'use_excerpt' ) ) {
83 $excerpt = $this->get_excerpt( $indexable->object_id );
84 $has_excerpt = ! empty( $excerpt );
85
86 if ( $has_excerpt && ! isset( $schema_data['description'] ) ) {
87 $schema_data['description'] = $excerpt;
88 }
89 }
90
91 if ( $this->config->is_enhancement_enabled( 'article_body' ) && ! isset( $schema_data['articleBody'] ) ) {
92 if ( $this->config->should_include_article_body( $has_excerpt ) ) {
93 $article_body = $this->get_article_body( $indexable->object_id );
94 if ( ! empty( $article_body ) ) {
95 $schema_data['articleBody'] = $article_body;
96 }
97 }
98 }
99
100 if ( $this->config->is_enhancement_enabled( 'keywords' ) ) {
101 $keywords = $this->get_article_keywords( $indexable->object_id );
102 if ( ! empty( $keywords ) ) {
103 $existing = (array) ( $schema_data['keywords'] ?? [] );
104 $schema_data['keywords'] = \implode( ', ', \array_unique( \array_merge( $existing, $keywords ) ) );
105 }
106 }
107
108 return $schema_data;
109 } catch ( Exception $e ) {
110 return $schema_data;
111 }
112 }
113
114 /**
115 * Get article keywords
116 *
117 * Extracts post tags and optionally categories as keywords.
118 *
119 * @param int $post_id Post ID.
120 *
121 * @return array<string> Array of keyword strings.
122 */
123 private function get_article_keywords( int $post_id ): array {
124 try {
125 $keywords = [];
126
127 if ( $this->config->get_config_value( 'categories_as_keywords', false ) ) {
128 $categories = \get_the_category( $post_id );
129 if ( \is_array( $categories ) && ! empty( $categories ) ) {
130 foreach ( $categories as $category ) {
131 if ( isset( $category->name ) && $category->name !== 'Uncategorized' ) {
132 $keywords[] = $category->name;
133 }
134 }
135 }
136 }
137
138 return \array_unique( $keywords );
139 } catch ( Exception $e ) {
140 return [];
141 }
142 }
143
144 /**
145 * Get article excerpt for description field
146 *
147 * Retrieves post excerpt with robust validation (no empty/whitespace-only).
148 * Falls back to auto-generated excerpt from content unless prefer_manual is enabled.
149 *
150 * @param int $post_id Post ID.
151 *
152 * @return string|null Excerpt or null if unavailable/invalid.
153 */
154 private function get_excerpt( int $post_id ): ?string {
155 try {
156 $excerpt = \get_post_field( 'post_excerpt', $post_id );
157 if ( \is_wp_error( $excerpt ) ) {
158 $excerpt = '';
159 }
160
161 if ( empty( $excerpt ) || \trim( $excerpt ) === '' ) {
162 if ( $this->config->get_config_value( 'excerpt_prefer_manual', false ) ) {
163 return null;
164 }
165
166 $content = \get_post_field( 'post_content', $post_id );
167
168 if ( \is_wp_error( $content ) || empty( $content ) ) {
169 return null;
170 }
171 $excerpt = \wp_trim_excerpt( $content, $post_id );
172
173 if ( empty( $excerpt ) || \trim( $excerpt ) === '' ) {
174 return null;
175 }
176 }
177 $excerpt = \wp_strip_all_tags( $excerpt );
178
179 // Apply max length if configured.
180 $max_length = $this->config->get_config_value( 'excerpt_max_length', 0 );
181 $excerpt = $this->trim_content_to_max_length( $max_length, $excerpt );
182
183 $excerpt = \trim( $excerpt );
184
185 return ( $excerpt !== '' ) ? $excerpt : null;
186 } catch ( Exception $e ) {
187 return null;
188 }
189 }
190
191 /**
192 * Get article body (full post content)
193 *
194 * Extracts full post content with optional HTML and shortcode stripping.
195 * Respects max_length configuration if set.
196 *
197 * @param int $post_id Post ID.
198 *
199 * @return string|null Article body or null if unavailable.
200 */
201 private function get_article_body( int $post_id ): ?string {
202 try {
203 $content = \get_post_field( 'post_content', $post_id );
204
205 if ( \is_wp_error( $content ) || empty( $content ) ) {
206 return null;
207 }
208
209 if ( $this->config->get_config_value( 'strip_shortcodes_from_body', true ) ) {
210 $content = \strip_shortcodes( $content );
211 }
212
213 if ( $this->config->get_config_value( 'strip_html_from_body', true ) ) {
214 $content = \wp_strip_all_tags( $content );
215 }
216
217 $max_length = $this->config->get_config_value( 'article_body_max_length', Article_Config::DEFAULT_MAX_ARTICLE_BODY_LENGTH );
218
219 return $this->trim_content_to_max_length( $max_length, $content );
220 } catch ( Exception $e ) {
221 return null;
222 }
223 }
224 }
225