• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 8ad612e0d07e81871206f56a097e212b74325549

07 Oct 2025 07:19AM UTC coverage: 53.67% (+0.9%) from 52.782%
8ad612e0d07e81871206f56a097e212b74325549

Pull #22245

github

web-flow
Merge 9c9c87348 into b095d7ef1
Pull Request #22245: Remove the translation loading.

7852 of 13939 branches covered (56.33%)

Branch coverage included in aggregate %.

30868 of 58205 relevant lines covered (53.03%)

41005.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.06
/src/integrations/front-end-integration.php
1
<?php
2

3
namespace Yoast\WP\SEO\Integrations;
4

5
use WP_HTML_Tag_Processor;
6
use WPSEO_Replace_Vars;
7
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
8
use Yoast\WP\SEO\Context\Meta_Tags_Context;
9
use Yoast\WP\SEO\Helpers\Options_Helper;
10
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
11
use Yoast\WP\SEO\Presenters\Abstract_Indexable_Presenter;
12
use Yoast\WP\SEO\Presenters\Debug\Marker_Close_Presenter;
13
use Yoast\WP\SEO\Presenters\Debug\Marker_Open_Presenter;
14
use Yoast\WP\SEO\Presenters\Title_Presenter;
15
use Yoast\WP\SEO\Surfaces\Helpers_Surface;
16
use YoastSEO_Vendor\Symfony\Component\DependencyInjection\ContainerInterface;
17

18
/**
19
 * Class Front_End_Integration.
20
 */
21
class Front_End_Integration implements Integration_Interface {
22

23
        /**
24
         * The memoizer for the meta tags context.
25
         *
26
         * @var Meta_Tags_Context_Memoizer
27
         */
28
        private $context_memoizer;
29

30
        /**
31
         * The container.
32
         *
33
         * @var ContainerInterface
34
         */
35
        protected $container;
36

37
        /**
38
         * Represents the options helper.
39
         *
40
         * @var Options_Helper
41
         */
42
        protected $options;
43

44
        /**
45
         * The helpers surface.
46
         *
47
         * @var Helpers_Surface
48
         */
49
        protected $helpers;
50

51
        /**
52
         * The replace vars helper.
53
         *
54
         * @var WPSEO_Replace_Vars
55
         */
56
        protected $replace_vars;
57

58
        /**
59
         * The presenters we loop through on each page load.
60
         *
61
         * @var string[]
62
         */
63
        protected $base_presenters = [
64
                'Title',
65
                'Meta_Description',
66
                'Robots',
67
        ];
68

69
        /**
70
         * The presenters we loop through on each page load.
71
         *
72
         * @var string[]
73
         */
74
        protected $indexing_directive_presenters = [
75
                'Canonical',
76
                'Rel_Prev',
77
                'Rel_Next',
78
        ];
79

80
        /**
81
         * The Open Graph specific presenters.
82
         *
83
         * @var string[]
84
         */
85
        protected $open_graph_presenters = [
86
                'Open_Graph\Locale',
87
                'Open_Graph\Type',
88
                'Open_Graph\Title',
89
                'Open_Graph\Description',
90
                'Open_Graph\Url',
91
                'Open_Graph\Site_Name',
92
                'Open_Graph\Article_Publisher',
93
                'Open_Graph\Article_Author',
94
                'Open_Graph\Article_Published_Time',
95
                'Open_Graph\Article_Modified_Time',
96
                'Open_Graph\Image',
97
                'Meta_Author',
98
        ];
99

100
        /**
101
         * The Open Graph specific presenters that should be output on error pages.
102
         *
103
         * @var array<string>
104
         */
105
        protected $open_graph_error_presenters = [
106
                'Open_Graph\Locale',
107
                'Open_Graph\Title',
108
                'Open_Graph\Site_Name',
109
        ];
110

111
        /**
112
         * The Twitter card specific presenters.
113
         *
114
         * @var array<string>
115
         */
116
        protected $twitter_card_presenters = [
117
                'Twitter\Card',
118
                'Twitter\Title',
119
                'Twitter\Description',
120
                'Twitter\Image',
121
                'Twitter\Creator',
122
                'Twitter\Site',
123
        ];
124

125
        /**
126
         * The Slack specific presenters.
127
         *
128
         * @var array<string>
129
         */
130
        protected $slack_presenters = [
131
                'Slack\Enhanced_Data',
132
        ];
133

134
        /**
135
         * The Webmaster verification specific presenters.
136
         *
137
         * @var array<string>
138
         */
139
        protected $webmaster_verification_presenters = [
140
                'Webmaster\Ahrefs',
141
                'Webmaster\Baidu',
142
                'Webmaster\Bing',
143
                'Webmaster\Google',
144
                'Webmaster\Pinterest',
145
                'Webmaster\Yandex',
146
        ];
147

148
        /**
149
         * Presenters that are only needed on singular pages.
150
         *
151
         * @var array<string>
152
         */
153
        protected $singular_presenters = [
154
                'Meta_Author',
155
                'Open_Graph\Article_Author',
156
                'Open_Graph\Article_Publisher',
157
                'Open_Graph\Article_Published_Time',
158
                'Open_Graph\Article_Modified_Time',
159
                'Twitter\Creator',
160
                'Slack\Enhanced_Data',
161
        ];
162

163
        /**
164
         * The presenters we want to be last in our output.
165
         *
166
         * @var array<string>
167
         */
168
        protected $closing_presenters = [
169
                'Schema',
170
        ];
171

172
        /**
173
         * The next output.
174
         *
175
         * @var string
176
         */
177
        protected $next;
178

179
        /**
180
         * The prev output.
181
         *
182
         * @var string
183
         */
184
        protected $prev;
185

186
        /**
187
         * Returns the conditionals based on which this loadable should be active.
188
         *
189
         * @return array<string> The conditionals.
190
         */
191
        public static function get_conditionals() {
2✔
192
                return [ Front_End_Conditional::class ];
2✔
193
        }
194

195
        /**
196
         * Front_End_Integration constructor.
197
         *
198
         * @codeCoverageIgnore It sets dependencies.
199
         *
200
         * @param Meta_Tags_Context_Memoizer $context_memoizer  The meta tags context memoizer.
201
         * @param ContainerInterface         $service_container The DI container.
202
         * @param Options_Helper             $options           The options helper.
203
         * @param Helpers_Surface            $helpers           The helpers surface.
204
         * @param WPSEO_Replace_Vars         $replace_vars      The replace vars helper.
205
         */
206
        public function __construct(
207
                Meta_Tags_Context_Memoizer $context_memoizer,
208
                ContainerInterface $service_container,
209
                Options_Helper $options,
210
                Helpers_Surface $helpers,
211
                WPSEO_Replace_Vars $replace_vars
212
        ) {
213
                $this->container        = $service_container;
214
                $this->context_memoizer = $context_memoizer;
215
                $this->options          = $options;
216
                $this->helpers          = $helpers;
217
                $this->replace_vars     = $replace_vars;
218
        }
219

220
        /**
221
         * Registers the appropriate hooks to show the SEO metadata on the frontend.
222
         *
223
         * Removes some actions to remove metadata that WordPress shows on the frontend,
224
         * to avoid duplicate and/or mismatched metadata.
225
         *
226
         * @return void
227
         */
228
        public function register_hooks() {
2✔
229
                \add_filter( 'render_block', [ $this, 'query_loop_next_prev' ], 1, 2 );
2✔
230

231
                \add_action( 'wp_head', [ $this, 'call_wpseo_head' ], 1 );
2✔
232
                // Filter the title for compatibility with other plugins and themes.
233
                \add_filter( 'wp_title', [ $this, 'filter_title' ], 15 );
2✔
234
                // Filter the title for compatibility with block-based themes.
235
                \add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );
2✔
236

237
                // Removes our robots presenter from the list when wp_robots is handling this.
238
                \add_filter( 'wpseo_frontend_presenter_classes', [ $this, 'filter_robots_presenter' ] );
2✔
239

240
                \add_action( 'wpseo_head', [ $this, 'present_head' ], -9999 );
2✔
241

242
                \remove_action( 'wp_head', 'rel_canonical' );
2✔
243
                \remove_action( 'wp_head', 'index_rel_link' );
2✔
244
                \remove_action( 'wp_head', 'start_post_rel_link' );
2✔
245
                \remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head' );
2✔
246
                \remove_action( 'wp_head', 'noindex', 1 );
2✔
247
                \remove_action( 'wp_head', '_wp_render_title_tag', 1 );
2✔
248
                \remove_action( 'wp_head', '_block_template_render_title_tag', 1 );
2✔
249
                \remove_action( 'wp_head', 'gutenberg_render_title_tag', 1 );
2✔
250
        }
251

252
        /**
253
         * Filters the title, mainly used for compatibility reasons.
254
         *
255
         * @return string
256
         */
257
        public function filter_title() {
×
258
                $context = $this->context_memoizer->for_current_page();
×
259

260
                $title_presenter = new Title_Presenter();
×
261

262
                /** This filter is documented in src/integrations/front-end-integration.php */
263
                $title_presenter->presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );
×
264
                $title_presenter->replace_vars = $this->replace_vars;
×
265
                $title_presenter->helpers      = $this->helpers;
×
266

267
                \remove_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );
×
268
                $title = \esc_html( $title_presenter->get() );
×
269
                \add_filter( 'pre_get_document_title', [ $this, 'filter_title' ], 15 );
×
270

271
                return $title;
×
272
        }
273

274
        /**
275
         * Filters the next and prev links in the query loop block.
276
         *
277
         * @param string                   $html  The HTML output.
278
         * @param array<string|array|null> $block The block.
279
         * @return string The filtered HTML output.
280
         */
281
        public function query_loop_next_prev( $html, $block ) {
6✔
282
                if ( $block['blockName'] === 'core/query' ) {
6✔
283
                        // Check that the query does not inherit the main query.
284
                        if ( isset( $block['attrs']['query']['inherit'] ) && ! $block['attrs']['query']['inherit'] ) {
2✔
285
                                \add_filter( 'wpseo_adjacent_rel_url', [ $this, 'adjacent_rel_url' ], 1, 3 );
2✔
286
                        }
287
                }
288

289
                if ( $block['blockName'] === 'core/query-pagination-next' ) {
6✔
290
                        $this->next = $html;
2✔
291
                }
292

293
                if ( $block['blockName'] === 'core/query-pagination-previous' ) {
6✔
294
                        $this->prev = $html;
2✔
295
                }
296

297
                return $html;
6✔
298
        }
299

300
        /**
301
         * Returns correct adjacent pages when Query loop block does not inherit query from template.
302
         * Prioritizes existing prev and next links.
303
         * Includes a safety check for full urls though it is not expected in the query pagination block.
304
         *
305
         * @param string                      $link         The current link.
306
         * @param string                      $rel          Link relationship, prev or next.
307
         * @param Indexable_Presentation|null $presentation The indexable presentation.
308
         *
309
         * @return string The correct link.
310
         */
311
        public function adjacent_rel_url( $link, $rel, $presentation = null ) {
24✔
312
                // Prioritize existing prev and next links.
313
                if ( $link ) {
24✔
314
                        return $link;
4✔
315
                }
316

317
                // Safety check for rel value.
318
                if ( $rel !== 'next' && $rel !== 'prev' ) {
20✔
319
                        return $link;
4✔
320
                }
321

322
                // Check $this->next or $this->prev for existing links.
323
                if ( $this->$rel === null ) {
16✔
324
                        return $link;
4✔
325
                }
326

327
                $processor = new WP_HTML_Tag_Processor( $this->$rel );
12✔
328

329
                if ( ! $processor->next_tag( [ 'tag_name' => 'a' ] ) ) {
12✔
330
                        return $link;
×
331
                }
332

333
                $href = $processor->get_attribute( 'href' );
12✔
334

335
                if ( ! $href ) {
12✔
336
                        return $link;
×
337
                }
338

339
                // Safety check for full url, not expected.
340
                if ( \strpos( $href, 'http' ) === 0 ) {
12✔
341
                        return $href;
4✔
342
                }
343

344
                // Check if $href is relative and append last part of the url to permalink.
345
                if ( \strpos( $href, '/' ) === 0 ) {
8✔
346
                        $href_parts = \explode( '/', $href );
8✔
347
                        return $presentation->permalink . \end( $href_parts );
8✔
348
                }
349

350
                return $link;
×
351
        }
352

353
        /**
354
         * Filters our robots presenter, but only when wp_robots is attached to the wp_head action.
355
         *
356
         * @param array<string> $presenters The presenters for current page.
357
         *
358
         * @return array<string> The filtered presenters.
359
         */
360
        public function filter_robots_presenter( $presenters ) {
8✔
361
                if ( ! \function_exists( 'wp_robots' ) ) {
8✔
362
                        return $presenters;
×
363
                }
364

365
                if ( ! \has_action( 'wp_head', 'wp_robots' ) ) {
8✔
366
                        return $presenters;
4✔
367
                }
368

369
                if ( \wp_is_serving_rest_request() ) {
4✔
370
                        return $presenters;
2✔
371
                }
372

373
                return \array_diff( $presenters, [ 'Yoast\\WP\\SEO\\Presenters\\Robots_Presenter' ] );
2✔
374
        }
375

376
        /**
377
         * Presents the head in the front-end. Resets wp_query if it's not the main query.
378
         *
379
         * @codeCoverageIgnore It just calls a WordPress function.
380
         *
381
         * @return void
382
         */
383
        public function call_wpseo_head() {
384
                global $wp_query;
385

386
                $old_wp_query = $wp_query;
387
                // phpcs:ignore WordPress.WP.DiscouragedFunctions.wp_reset_query_wp_reset_query -- Reason: The recommended function, wp_reset_postdata, doesn't reset wp_query.
388
                \wp_reset_query();
389

390
                \do_action( 'wpseo_head' );
391

392
                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Reason: we have to restore the query.
393
                $GLOBALS['wp_query'] = $old_wp_query;
394
        }
395

396
        /**
397
         * Echoes all applicable presenters for a page.
398
         *
399
         * @return void
400
         */
401
        public function present_head() {
2✔
402
                $context    = $this->context_memoizer->for_current_page();
2✔
403
                $presenters = $this->get_presenters( $context->page_type, $context );
2✔
404

405
                /**
406
                 * Filter 'wpseo_frontend_presentation' - Allow filtering the presentation used to output our meta values.
407
                 *
408
                 * @param Indexable_Presention $presentation The indexable presentation.
409
                 * @param Meta_Tags_Context    $context      The meta tags context for the current page.
410
                 */
411
                $presentation = \apply_filters( 'wpseo_frontend_presentation', $context->presentation, $context );
2✔
412

413
                echo \PHP_EOL;
2✔
414
                foreach ( $presenters as $presenter ) {
2✔
415
                        $presenter->presentation = $presentation;
2✔
416
                        $presenter->helpers      = $this->helpers;
2✔
417
                        $presenter->replace_vars = $this->replace_vars;
2✔
418

419
                        $output = $presenter->present();
2✔
420
                        if ( ! empty( $output ) ) {
2✔
421
                                // phpcs:ignore WordPress.Security.EscapeOutput -- Presenters are responsible for correctly escaping their output.
422
                                echo "\t" . $output . \PHP_EOL;
2✔
423
                        }
424
                }
425
                echo \PHP_EOL . \PHP_EOL;
2✔
426
        }
427

428
        /**
429
         * Returns all presenters for this page.
430
         *
431
         * @param string                 $page_type The page type.
432
         * @param Meta_Tags_Context|null $context   The meta tags context for the current page.
433
         *
434
         * @return Abstract_Indexable_Presenter[] The presenters.
435
         */
436
        public function get_presenters( $page_type, $context = null ) {
14✔
437
                if ( $context === null ) {
14✔
438
                        $context = $this->context_memoizer->for_current_page();
14✔
439
                }
440

441
                $needed_presenters = $this->get_needed_presenters( $page_type );
14✔
442

443
                $callback   = static function ( $presenter ) {
14✔
444
                        if ( ! \class_exists( $presenter ) ) {
14✔
445
                                return null;
×
446
                        }
447
                        return new $presenter();
14✔
448
                };
14✔
449
                $presenters = \array_filter( \array_map( $callback, $needed_presenters ) );
14✔
450

451
                /**
452
                 * Filter 'wpseo_frontend_presenters' - Allow filtering the presenter instances in or out of the request.
453
                 *
454
                 * @param Abstract_Indexable_Presenter[] $presenters List of presenter instances.
455
                 * @param Meta_Tags_Context              $context    The meta tags context for the current page.
456
                 */
457
                $presenter_instances = \apply_filters( 'wpseo_frontend_presenters', $presenters, $context );
14✔
458

459
                if ( ! \is_array( $presenter_instances ) ) {
14✔
460
                        $presenter_instances = $presenters;
×
461
                }
462

463
                $is_presenter_callback = static function ( $presenter_instance ) {
14✔
464
                        return $presenter_instance instanceof Abstract_Indexable_Presenter;
14✔
465
                };
14✔
466
                $presenter_instances   = \array_filter( $presenter_instances, $is_presenter_callback );
14✔
467

468
                return \array_merge(
14✔
469
                        [ new Marker_Open_Presenter() ],
14✔
470
                        $presenter_instances,
14✔
471
                        [ new Marker_Close_Presenter() ]
14✔
472
                );
14✔
473
        }
474

475
        /**
476
         * Generate the array of presenters we need for the current request.
477
         *
478
         * @param string $page_type The page type we're retrieving presenters for.
479
         *
480
         * @return string[] The presenters.
481
         */
482
        private function get_needed_presenters( $page_type ) {
14✔
483
                $presenters = $this->get_presenters_for_page_type( $page_type );
14✔
484

485
                $presenters = $this->maybe_remove_title_presenter( $presenters );
14✔
486

487
                $callback   = static function ( $presenter ) {
14✔
488
                        return "Yoast\WP\SEO\Presenters\\{$presenter}_Presenter";
14✔
489
                };
14✔
490
                $presenters = \array_map( $callback, $presenters );
14✔
491

492
                /**
493
                 * Filter 'wpseo_frontend_presenter_classes' - Allow filtering presenters in or out of the request.
494
                 *
495
                 * @param array  $presenters List of presenters.
496
                 * @param string $page_type  The current page type.
497
                 */
498
                $presenters = \apply_filters( 'wpseo_frontend_presenter_classes', $presenters, $page_type );
14✔
499

500
                return $presenters;
14✔
501
        }
502

503
        /**
504
         * Filters the presenters based on the page type.
505
         *
506
         * @param string $page_type The page type.
507
         *
508
         * @return string[] The presenters.
509
         */
510
        private function get_presenters_for_page_type( $page_type ) {
14✔
511
                if ( $page_type === 'Error_Page' ) {
14✔
512
                        $presenters = $this->base_presenters;
8✔
513
                        if ( $this->options->get( 'opengraph' ) === true ) {
8✔
514
                                $presenters = \array_merge( $presenters, $this->open_graph_error_presenters );
8✔
515
                        }
516
                        return \array_merge( $presenters, $this->closing_presenters );
8✔
517
                }
518

519
                $presenters = $this->get_all_presenters();
6✔
520
                if ( \in_array( $page_type, [ 'Static_Home_Page', 'Home_Page' ], true ) ) {
6✔
521
                        $presenters = \array_merge( $presenters, $this->webmaster_verification_presenters );
2✔
522
                }
523

524
                // Filter out the presenters only needed for singular pages on non-singular pages.
525
                if ( ! \in_array( $page_type, [ 'Post_Type', 'Static_Home_Page' ], true ) ) {
6✔
526
                        $presenters = \array_diff( $presenters, $this->singular_presenters );
2✔
527
                }
528

529
                // Filter out `twitter:data` presenters for static home pages.
530
                if ( $page_type === 'Static_Home_Page' ) {
6✔
531
                        $presenters = \array_diff( $presenters, $this->slack_presenters );
2✔
532
                }
533

534
                return $presenters;
6✔
535
        }
536

537
        /**
538
         * Returns a list of all available presenters based on settings.
539
         *
540
         * @return string[] The presenters.
541
         */
542
        private function get_all_presenters() {
6✔
543
                $presenters = \array_merge( $this->base_presenters, $this->indexing_directive_presenters );
6✔
544
                if ( $this->options->get( 'opengraph' ) === true ) {
6✔
545
                        $presenters = \array_merge( $presenters, $this->open_graph_presenters );
6✔
546
                }
547
                if ( $this->options->get( 'twitter' ) === true && \apply_filters( 'wpseo_output_twitter_card', true ) !== false ) {
6✔
548
                        $presenters = \array_merge( $presenters, $this->twitter_card_presenters );
6✔
549
                }
550
                if ( $this->options->get( 'enable_enhanced_slack_sharing' ) === true && \apply_filters( 'wpseo_output_enhanced_slack_data', true ) !== false ) {
6✔
551
                        $presenters = \array_merge( $presenters, $this->slack_presenters );
6✔
552
                }
553

554
                return \array_merge( $presenters, $this->closing_presenters );
6✔
555
        }
556

557
        /**
558
         * Whether the title presenter should be removed.
559
         *
560
         * @return bool True when the title presenter should be removed, false otherwise.
561
         */
562
        public function should_title_presenter_be_removed() {
2✔
563
                return ! \get_theme_support( 'title-tag' ) && ! $this->options->get( 'forcerewritetitle', false );
2✔
564
        }
565

566
        /**
567
         * Checks if the Title presenter needs to be removed.
568
         *
569
         * @param string[] $presenters The presenters.
570
         *
571
         * @return string[] The presenters.
572
         */
573
        private function maybe_remove_title_presenter( $presenters ) {
×
574
                // Do not remove the title if we're on a REST request.
575
                if ( \wp_is_serving_rest_request() ) {
×
576
                        return $presenters;
×
577
                }
578

579
                // Remove the title presenter if the theme is hardcoded to output a title tag so we don't have two title tags.
580
                if ( $this->should_title_presenter_be_removed() ) {
×
581
                        $presenters = \array_diff( $presenters, [ 'Title' ] );
×
582
                }
583

584
                return $presenters;
×
585
        }
586
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc