PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.5
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 / integrations / blocks / structured-data-blocks.php

structured-data-blocks.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 18.5, at src/integrations/blocks/structured-data-blocks.php

370 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\Integrations\Blocks;
4
5 use WPSEO_Admin_Asset_Manager;
6 use Yoast\WP\SEO\Conditionals\No_Conditionals;
7 use Yoast\WP\SEO\Helpers\Image_Helper;
8 use Yoast\WP\SEO\Integrations\Integration_Interface;
9
10 /**
11 * Class to load assets required for structured data blocks.
12 */
13 class Structured_Data_Blocks implements Integration_Interface {
14
15 use No_Conditionals;
16
17 /**
18 * An instance of the WPSEO_Admin_Asset_Manager class.
19 *
20 * @var WPSEO_Admin_Asset_Manager
21 */
22 protected $asset_manager;
23
24 /**
25 * An instance of the image helper class.
26 *
27 * @var Image_Helper
28 */
29 protected $image_helper;
30
31 /**
32 * The image caches per post.
33 *
34 * @var array
35 */
36 protected $caches = [];
37
38 /**
39 * The used cache keys per post.
40 *
41 * @var array
42 */
43 protected $used_caches = [];
44
45 /**
46 * Whether or not we've registered our shutdown function.
47 *
48 * @var bool
49 */
50 protected $registered_shutdown_function = false;
51
52 /**
53 * Structured_Data_Blocks constructor.
54 *
55 * @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager.
56 * @param Image_Helper $image_helper The image helper.
57 */
58 public function __construct(
59 WPSEO_Admin_Asset_Manager $asset_manager,
60 Image_Helper $image_helper
61 ) {
62 $this->asset_manager = $asset_manager;
63 $this->image_helper = $image_helper;
64 }
65
66 /**
67 * Registers hooks for Structured Data Blocks with WordPress.
68 */
69 public function register_hooks() {
70 \add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_assets' ] );
71 $this->register_blocks();
72 }
73
74 /**
75 * Registers the blocks.
76 *
77 * @return void
78 */
79 public function register_blocks() {
80 \register_block_type(
81 'yoast/faq-block',
82 [
83 'render_callback' => [ $this, 'optimize_faq_images' ],
84 'attributes' => [
85 'className' => [
86 'default' => '',
87 'type' => 'string',
88 ],
89 'questions' => [
90 'type' => 'array',
91 ],
92 'additionalListCssClasses' => [
93 'type' => 'string',
94 ],
95 ],
96 ]
97 );
98 \register_block_type(
99 'yoast/how-to-block',
100 [
101 'render_callback' => [ $this, 'optimize_how_to_images' ],
102 'attributes' => [
103 'hasDuration' => [
104 'type' => 'boolean',
105 ],
106 'days' => [
107 'type' => 'string',
108 ],
109 'hours' => [
110 'type' => 'string',
111 ],
112 'minutes' => [
113 'type' => 'string',
114 ],
115 'description' => [
116 'type' => 'array',
117 'source' => 'children',
118 'selector' => '.schema-how-to-description',
119 ],
120 'jsonDescription' => [
121 'type' => 'string',
122 ],
123 'steps' => [
124 'type' => 'array',
125 ],
126 'additionalListCssClasses' => [
127 'type' => 'string',
128 ],
129 'unorderedList' => [
130 'type' => 'boolean',
131 ],
132 'durationText' => [
133 'type' => 'string',
134 ],
135 'defaultDurationText' => [
136 'type' => 'string',
137 ],
138 ],
139 ]
140 );
141 }
142
143 /**
144 * Enqueue Gutenberg block assets for backend editor.
145 */
146 public function enqueue_block_editor_assets() {
147 /**
148 * Filter: 'wpseo_enable_structured_data_blocks' - Allows disabling Yoast's schema blocks entirely.
149 *
150 * @api bool If false, our structured data blocks won't show.
151 */
152 if ( ! \apply_filters( 'wpseo_enable_structured_data_blocks', true ) ) {
153 return;
154 }
155
156 $this->asset_manager->enqueue_script( 'structured-data-blocks' );
157 $this->asset_manager->enqueue_style( 'structured-data-blocks' );
158 }
159
160 /**
161 * Optimizes images in the FAQ blocks.
162 *
163 * @param array $attributes The attributes.
164 * @param string $content The content.
165 *
166 * @return string The content with images optimized.
167 */
168 public function optimize_faq_images( $attributes, $content ) {
169 if ( ! isset( $attributes['questions'] ) ) {
170 return $content;
171 }
172
173 return $this->optimize_images( $attributes['questions'], 'answer', $content );
174 }
175
176 /**
177 * Optimizes images in the How-To blocks.
178 *
179 * @param array $attributes The attributes.
180 * @param string $content The content.
181 *
182 * @return string The content with images optimized.
183 */
184 public function optimize_how_to_images( $attributes, $content ) {
185 if ( ! isset( $attributes['steps'] ) ) {
186 return $content;
187 }
188
189 return $this->optimize_images( $attributes['steps'], 'text', $content );
190 }
191
192 /**
193 * Optimizes images in structured data blocks.
194 *
195 * @param array $elements The list of elements from the block attributes.
196 * @param string $key The key in the data to iterate over.
197 * @param string $content The content.
198 *
199 * @return string The content with images optimized.
200 */
201 private function optimize_images( $elements, $key, $content ) {
202 global $post;
203 if ( ! $post ) {
204 return $content;
205 }
206
207 $this->add_images_from_attributes_to_used_cache( $post->ID, $elements, $key );
208
209 // Then replace all images with optimized versions in the content.
210 $content = \preg_replace_callback(
211 '/<img[^>]+>/',
212 function ( $matches ) {
213 \preg_match( '/src="([^"]+)"/', $matches[0], $src_matches );
214 if ( ! $src_matches || ! isset( $src_matches[1] ) ) {
215 return $matches[0];
216 }
217 $attachment_id = $this->attachment_src_to_id( $src_matches[1] );
218 if ( $attachment_id === 0 ) {
219 return $matches[0];
220 }
221 $image_size = 'full';
222 $image_style = [ 'style' => 'max-width: 100%; height: auto;' ];
223 \preg_match( '/style="[^"]*width:\s*(\d+)px[^"]*"/', $matches[0], $style_matches );
224 if ( $style_matches && isset( $style_matches[1] ) ) {
225 $width = (int) $style_matches[1];
226 $meta_data = \wp_get_attachment_metadata( $attachment_id );
227 if ( isset( $meta_data['height'] ) && isset( $meta_data['width'] ) && $meta_data['height'] > 0 && $meta_data['width'] > 0 ) {
228 $aspect_ratio = ( $meta_data['height'] / $meta_data['width'] );
229 $height = ( $width * $aspect_ratio );
230 $image_size = [ $width, $height ];
231 }
232 $image_style = '';
233 }
234
235 /**
236 * Filter: 'wpseo_structured_data_blocks_image_size' - Allows adjusting the image size in structured data blocks.
237 *
238 * @since 18.2
239 *
240 * @param string|int[] $image_size The image size. Accepts any registered image size name, or an array of width and height values in pixels (in that order).
241 * @param int $attachment_id The id of the attachment.
242 * @param string $attachment_src The attachment src.
243 */
244 $image_size = \apply_filters(
245 'wpseo_structured_data_blocks_image_size',
246 $image_size,
247 $attachment_id,
248 $src_matches[1]
249 );
250 $image_html = \wp_get_attachment_image(
251 $attachment_id,
252 $image_size,
253 false,
254 $image_style
255 );
256
257 if ( empty( $image_html ) ) {
258 return $matches[0];
259 }
260
261 return $image_html;
262 },
263 $content
264 );
265
266 if ( ! $this->registered_shutdown_function ) {
267 \register_shutdown_function( [ $this, 'maybe_save_used_caches' ] );
268 $this->registered_shutdown_function = true;
269 }
270
271 return $content;
272 }
273
274 /**
275 * If the caches of structured data block images have been changed, saves them.
276 *
277 * @return void
278 */
279 public function maybe_save_used_caches() {
280 foreach ( $this->used_caches as $post_id => $used_cache ) {
281 if ( isset( $this->caches[ $post_id ] ) && $used_cache === $this->caches[ $post_id ] ) {
282 continue;
283 }
284 \update_post_meta( $post_id, 'yoast-structured-data-blocks-images-cache', $used_cache );
285 }
286 }
287
288 /**
289 * Converts an attachment src to an attachment ID.
290 *
291 * @param string $src The attachment src.
292 *
293 * @return int The attachment ID. 0 if none was found.
294 */
295 private function attachment_src_to_id( $src ) {
296 global $post;
297
298 if ( isset( $this->used_caches[ $post->ID ][ $src ] ) ) {
299 return $this->used_caches[ $post->ID ][ $src ];
300 }
301
302 $cache = $this->get_cache_for_post( $post->ID );
303 if ( isset( $cache[ $src ] ) ) {
304 $this->used_caches[ $post->ID ][ $src ] = $cache[ $src ];
305 return $cache[ $src ];
306 }
307
308 $this->used_caches[ $post->ID ][ $src ] = $this->image_helper->get_attachment_by_url( $src );
309 return $this->used_caches[ $post->ID ][ $src ];
310 }
311
312 /**
313 * Returns the cache from postmeta for a given post.
314 *
315 * @param int $post_id The post ID.
316 *
317 * @return array The images cache.
318 */
319 private function get_cache_for_post( $post_id ) {
320 if ( isset( $this->caches[ $post_id ] ) ) {
321 return $this->caches[ $post_id ];
322 }
323
324 $cache = \get_post_meta( $post_id, 'yoast-structured-data-blocks-images-cache', true );
325 if ( ! $cache ) {
326 $cache = [];
327 }
328
329 $this->caches[ $post_id ] = $cache;
330 return $cache;
331 }
332
333 /**
334 * Adds any images that have their ID in the block attributes to the cache.
335 *
336 * @param int $post_id The post ID.
337 * @param array $elements The elements.
338 * @param string $key The key in the elements we should loop over.
339 *
340 * @return void
341 */
342 private function add_images_from_attributes_to_used_cache( $post_id, $elements, $key ) {
343 // First grab all image IDs from the attributes.
344 $images = [];
345 foreach ( $elements as $element ) {
346 if ( ! isset( $element[ $key ] ) ) {
347 continue;
348 }
349 foreach ( $element[ $key ] as $part ) {
350 if ( ! \is_array( $part ) || ! isset( $part['type'] ) || $part['type'] !== 'img' ) {
351 continue;
352 }
353
354 if ( ! isset( $part['key'] ) || ! isset( $part['props']['src'] ) ) {
355 continue;
356 }
357
358 $images[ $part['props']['src'] ] = (int) $part['key'];
359 }
360 }
361
362 if ( isset( $this->used_caches[ $post_id ] ) ) {
363 $this->used_caches[ $post_id ] = \array_merge( $this->used_caches[ $post_id ], $images );
364 }
365 else {
366 $this->used_caches[ $post_id ] = $images;
367 }
368 }
369 }
370