PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.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
← All changes | src/integrations/front-end-integration.php +215 -42 18.428.5 View file →
@@ -1,18 +1,22 @@
1 1 <?php
2 2
3 3 namespace Yoast\WP\SEO\Integrations;
4 4
5 +use WP_HTML_Tag_Processor;
5 6 use WPSEO_Replace_Vars;
7 +use Yoast\WP\SEO\Conditionals\Dynamic_Product_Permalinks_Conditional;
6 8 use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
9 +use Yoast\WP\SEO\Conditionals\WooCommerce_Version_Conditional;
7 10 use Yoast\WP\SEO\Context\Meta_Tags_Context;
8 11 use Yoast\WP\SEO\Helpers\Options_Helper;
9 -use Yoast\WP\SEO\Helpers\Request_Helper;
12 +use Yoast\WP\SEO\Helpers\Permalink_Helper;
10 13 use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
11 14 use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter;
12 15 use Yoast\WP\SEO\Presenters\Debug\Marker_Close_Presenter;
13 16 use Yoast\WP\SEO\Presenters\Debug\Marker_Open_Presenter;
14 17 use Yoast\WP\SEO\Presenters\Title_Presenter;
18 +use Yoast\WP\SEO\Repositories\Indexable_Repository;
15 19 use Yoast\WP\SEO\Surfaces\Helpers_Surface;
16 20 use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;
17 21
18 22 /**
@@ -41,13 +45,13 @@
41 45 */
42 46 protected $options;
43 47
44 48 /**
45 - * Represents the request helper.
49 + * Represents the permalink helper.
46 50 *
47 - * @var Request_Helper
51 + * @var Permalink_Helper
48 52 */
49 - protected $request;
53 + protected $permalink_helper;
50 54
51 55 /**
52 56 * The helpers surface.
53 57 *
@@ -55,8 +59,15 @@
55 59 */
56 60 protected $helpers;
57 61
58 62 /**
63 + * The indexable repository.
64 + *
65 + * @var Indexable_Repository
66 + */
67 + protected $indexable_repository;
68 +
69 + /**
59 70 * The replace vars helper.
60 71 *
61 72 * @var WPSEO_Replace_Vars
62 73 */
@@ -100,14 +111,15 @@
100 111 'Open_Graph\Article_Author',
101 112 'Open_Graph\Article_Published_Time',
102 113 'Open_Graph\Article_Modified_Time',
103 114 'Open_Graph\Image',
115 + 'Meta_Author',
104 116 ];
105 117
106 118 /**
107 119 * The Open Graph specific presenters that should be output on error pages.
108 120 *
109 - * @var array
121 + * @var array<string>
110 122 */
111 123 protected $open_graph_error_presenters = [
112 124 'Open_Graph\Locale',
113 125 'Open_Graph\Title',
@@ -116,9 +128,9 @@
116 128
117 129 /**
118 130 * The Twitter card specific presenters.
119 131 *
120 - * @var string[]
132 + * @var array<string>
121 133 */
122 134 protected $twitter_card_presenters = [
123 135 'Twitter\Card',
124 136 'Twitter\Title',
@@ -130,9 +142,9 @@
130 142
131 143 /**
132 144 * The Slack specific presenters.
133 145 *
134 - * @var string[]
146 + * @var array<string>
135 147 */
136 148 protected $slack_presenters = [
137 149 'Slack\Enhanced_Data',
138 150 ];
@@ -139,11 +151,12 @@
139 151
140 152 /**
141 153 * The Webmaster verification specific presenters.
142 154 *
143 - * @var string[]
155 + * @var array<string>
144 156 */
145 157 protected $webmaster_verification_presenters = [
158 + 'Webmaster\Ahrefs',
146 159 'Webmaster\Baidu',
147 160 'Webmaster\Bing',
148 161 'Webmaster\Google',
149 162 'Webmaster\Pinterest',
@@ -152,11 +165,12 @@
152 165
153 166 /**
154 167 * Presenters that are only needed on singular pages.
155 168 *
156 - * @var string[]
169 + * @var array<string>
157 170 */
158 171 protected $singular_presenters = [
172 + 'Meta_Author',
159 173 'Open_Graph\Article_Author',
160 174 'Open_Graph\Article_Publisher',
161 175 'Open_Graph\Article_Published_Time',
162 176 'Open_Graph\Article_Modified_Time',
@@ -166,9 +180,9 @@
166 180
167 181 /**
168 182 * The presenters we want to be last in our output.
169 183 *
170 - * @var string[]
184 + * @var array<string>
171 185 */
172 186 protected $closing_presenters = [
173 187 'Schema',
174 188 ];
@@ -173,11 +187,25 @@
173 187 'Schema',
174 188 ];
175 189
176 190 /**
191 + * The next output.
192 + *
193 + * @var string
194 + */
195 + protected $next;
196 +
197 + /**
198 + * The prev output.
199 + *
200 + * @var string
201 + */
202 + protected $prev;
203 +
204 + /**
177 205 * Returns the conditionals based on which this loadable should be active.
178 206 *
179 - * @return array The conditionals.
207 + * @return array<string> The conditionals.
180 208 */
181 209 public static function get_conditionals() {
182 210 return [ Front_End_Conditional::class ];
183 211 }
@@ -186,29 +214,32 @@
186 214 * Front_End_Integration constructor.
187 215 *
188 216 * @codeCoverageIgnore It sets dependencies.
189 217 *
190 - * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
191 - * @param ContainerInterface $service_container The DI container.
192 - * @param Options_Helper $options The options helper.
193 - * @param Request_Helper $request The request helper.
194 - * @param Helpers_Surface $helpers The helpers surface.
195 - * @param WPSEO_Replace_Vars $replace_vars The replace vars helper.
218 + * @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
219 + * @param ContainerInterface $service_container The DI container.
220 + * @param Options_Helper $options The options helper.
221 + * @param Helpers_Surface $helpers The helpers surface.
222 + * @param WPSEO_Replace_Vars $replace_vars The replace vars helper.
223 + * @param Indexable_Repository $indexable_repository The indexable repository.
224 + * @param Permalink_Helper $permalink_helper The permalink helper.
196 225 */
197 226 public function __construct(
198 227 Meta_Tags_Context_Memoizer $context_memoizer,
199 228 ContainerInterface $service_container,
200 229 Options_Helper $options,
201 - Request_Helper $request,
202 230 Helpers_Surface $helpers,
203 - WPSEO_Replace_Vars $replace_vars
231 + WPSEO_Replace_Vars $replace_vars,
232 + Indexable_Repository $indexable_repository,
233 + Permalink_Helper $permalink_helper
204 234 ) {
205 - $this->container = $service_container;
206 - $this->context_memoizer = $context_memoizer;
207 - $this->options = $options;
208 - $this->request = $request;
209 - $this->helpers = $helpers;
210 - $this->replace_vars = $replace_vars;
235 + $this->container = $service_container;
236 + $this->context_memoizer = $context_memoizer;
237 + $this->options = $options;
238 + $this->helpers = $helpers;
239 + $this->replace_vars = $replace_vars;
240 + $this->indexable_repository = $indexable_repository;
241 + $this->permalink_helper = $permalink_helper;
211 242 }
212 243
213 244 /**
214 245 * Registers the appropriate hooks to show the SEO metadata on the frontend.
@@ -214,10 +245,14 @@
214 245 * Registers the appropriate hooks to show the SEO metadata on the frontend.
215 246 *
216 247 * Removes some actions to remove metadata that WordPress shows on the frontend,
217 248 * to avoid duplicate and/or mismatched metadata.
249 + *
250 + * @return void
218 251 */
219 252 public function register_hooks() {
253 + \add_filter( 'render_block', [ $this, 'query_loop_next_prev' ], 1, 2 );
254 +
220 255 \add_action( 'wp_head', [ $this, 'call_wpseo_head' ], 1 );
221 256 // Filter the title for compatibility with other plugins and themes.
222 257 \add_filter( 'wp_title', [ $this, 'filter_title' ], 15 );
223 258 // Filter the title for compatibility with block-based themes.
@@ -226,8 +261,9 @@
226 261 // Removes our robots presenter from the list when wp_robots is handling this.
227 262 \add_filter( 'wpseo_frontend_presenter_classes', [ $this, 'filter_robots_presenter' ] );
228 263
229 264 \add_action( 'wpseo_head', [ $this, 'present_head' ], -9999 );
265 + \add_action( 'wpseo_head', [ $this, 'update_outdated_permalink' ], -10_000 );
230 266
231 267 \remove_action( 'wp_head', 'rel_canonical' );
232 268 \remove_action( 'wp_head', 'index_rel_link' );
233 269 \remove_action( 'wp_head', 'start_post_rel_link' );
@@ -260,13 +296,134 @@
260 296 return $title;
261 297 }
262 298
263 299 /**
300 + * Checks if the current entity has a permalink that has a mismatch
301 + * with the permalink stored in its indexable. If they differ, purges the indexable's
302 + * permalink so it will be recalculated in the same request.
303 + *
304 + * @return void
305 + */
306 + public function update_outdated_permalink() {
307 + $dynamic_permalinks_conditional = new Dynamic_Product_Permalinks_Conditional();
308 + if ( ! $dynamic_permalinks_conditional->is_met() ) {
309 + return;
310 + }
311 +
312 + $woocommerce_version_conditional = new WooCommerce_Version_Conditional();
313 + if ( ! $woocommerce_version_conditional->is_met() ) {
314 + return;
315 + }
316 +
317 + $context = $this->context_memoizer->for_current_page();
318 +
319 + // We're adding this fix only for products because of the 10.5 Woo release. We might expand this for all cases in the future.
320 + if ( $context->indexable->object_sub_type !== 'product' ) {
321 + return;
322 + }
323 +
324 + $current_permalink = $this->permalink_helper->get_permalink_for_post( $context->indexable->object_sub_type, $context->indexable->object_id );
325 + $indexable_permalink = $context->indexable->permalink;
326 +
327 + // Only purge if the permalinks differ.
328 + if ( $current_permalink !== $indexable_permalink ) {
329 + $this->indexable_repository->reset_permalink(
330 + $context->indexable->object_type,
331 + $context->indexable->object_sub_type,
332 + $context->indexable->object_id,
333 + );
334 +
335 + // Clear the memoizer caches so present_head() sees the updated indexable.
336 + $this->context_memoizer->clear_for_current_page();
337 + $this->context_memoizer->clear( $context->indexable );
338 + }
339 + }
340 +
341 + /**
342 + * Filters the next and prev links in the query loop block.
343 + *
344 + * @param string $html The HTML output.
345 + * @param array<string|array|null> $block The block.
346 + * @return string The filtered HTML output.
347 + */
348 + public function query_loop_next_prev( $html, $block ) {
349 + if ( $block['blockName'] === 'core/query' ) {
350 + // Check that the query does not inherit the main query.
351 + if ( isset( $block['attrs']['query']['inherit'] ) && ! $block['attrs']['query']['inherit'] ) {
352 + \add_filter( 'wpseo_adjacent_rel_url', [ $this, 'adjacent_rel_url' ], 1, 3 );
353 + }
354 + }
355 +
356 + if ( $block['blockName'] === 'core/query-pagination-next' ) {
357 + $this->next = $html;
358 + }
359 +
360 + if ( $block['blockName'] === 'core/query-pagination-previous' ) {
361 + $this->prev = $html;
362 + }
363 +
364 + return $html;
365 + }
366 +
367 + /**
368 + * Returns correct adjacent pages when Query loop block does not inherit query from template.
369 + * Prioritizes existing prev and next links.
370 + * Includes a safety check for full urls though it is not expected in the query pagination block.
371 + *
372 + * @param string $link The current link.
373 + * @param string $rel Link relationship, prev or next.
374 + * @param Indexable_Presentation|null $presentation The indexable presentation.
375 + *
376 + * @return string The correct link.
377 + */
378 + public function adjacent_rel_url( $link, $rel, $presentation = null ) {
379 + // Prioritize existing prev and next links.
380 + if ( $link ) {
381 + return $link;
382 + }
383 +
384 + // Safety check for rel value.
385 + if ( $rel !== 'next' && $rel !== 'prev' ) {
386 + return $link;
387 + }
388 +
389 + // Check $this->next or $this->prev for existing links.
390 + if ( $this->$rel === null ) {
391 + return $link;
392 + }
393 +
394 + $processor = new WP_HTML_Tag_Processor( $this->$rel );
395 +
396 + if ( ! $processor->next_tag( [ 'tag_name' => 'a' ] ) ) {
397 + return $link;
398 + }
399 +
400 + $href = $processor->get_attribute( 'href' );
401 +
402 + if ( ! $href ) {
403 + return $link;
404 + }
405 +
406 + // Safety check for full url, not expected.
407 + if ( \strpos( $href, 'http' ) === 0 ) {
408 + return $href;
409 + }
410 +
411 + // Check if $href is relative and append last part of the url to permalink.
412 + if ( \strpos( $href, '/' ) === 0 ) {
413 + $href_parts = \explode( '/', $href );
414 + return $presentation->permalink . \end( $href_parts );
415 + }
416 +
417 + return $link;
418 + }
419 +
420 + /**
264 421 * Filters our robots presenter, but only when wp_robots is attached to the wp_head action.
265 422 *
266 - * @param array $presenters The presenters for current page.
423 + * @param array<string> $presenters The presenters for current page.
267 424 *
268 - * @return array The filtered presenters.
425 + * @return array<string> The filtered presenters.
269 426 */
270 427 public function filter_robots_presenter( $presenters ) {
271 428 if ( ! \function_exists( 'wp_robots' ) ) {
272 429 return $presenters;
@@ -275,9 +432,9 @@
275 432 if ( ! \has_action( 'wp_head', 'wp_robots' ) ) {
276 433 return $presenters;
277 434 }
278 435
279 - if ( $this->request->is_rest_request() ) {
436 + if ( \wp_is_serving_rest_request() ) {
280 437 return $presenters;
281 438 }
282 439
283 440 return \array_diff( $presenters, [ 'Yoast\\WP\\SEO\\Presenters\\Robots_Presenter' ] );
@@ -286,8 +443,10 @@
286 443 /**
287 444 * Presents the head in the front-end. Resets wp_query if it's not the main query.
288 445 *
289 446 * @codeCoverageIgnore It just calls a WordPress function.
447 + *
448 + * @return void
290 449 */
291 450 public function call_wpseo_head() {
292 451 global $wp_query;
293 452
@@ -302,8 +461,10 @@
302 461 }
303 462
304 463 /**
305 464 * Echoes all applicable presenters for a page.
465 + *
466 + * @return void
306 467 */
307 468 public function present_head() {
308 469 $context = $this->context_memoizer->for_current_page();
309 470 $presenters = $this->get_presenters( $context->page_type, $context );
@@ -310,9 +471,10 @@
310 471
311 472 /**
312 473 * Filter 'wpseo_frontend_presentation' - Allow filtering the presentation used to output our meta values.
313 474 *
314 - * @api Indexable_Presention The indexable presentation.
475 + * @param Indexable_Presentation $presentation The indexable presentation.
476 + * @param Meta_Tags_Context $context The meta tags context for the current page.
315 477 */
316 478 $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );
317 479
318 480 echo \PHP_EOL;
@@ -338,15 +500,13 @@
338 500 *
339 501 * @return Abstract_Indexable_Presenter[] The presenters.
340 502 */
341 503 public function get_presenters( $page_type, $context = null ) {
342 - if ( \is_null( $context ) ) {
343 - $context = $this->context_memoizer->for_current_page();
344 - }
504 + $context ??= $this->context_memoizer->for_current_page();
345 505
346 506 $needed_presenters = $this->get_needed_presenters( $page_type );
347 507
348 - $callback = static function( $presenter ) {
508 + $callback = static function ( $presenter ) {
349 509 if ( ! \class_exists( $presenter ) ) {
350 510 return null;
351 511 }
352 512 return new $presenter();
@@ -355,12 +515,10 @@
355 515
356 516 /**
357 517 * Filter 'wpseo_frontend_presenters' - Allow filtering the presenter instances in or out of the request.
358 518 *
359 - * @param array $presenters The presenters.
360 - * @param Meta_Tags_Context $context The meta tags context for the current page.
361 - *
362 - * @api Abstract_Indexable_Presenter[] List of presenter instances.
519 + * @param Abstract_Indexable_Presenter[] $presenters List of presenter instances.
520 + * @param Meta_Tags_Context $context The meta tags context for the current page.
363 521 */
364 522 $presenter_instances = \apply_filters( 'wpseo_frontend_presenters', $presenters, $context );
365 523
366 524 if ( ! \is_array( $presenter_instances ) ) {
@@ -374,9 +532,9 @@
374 532
375 533 return \array_merge(
376 534 [ new Marker_Open_Presenter() ],
377 535 $presenter_instances,
378 - [ new Marker_Close_Presenter() ]
536 + [ new Marker_Close_Presenter() ],
379 537 );
380 538 }
381 539
382 540 /**
@@ -398,11 +556,12 @@
398 556
399 557 /**
400 558 * Filter 'wpseo_frontend_presenter_classes' - Allow filtering presenters in or out of the request.
401 559 *
402 - * @api array List of presenters.
560 + * @param array $presenters List of presenters.
561 + * @param string $page_type The current page type.
403 562 */
404 - $presenters = \apply_filters( 'wpseo_frontend_presenter_classes', $presenters );
563 + $presenters = \apply_filters( 'wpseo_frontend_presenter_classes', $presenters, $page_type );
405 564
406 565 return $presenters;
407 566 }
408 567
@@ -431,8 +590,13 @@
431 590 if ( ! \in_array( $page_type, [ 'Post_Type', 'Static_Home_Page' ], true ) ) {
432 591 $presenters = \array_diff( $presenters, $this->singular_presenters );
433 592 }
434 593
594 + // Filter out `twitter:data` presenters for static home pages.
595 + if ( $page_type === 'Static_Home_Page' ) {
596 + $presenters = \array_diff( $presenters, $this->slack_presenters );
597 + }
598 +
435 599 return $presenters;
436 600 }
437 601
438 602 /**
@@ -455,8 +619,17 @@
455 619 return \array_merge( $presenters, $this->closing_presenters );
456 620 }
457 621
458 622 /**
623 + * Whether the title presenter should be removed.
624 + *
625 + * @return bool True when the title presenter should be removed, false otherwise.
626 + */
627 + public function should_title_presenter_be_removed() {
628 + return ! \get_theme_support( 'title-tag' ) && ! $this->options->get( 'forcerewritetitle', false );
629 + }
630 +
631 + /**
459 632 * Checks if the Title presenter needs to be removed.
460 633 *
461 634 * @param string[] $presenters The presenters.
462 635 *
@@ -463,14 +636,14 @@
463 636 * @return string[] The presenters.
464 637 */
465 638 private function maybe_remove_title_presenter( $presenters ) {
466 639 // Do not remove the title if we're on a REST request.
467 - if ( $this->request->is_rest_request() ) {
640 + if ( \wp_is_serving_rest_request() ) {
468 641 return $presenters;
469 642 }
470 643
471 644 // Remove the title presenter if the theme is hardcoded to output a title tag so we don't have two title tags.
472 - if ( ! \get_theme_support( 'title-tag' ) && ! $this->options->get( 'forcerewritetitle', false ) ) {
645 + if ( $this->should_title_presenter_be_removed() ) {
473 646 $presenters = \array_diff( $presenters, [ 'Title' ] );
474 647 }
475 648
476 649 return $presenters;