PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
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 / generators / breadcrumbs-generator.php

breadcrumbs-generator.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.2, at src/generators/breadcrumbs-generator.php

432 lines 12.6 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\Generators;
4
5 use Yoast\WP\SEO\Context\Meta_Tags_Context;
6 use Yoast\WP\SEO\Helpers\Current_Page_Helper;
7 use Yoast\WP\SEO\Helpers\Options_Helper;
8 use Yoast\WP\SEO\Helpers\Pagination_Helper;
9 use Yoast\WP\SEO\Helpers\Post_Type_Helper;
10 use Yoast\WP\SEO\Helpers\Url_Helper;
11 use Yoast\WP\SEO\Models\Indexable;
12 use Yoast\WP\SEO\Repositories\Indexable_Repository;
13
14 /**
15 * Represents the generator class for the breadcrumbs.
16 */
17 class Breadcrumbs_Generator implements Generator_Interface {
18
19 /**
20 * The indexable repository.
21 *
22 * @var Indexable_Repository
23 */
24 private $repository;
25
26 /**
27 * The options helper.
28 *
29 * @var Options_Helper
30 */
31 private $options;
32
33 /**
34 * The current page helper.
35 *
36 * @var Current_Page_Helper
37 */
38 private $current_page_helper;
39
40 /**
41 * The post type helper.
42 *
43 * @var Post_Type_Helper
44 */
45 private $post_type_helper;
46
47 /**
48 * The URL helper.
49 *
50 * @var Url_Helper
51 */
52 private $url_helper;
53
54 /**
55 * The pagination helper.
56 *
57 * @var Pagination_Helper
58 */
59 private $pagination_helper;
60
61 /**
62 * Breadcrumbs_Generator constructor.
63 *
64 * @param Indexable_Repository $repository The repository.
65 * @param Options_Helper $options The options helper.
66 * @param Current_Page_Helper $current_page_helper The current page helper.
67 * @param Post_Type_Helper $post_type_helper The post type helper.
68 * @param Url_Helper $url_helper The URL helper.
69 * @param Pagination_Helper $pagination_helper The pagination helper.
70 */
71 public function __construct(
72 Indexable_Repository $repository,
73 Options_Helper $options,
74 Current_Page_Helper $current_page_helper,
75 Post_Type_Helper $post_type_helper,
76 Url_Helper $url_helper,
77 Pagination_Helper $pagination_helper
78 ) {
79 $this->repository = $repository;
80 $this->options = $options;
81 $this->current_page_helper = $current_page_helper;
82 $this->post_type_helper = $post_type_helper;
83 $this->url_helper = $url_helper;
84 $this->pagination_helper = $pagination_helper;
85 }
86
87 /**
88 * Generates the breadcrumbs.
89 *
90 * @param Meta_Tags_Context $context The meta tags context.
91 *
92 * @return array<array<int, string>> An array of associative arrays that each have a 'text' and a 'url'.
93 */
94 public function generate( Meta_Tags_Context $context ) {
95 $static_ancestors = [];
96 $breadcrumbs_home = $this->options->get( 'breadcrumbs-home' );
97 if ( $breadcrumbs_home !== '' && ! \in_array( $this->current_page_helper->get_page_type(), [ 'Home_Page', 'Static_Home_Page' ], true ) ) {
98 $front_page_id = $this->current_page_helper->get_front_page_id();
99 if ( $front_page_id === 0 ) {
100 $home_page_ancestor = $this->repository->find_for_home_page();
101 if ( \is_a( $home_page_ancestor, Indexable::class ) ) {
102 $static_ancestors[] = $home_page_ancestor;
103 }
104 }
105 else {
106 $static_ancestor = $this->repository->find_by_id_and_type( $front_page_id, 'post' );
107 if ( \is_a( $static_ancestor, Indexable::class ) && $static_ancestor->post_status !== 'unindexed' ) {
108 $static_ancestors[] = $static_ancestor;
109 }
110 }
111 }
112 $page_for_posts = \get_option( 'page_for_posts' );
113 if ( $this->should_have_blog_crumb( $page_for_posts, $context ) ) {
114 $static_ancestor = $this->repository->find_by_id_and_type( $page_for_posts, 'post' );
115 if ( \is_a( $static_ancestor, Indexable::class ) && $static_ancestor->post_status !== 'unindexed' ) {
116 $static_ancestors[] = $static_ancestor;
117 }
118 }
119 if (
120 $context->indexable->object_type === 'post'
121 && $context->indexable->object_sub_type !== 'post'
122 && $context->indexable->object_sub_type !== 'page'
123 && $this->post_type_helper->has_archive( $context->indexable->object_sub_type )
124 ) {
125 $static_ancestor = $this->repository->find_for_post_type_archive( $context->indexable->object_sub_type );
126 if ( \is_a( $static_ancestor, Indexable::class ) ) {
127 $static_ancestors[] = $static_ancestor;
128 }
129 }
130 if ( $context->indexable->object_type === 'term' ) {
131 $parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type );
132 if ( $parent && $parent !== 'post' && $this->post_type_helper->has_archive( $parent ) ) {
133 $static_ancestor = $this->repository->find_for_post_type_archive( $parent );
134 if ( \is_a( $static_ancestor, Indexable::class ) ) {
135 $static_ancestors[] = $static_ancestor;
136 }
137 }
138 }
139 $indexables = [];
140 if ( ! \in_array( $this->current_page_helper->get_page_type(), [ 'Home_Page', 'Static_Home_Page' ], true ) ) {
141 // Get all ancestors of the indexable and append itself to get all indexables in the full crumb.
142 $indexables = $this->repository->get_ancestors( $context->indexable );
143 }
144 $indexables[] = $context->indexable;
145
146 if ( ! empty( $static_ancestors ) ) {
147 \array_unshift( $indexables, ...$static_ancestors );
148 }
149
150 $indexables = \apply_filters( 'wpseo_breadcrumb_indexables', $indexables, $context );
151 $indexables = \is_array( $indexables ) ? $indexables : [];
152 $indexables = \array_filter(
153 $indexables,
154 static function ( $indexable ) {
155 return \is_a( $indexable, Indexable::class );
156 },
157 );
158
159 $crumbs = \array_map( [ $this, 'get_post_type_crumb' ], $indexables );
160
161 if ( $breadcrumbs_home !== '' ) {
162 $crumbs[0]['text'] = $breadcrumbs_home;
163 }
164
165 $crumbs = $this->add_paged_crumb( $crumbs, $context->indexable );
166
167 /**
168 * Filter: 'wpseo_breadcrumb_links' - Allow the developer to filter the Yoast SEO breadcrumb links, add to them, change order, etc.
169 *
170 * @param array $crumbs The crumbs array.
171 */
172 $filtered_crumbs = \apply_filters( 'wpseo_breadcrumb_links', $crumbs );
173
174 // Basic check to make sure the filtered crumbs are in an array.
175 if ( ! \is_array( $filtered_crumbs ) ) {
176 \_doing_it_wrong(
177 'Filter: \'wpseo_breadcrumb_links\'',
178 'The `wpseo_breadcrumb_links` filter should return a multi-dimensional array.',
179 'YoastSEO v20.0',
180 );
181 }
182 else {
183 $crumbs = $filtered_crumbs;
184 }
185
186 $filter_callback = static function ( $link_info, $index ) use ( $crumbs ) {
187 /**
188 * Filter: 'wpseo_breadcrumb_single_link_info' - Allow developers to filter the Yoast SEO Breadcrumb link information.
189 *
190 * @param array $link_info The breadcrumb link information.
191 * @param int $index The index of the breadcrumb in the list.
192 * @param array $crumbs The complete list of breadcrumbs.
193 */
194 return \apply_filters( 'wpseo_breadcrumb_single_link_info', $link_info, $index, $crumbs );
195 };
196 return \array_map( $filter_callback, $crumbs, \array_keys( $crumbs ) );
197 }
198
199 /**
200 * Returns the modified post crumb.
201 *
202 * @param string[] $crumb The crumb.
203 * @param Indexable $ancestor The indexable.
204 *
205 * @return array<int, string> The crumb.
206 */
207 private function get_post_crumb( $crumb, $ancestor ) {
208 $crumb['id'] = $ancestor->object_id;
209
210 return $crumb;
211 }
212
213 /**
214 * Adds the correct ID to the crumb array based on the ancestor provided.
215 *
216 * @param Indexable $ancestor The ancestor indexable.
217 *
218 * @return string[]
219 */
220 private function get_post_type_crumb( Indexable $ancestor ) {
221 $crumb = [
222 'url' => $ancestor->permalink,
223 'text' => $ancestor->breadcrumb_title,
224 ];
225
226 switch ( $ancestor->object_type ) {
227 case 'post':
228 $crumb = $this->get_post_crumb( $crumb, $ancestor );
229 break;
230 case 'post-type-archive':
231 $crumb = $this->get_post_type_archive_crumb( $crumb, $ancestor );
232 break;
233 case 'term':
234 $crumb = $this->get_term_crumb( $crumb, $ancestor );
235 break;
236 case 'system-page':
237 $crumb = $this->get_system_page_crumb( $crumb, $ancestor );
238 break;
239 case 'user':
240 $crumb = $this->get_user_crumb( $crumb, $ancestor );
241 break;
242 case 'date-archive':
243 $crumb = $this->get_date_archive_crumb( $crumb );
244 break;
245 default:
246 // Handle unknown object types (optional).
247 break;
248 }
249
250 return $crumb;
251 }
252
253 /**
254 * Returns the modified post type crumb.
255 *
256 * @param string[] $crumb The crumb.
257 * @param Indexable $ancestor The indexable.
258 *
259 * @return string[] The crumb.
260 */
261 private function get_post_type_archive_crumb( $crumb, $ancestor ) {
262 $crumb['ptarchive'] = $ancestor->object_sub_type;
263
264 return $crumb;
265 }
266
267 /**
268 * Returns the modified term crumb.
269 *
270 * @param string[] $crumb The crumb.
271 * @param Indexable $ancestor The indexable.
272 *
273 * @return array<int, string> The crumb.
274 */
275 private function get_term_crumb( $crumb, $ancestor ) {
276 $crumb['term_id'] = $ancestor->object_id;
277 $crumb['taxonomy'] = $ancestor->object_sub_type;
278
279 return $crumb;
280 }
281
282 /**
283 * Returns the modified system page crumb.
284 *
285 * @param string[] $crumb The crumb.
286 * @param Indexable $ancestor The indexable.
287 *
288 * @return string[] The crumb.
289 */
290 private function get_system_page_crumb( $crumb, $ancestor ) {
291 if ( $ancestor->object_sub_type === 'search-result' ) {
292 $crumb['text'] = $this->options->get( 'breadcrumbs-searchprefix' ) . ' ' . \esc_html( \get_search_query() );
293 $crumb['url'] = \get_search_link();
294 }
295 elseif ( $ancestor->object_sub_type === '404' ) {
296 $crumb['text'] = $this->options->get( 'breadcrumbs-404crumb' );
297 }
298
299 return $crumb;
300 }
301
302 /**
303 * Returns the modified user crumb.
304 *
305 * @param string[] $crumb The crumb.
306 * @param Indexable $ancestor The indexable.
307 *
308 * @return string[] The crumb.
309 */
310 private function get_user_crumb( $crumb, $ancestor ) {
311 $display_name = \get_the_author_meta( 'display_name', $ancestor->object_id );
312 $crumb['text'] = $this->options->get( 'breadcrumbs-archiveprefix' ) . ' ' . $display_name;
313
314 return $crumb;
315 }
316
317 /**
318 * Returns the modified date archive crumb.
319 *
320 * @param string[] $crumb The crumb.
321 *
322 * @return string[] The crumb.
323 */
324 protected function get_date_archive_crumb( $crumb ) {
325 $home_url = $this->url_helper->home();
326 $prefix = $this->options->get( 'breadcrumbs-archiveprefix' );
327
328 if ( \is_day() ) {
329 $day = \esc_html( \get_the_date() );
330 $crumb['url'] = $home_url . \get_the_date( 'Y/m/d' ) . '/';
331 $crumb['text'] = $prefix . ' ' . $day;
332 }
333 elseif ( \is_month() ) {
334 $month = \esc_html( \trim( \single_month_title( ' ', false ) ) );
335 $crumb['url'] = $home_url . \get_the_date( 'Y/m' ) . '/';
336 $crumb['text'] = $prefix . ' ' . $month;
337 }
338 elseif ( \is_year() ) {
339 $year = \get_the_date( 'Y' );
340 $crumb['url'] = $home_url . $year . '/';
341 $crumb['text'] = $prefix . ' ' . $year;
342 }
343
344 return $crumb;
345 }
346
347 /**
348 * Returns whether or not a blog crumb should be added.
349 *
350 * @param int $page_for_posts The page for posts ID.
351 * @param Meta_Tags_Context $context The meta tags context.
352 *
353 * @return bool Whether or not a blog crumb should be added.
354 */
355 protected function should_have_blog_crumb( $page_for_posts, $context ) {
356 // When there is no page configured as blog page.
357 if ( \get_option( 'show_on_front' ) !== 'page' || ! $page_for_posts ) {
358 return false;
359 }
360
361 if ( $context->indexable->object_type === 'term' ) {
362 $parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type );
363 return $parent === 'post';
364 }
365
366 if ( $this->options->get( 'breadcrumbs-display-blog-page' ) !== true ) {
367 return false;
368 }
369
370 // When the current page is the home page, searchpage or isn't a singular post.
371 if ( \is_home() || \is_search() || ! \is_singular( 'post' ) ) {
372 return false;
373 }
374
375 return true;
376 }
377
378 /**
379 * Returns the post type parent of a given taxonomy.
380 *
381 * @param string $taxonomy The taxonomy.
382 *
383 * @return string|false The parent if it exists, false otherwise.
384 */
385 protected function get_taxonomy_post_type_parent( $taxonomy ) {
386 $parent = $this->options->get( 'taxonomy-' . $taxonomy . '-ptparent' );
387
388 if ( empty( $parent ) || (string) $parent === '0' ) {
389 return false;
390 }
391
392 return $parent;
393 }
394
395 /**
396 * Adds a crumb for the current page, if we're on an archive page or paginated post.
397 *
398 * @param string[] $crumbs The array of breadcrumbs.
399 * @param Indexable $current_indexable The current indexable.
400 *
401 * @return string[] The breadcrumbs.
402 */
403 protected function add_paged_crumb( array $crumbs, $current_indexable ) {
404 $is_simple_page = $this->current_page_helper->is_simple_page();
405
406 // If we're not on a paged page do nothing.
407 if ( ! $is_simple_page && ! $this->current_page_helper->is_paged() ) {
408 return $crumbs;
409 }
410
411 // If we're not on a paginated post do nothing.
412 if ( $is_simple_page && $current_indexable->number_of_pages === null ) {
413 return $crumbs;
414 }
415
416 $current_page_number = $this->pagination_helper->get_current_page_number();
417 if ( $current_page_number <= 1 ) {
418 return $crumbs;
419 }
420
421 $crumbs[] = [
422 'text' => \sprintf(
423 /* translators: %s expands to the current page number */
424 \__( 'Page %s', 'wordpress-seo' ),
425 $current_page_number,
426 ),
427 ];
428
429 return $crumbs;
430 }
431 }
432