Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
91.87% |
113 / 123 |
|
50.00% |
6 / 12 |
CRAP | |
0.00% |
0 / 1 |
| Breadcrumb_Schema_Node | |
91.87% |
113 / 123 |
|
50.00% |
6 / 12 |
78.02 | |
0.00% |
0 / 1 |
| build | |
97.56% |
40 / 41 |
|
0.00% |
0 / 1 |
18 | |||
| singular_items | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
15 | |||
| taxonomy_items | |
80.00% |
12 / 15 |
|
0.00% |
0 / 1 |
8.51 | |||
| blog_items | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| post_type_archive_items | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
4.25 | |||
| author_items | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
| date_items | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
9.37 | |||
| add_posts_page | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| posts_page | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
5.39 | |||
| add_item | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
| add_current | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| text | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * BreadcrumbList Schema.org node builder. |
| 4 | * |
| 5 | * @package automattic/jetpack-seo-package |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\SEO; |
| 9 | |
| 10 | /** |
| 11 | * Builds a BreadcrumbList for the current public request. |
| 12 | */ |
| 13 | class Breadcrumb_Schema_Node { |
| 14 | |
| 15 | /** |
| 16 | * Build the breadcrumb node for the current request. |
| 17 | * |
| 18 | * @return array|null |
| 19 | */ |
| 20 | public static function build() { |
| 21 | if ( is_front_page() || is_search() || is_404() ) { |
| 22 | return null; |
| 23 | } |
| 24 | |
| 25 | $items = array( |
| 26 | array( |
| 27 | 'name' => __( 'Home', 'jetpack-seo' ), |
| 28 | 'item' => home_url( '/' ), |
| 29 | ), |
| 30 | ); |
| 31 | |
| 32 | if ( is_singular() ) { |
| 33 | $items = self::singular_items( $items ); |
| 34 | } elseif ( is_category() || is_tag() || is_tax() ) { |
| 35 | $items = self::taxonomy_items( $items ); |
| 36 | } elseif ( is_home() ) { |
| 37 | $items = self::blog_items( $items ); |
| 38 | } elseif ( is_post_type_archive() ) { |
| 39 | $items = self::post_type_archive_items( $items ); |
| 40 | } elseif ( is_author() ) { |
| 41 | $items = self::author_items( $items ); |
| 42 | } elseif ( is_date() ) { |
| 43 | $items = self::date_items( $items ); |
| 44 | } else { |
| 45 | return null; |
| 46 | } |
| 47 | |
| 48 | if ( null === $items || count( $items ) < 2 ) { |
| 49 | return null; |
| 50 | } |
| 51 | |
| 52 | $list_items = array(); |
| 53 | $last = count( $items ) - 1; |
| 54 | foreach ( $items as $index => $item ) { |
| 55 | $name = self::text( $item['name'] ?? '' ); |
| 56 | if ( '' === $name ) { |
| 57 | return null; |
| 58 | } |
| 59 | |
| 60 | $list_item = array( |
| 61 | '@type' => 'ListItem', |
| 62 | 'position' => $index + 1, |
| 63 | 'name' => $name, |
| 64 | ); |
| 65 | if ( $index < $last && ! empty( $item['item'] ) ) { |
| 66 | $list_item['item'] = $item['item']; |
| 67 | } |
| 68 | $list_items[] = $list_item; |
| 69 | } |
| 70 | |
| 71 | return array( |
| 72 | '@type' => 'BreadcrumbList', |
| 73 | 'itemListElement' => $list_items, |
| 74 | ); |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Add breadcrumbs for a singular post. |
| 79 | * |
| 80 | * @param array $items Existing items. |
| 81 | * @return array|null |
| 82 | */ |
| 83 | private static function singular_items( array $items ) { |
| 84 | $post = get_queried_object(); |
| 85 | if ( ! $post instanceof \WP_Post || 'publish' !== $post->post_status ) { |
| 86 | return null; |
| 87 | } |
| 88 | |
| 89 | $post_type = get_post_type_object( $post->post_type ); |
| 90 | if ( ! $post_type || ! is_post_type_viewable( $post_type ) ) { |
| 91 | return null; |
| 92 | } |
| 93 | |
| 94 | if ( 'post' === $post->post_type ) { |
| 95 | self::add_posts_page( $items ); |
| 96 | } elseif ( 'page' !== $post->post_type && $post_type->has_archive ) { |
| 97 | self::add_item( $items, $post_type->labels->name, get_post_type_archive_link( $post->post_type ) ); |
| 98 | } |
| 99 | |
| 100 | $front_page_id = 'page' === get_option( 'show_on_front' ) ? (int) get_option( 'page_on_front' ) : 0; |
| 101 | if ( is_post_type_hierarchical( $post->post_type ) ) { |
| 102 | foreach ( array_reverse( get_post_ancestors( $post ) ) as $ancestor_id ) { |
| 103 | $ancestor = get_post( $ancestor_id ); |
| 104 | if ( $ancestor instanceof \WP_Post && $front_page_id === $ancestor->ID ) { |
| 105 | continue; |
| 106 | } |
| 107 | if ( $ancestor instanceof \WP_Post && 'publish' === $ancestor->post_status ) { |
| 108 | self::add_item( $items, $ancestor->post_title, get_permalink( $ancestor ) ); |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | return self::add_current( $items, $post->post_title ); |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * Add breadcrumbs for a taxonomy archive. |
| 118 | * |
| 119 | * @param array $items Existing items. |
| 120 | * @return array|null |
| 121 | */ |
| 122 | private static function taxonomy_items( array $items ) { |
| 123 | $term = get_queried_object(); |
| 124 | if ( ! $term instanceof \WP_Term ) { |
| 125 | return null; |
| 126 | } |
| 127 | |
| 128 | $taxonomy = get_taxonomy( $term->taxonomy ); |
| 129 | if ( ! $taxonomy || ! $taxonomy->public ) { |
| 130 | return null; |
| 131 | } |
| 132 | |
| 133 | if ( $taxonomy->hierarchical ) { |
| 134 | foreach ( array_reverse( get_ancestors( $term->term_id, $term->taxonomy, 'taxonomy' ) ) as $ancestor_id ) { |
| 135 | $ancestor = get_term( $ancestor_id, $term->taxonomy ); |
| 136 | if ( ! $ancestor instanceof \WP_Term ) { |
| 137 | continue; |
| 138 | } |
| 139 | |
| 140 | $link = get_term_link( $ancestor ); |
| 141 | if ( ! is_wp_error( $link ) ) { |
| 142 | self::add_item( $items, $ancestor->name, $link ); |
| 143 | } |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | return self::add_current( $items, $term->name ); |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Add the configured posts page as the current blog archive. |
| 152 | * |
| 153 | * @param array $items Existing items. |
| 154 | * @return array|null |
| 155 | */ |
| 156 | private static function blog_items( array $items ) { |
| 157 | $page = self::posts_page(); |
| 158 | return null === $page ? null : self::add_current( $items, $page->post_title ); |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * Add a custom post type archive. |
| 163 | * |
| 164 | * @param array $items Existing items. |
| 165 | * @return array|null |
| 166 | */ |
| 167 | private static function post_type_archive_items( array $items ) { |
| 168 | $post_type = get_queried_object(); |
| 169 | if ( ! $post_type instanceof \WP_Post_Type || ! is_post_type_viewable( $post_type ) || ! $post_type->has_archive ) { |
| 170 | return null; |
| 171 | } |
| 172 | |
| 173 | return self::add_current( $items, $post_type->labels->name ); |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * Add an author archive, optionally under the posts page. |
| 178 | * |
| 179 | * @param array $items Existing items. |
| 180 | * @return array|null |
| 181 | */ |
| 182 | private static function author_items( array $items ) { |
| 183 | $author = get_queried_object(); |
| 184 | if ( ! $author instanceof \WP_User || ! $author->exists() ) { |
| 185 | return null; |
| 186 | } |
| 187 | |
| 188 | self::add_posts_page( $items ); |
| 189 | return self::add_current( $items, $author->display_name ); |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Add a year, month, or day archive under the posts page. |
| 194 | * |
| 195 | * @param array $items Existing items. |
| 196 | * @return array|null |
| 197 | */ |
| 198 | private static function date_items( array $items ) { |
| 199 | global $wp_locale; |
| 200 | |
| 201 | $year = (int) get_query_var( 'year' ); |
| 202 | $month = (int) get_query_var( 'monthnum' ); |
| 203 | $day = (int) get_query_var( 'day' ); |
| 204 | |
| 205 | if ( $year < 1 || $year > 9999 ) { |
| 206 | return null; |
| 207 | } |
| 208 | |
| 209 | self::add_posts_page( $items ); |
| 210 | if ( is_year() ) { |
| 211 | return self::add_current( $items, (string) $year ); |
| 212 | } |
| 213 | |
| 214 | if ( $month < 1 || $month > 12 ) { |
| 215 | return null; |
| 216 | } |
| 217 | |
| 218 | self::add_item( $items, (string) $year, get_year_link( $year ) ); |
| 219 | $month_name = $wp_locale->get_month( $month ); |
| 220 | if ( is_month() ) { |
| 221 | return self::add_current( $items, $month_name ); |
| 222 | } |
| 223 | |
| 224 | if ( ! is_day() || ! checkdate( $month, $day, $year ) ) { |
| 225 | return null; |
| 226 | } |
| 227 | |
| 228 | self::add_item( $items, $month_name, get_month_link( $year, $month ) ); |
| 229 | return self::add_current( $items, (string) $day ); |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Add the configured, distinct posts page as an ancestor. |
| 234 | * |
| 235 | * @param array $items Items passed by reference. |
| 236 | * @return void |
| 237 | */ |
| 238 | private static function add_posts_page( array &$items ) { |
| 239 | $page = self::posts_page(); |
| 240 | if ( null !== $page && (int) get_option( 'page_on_front' ) !== $page->ID ) { |
| 241 | self::add_item( $items, $page->post_title, get_permalink( $page ) ); |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * Get the valid configured posts page. |
| 247 | * |
| 248 | * @return \WP_Post|null |
| 249 | */ |
| 250 | private static function posts_page() { |
| 251 | if ( 'page' !== get_option( 'show_on_front' ) ) { |
| 252 | return null; |
| 253 | } |
| 254 | |
| 255 | $page = get_post( (int) get_option( 'page_for_posts' ) ); |
| 256 | return $page instanceof \WP_Post && 'publish' === $page->post_status && '' !== self::text( $page->post_title ) ? $page : null; |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Add a navigable ancestor when its name and URL are valid. |
| 261 | * |
| 262 | * @param array $items Items passed by reference. |
| 263 | * @param mixed $name Item name. |
| 264 | * @param string $url Item URL. |
| 265 | * @return void |
| 266 | */ |
| 267 | private static function add_item( array &$items, $name, $url ) { |
| 268 | $name = self::text( $name ); |
| 269 | if ( '' !== $name && is_string( $url ) && '' !== $url ) { |
| 270 | $items[] = array( |
| 271 | 'name' => $name, |
| 272 | 'item' => $url, |
| 273 | ); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * Add the non-linked current item. |
| 279 | * |
| 280 | * @param array $items Existing items. |
| 281 | * @param mixed $name Current item name. |
| 282 | * @return array|null |
| 283 | */ |
| 284 | private static function add_current( array $items, $name ) { |
| 285 | $name = self::text( $name ); |
| 286 | if ( '' === $name ) { |
| 287 | return null; |
| 288 | } |
| 289 | |
| 290 | $items[] = array( 'name' => $name ); |
| 291 | return $items; |
| 292 | } |
| 293 | |
| 294 | /** |
| 295 | * Normalize a breadcrumb name to trimmed plain text. |
| 296 | * |
| 297 | * @param mixed $value Raw value. |
| 298 | * @return string |
| 299 | */ |
| 300 | private static function text( $value ) { |
| 301 | return is_string( $value ) ? trim( wp_strip_all_tags( $value ) ) : ''; |
| 302 | } |
| 303 | } |