[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/blocks/ -> breadcrumbs.php (source)

   1  <?php
   2  /**
   3   * Server-side rendering of the `core/breadcrumbs` block.
   4   *
   5   * @package WordPress
   6   */
   7  
   8  /**
   9   * Renders the `core/breadcrumbs` block on the server.
  10   *
  11   * @since 7.0.0
  12   *
  13   * @param array    $attributes Block attributes.
  14   * @param string   $content    Block default content.
  15   * @param WP_Block $block      Block instance.
  16   *
  17   * @return string Returns the post breadcrumb for hierarchical post types.
  18   */
  19  function render_block_core_breadcrumbs( $attributes, $content, $block ) {
  20      $is_front_page = is_front_page();
  21  
  22      if ( ! $attributes['showOnHomePage'] && $is_front_page ) {
  23          return '';
  24      }
  25  
  26      $is_home          = is_home();
  27      $page_for_posts   = get_option( 'page_for_posts' );
  28      $breadcrumb_items = array();
  29  
  30      if ( $attributes['showHomeItem'] ) {
  31          // We make `home` a link if not on front page, or if front page
  32          // is set to a custom page and is paged.
  33          if ( ! $is_front_page || ( 'page' === get_option( 'show_on_front' ) && (int) get_query_var( 'page' ) > 1 ) ) {
  34              $breadcrumb_items[] = array(
  35                  'label' => __( 'Home' ),
  36                  'url'   => home_url( '/' ),
  37              );
  38          } else {
  39              $breadcrumb_items[] = block_core_breadcrumbs_create_item( __( 'Home' ), block_core_breadcrumbs_is_paged() );
  40          }
  41      }
  42  
  43      // Handle home.
  44      if ( $is_home ) {
  45          // These checks are explicitly nested in order not to execute the `else` branch.
  46          if ( $page_for_posts ) {
  47              $breadcrumb_items[] = block_core_breadcrumbs_create_item( block_core_breadcrumbs_get_post_title( $page_for_posts ), block_core_breadcrumbs_is_paged() );
  48          }
  49          if ( block_core_breadcrumbs_is_paged() ) {
  50              $breadcrumb_items[] = block_core_breadcrumbs_create_page_number_item();
  51          }
  52      } elseif ( $is_front_page ) {
  53          // Handle front page.
  54          // This check is explicitly nested in order not to execute the `else` branch.
  55          // If front page is set to custom page and is paged, add the page number.
  56          if ( (int) get_query_var( 'page' ) > 1 ) {
  57              $breadcrumb_items[] = block_core_breadcrumbs_create_page_number_item( 'page' );
  58          }
  59      } elseif ( is_search() ) {
  60          // Handle search results.
  61          $is_paged = block_core_breadcrumbs_is_paged();
  62          /* translators: %s: search query */
  63          $text               = sprintf( __( 'Search results for: "%s"' ), wp_trim_words( get_search_query(), 10 ) );
  64          $breadcrumb_items[] = block_core_breadcrumbs_create_item( $text, $is_paged );
  65          // Add the "Page X" as the current page if paginated.
  66          if ( $is_paged ) {
  67              $breadcrumb_items[] = block_core_breadcrumbs_create_page_number_item();
  68          }
  69      } elseif ( is_404() ) {
  70          // Handle 404 pages.
  71          $breadcrumb_items[] = array(
  72              'label' => __( 'Page not found' ),
  73          );
  74      } elseif ( is_archive() ) {
  75          // Handle archive pages (taxonomy, post type, date, author archives).
  76          $archive_breadcrumbs = block_core_breadcrumbs_get_archive_breadcrumbs();
  77          if ( ! empty( $archive_breadcrumbs ) ) {
  78              $breadcrumb_items = array_merge( $breadcrumb_items, $archive_breadcrumbs );
  79          }
  80      } else {
  81          // Handle single post/page breadcrumbs.
  82          if ( ! isset( $block->context['postId'] ) || ! isset( $block->context['postType'] ) ) {
  83              return '';
  84          }
  85  
  86          $post_id   = $block->context['postId'];
  87          $post_type = $block->context['postType'];
  88  
  89          $post = get_post( $post_id );
  90          if ( ! $post ) {
  91              return '';
  92          }
  93  
  94          // For non-hierarchical post types with parents (e.g., attachments), build trail for the parent.
  95          $post_parent = $post->post_parent;
  96          $parent_post = null;
  97          if ( ! is_post_type_hierarchical( $post_type ) && $post_parent ) {
  98              $parent_post = get_post( $post_parent );
  99              if ( $parent_post ) {
 100                  $post_id     = $parent_post->ID;
 101                  $post_type   = $parent_post->post_type;
 102                  $post_parent = $parent_post->post_parent;
 103              }
 104          }
 105  
 106          // Determine breadcrumb type.
 107          // Some non-hierarchical post types (e.g., attachments) can have parents.
 108          // Use hierarchical breadcrumbs if a parent exists, otherwise use taxonomy breadcrumbs.
 109          $show_terms = false;
 110          if ( ! is_post_type_hierarchical( $post_type ) && ! $post_parent ) {
 111              $show_terms = true;
 112          } elseif ( empty( get_object_taxonomies( $post_type, 'objects' ) ) ) {
 113              $show_terms = false;
 114          } else {
 115              $show_terms = $attributes['prefersTaxonomy'];
 116          }
 117  
 118          // Add post type archive link if applicable.
 119          $post_type_object = get_post_type_object( $post_type );
 120          $archive_link     = get_post_type_archive_link( $post_type );
 121          if ( $archive_link && untrailingslashit( home_url() ) !== untrailingslashit( $archive_link ) ) {
 122              $label = $post_type_object->labels->archives;
 123              if ( 'post' === $post_type && $page_for_posts ) {
 124                  $label = block_core_breadcrumbs_get_post_title( $page_for_posts );
 125              }
 126              $breadcrumb_items[] = array(
 127                  'label' => $label,
 128                  'url'   => $archive_link,
 129              );
 130          }
 131          // Build breadcrumb trail based on hierarchical structure or taxonomy terms.
 132          if ( ! $show_terms ) {
 133              $breadcrumb_items = array_merge( $breadcrumb_items, block_core_breadcrumbs_get_hierarchical_post_type_breadcrumbs( $post_id ) );
 134          } else {
 135              $breadcrumb_items = array_merge( $breadcrumb_items, block_core_breadcrumbs_get_terms_breadcrumbs( $post_id, $post_type ) );
 136          }
 137  
 138          // Add post title: linked when viewing a paginated page, plain text otherwise.
 139          $is_paged = (int) get_query_var( 'page' ) > 1 || (int) get_query_var( 'cpage' ) > 1;
 140          $title    = block_core_breadcrumbs_get_post_title( $post );
 141  
 142          if ( $is_paged ) {
 143              $breadcrumb_items[] = array(
 144                  'label'      => $title,
 145                  'url'        => get_permalink( $post ),
 146                  'allow_html' => true,
 147              );
 148              $breadcrumb_items[] = block_core_breadcrumbs_create_page_number_item( (int) get_query_var( 'cpage' ) > 1 ? 'cpage' : 'page' );
 149          } else {
 150              $breadcrumb_items[] = array(
 151                  'label'      => $title,
 152                  'allow_html' => true,
 153              );
 154          }
 155      }
 156  
 157      // Remove current item if disabled.
 158      if ( ! $attributes['showCurrentItem'] && ! empty( $breadcrumb_items ) ) {
 159          array_pop( $breadcrumb_items );
 160      }
 161  
 162      /**
 163       * Filters the breadcrumb items array before rendering.
 164       *
 165       * Allows developers to modify, add, or remove breadcrumb items.
 166       *
 167       * @since 7.0.0
 168       *
 169       * @param array[] $breadcrumb_items {
 170       *     Array of breadcrumb item data.
 171       *
 172       *     @type string $label      The breadcrumb text.
 173       *     @type string $url        Optional. The breadcrumb link URL.
 174       *     @type bool   $allow_html Optional. Whether to allow HTML in the label.
 175       *                              When true, the label will be sanitized with wp_kses_post(),
 176       *                              allowing only safe HTML tags. When false or omitted, all HTML
 177       *                              will be escaped with esc_html(). Default false.
 178       * }
 179       */
 180      $breadcrumb_items = apply_filters( 'block_core_breadcrumbs_items', $breadcrumb_items );
 181  
 182      if ( empty( $breadcrumb_items ) ) {
 183          return '';
 184      }
 185  
 186      $separator_attr     = $attributes['separator'] ?? null;
 187      $separator          = is_string( $separator_attr ) ? addcslashes( $separator_attr, '\\"' ) : '';
 188      $wrapper_attributes = get_block_wrapper_attributes(
 189          array(
 190              'style'      => '--separator: "' . $separator . '";',
 191              'aria-label' => __( 'Breadcrumbs' ),
 192          )
 193      );
 194  
 195      $breadcrumb_html = sprintf(
 196          '<nav %s><ol>%s</ol></nav>',
 197          $wrapper_attributes,
 198          implode(
 199              '',
 200              array_map(
 201                  static function ( $item ) {
 202                      $label = ! empty( $item['allow_html'] ) ? wp_kses_post( $item['label'] ) : esc_html( $item['label'] );
 203                      if ( ! empty( $item['url'] ) ) {
 204                          return '<li><a href="' . esc_url( $item['url'] ) . '">' . $label . '</a></li>';
 205                      }
 206                      return '<li><span aria-current="page">' . $label . '</span></li>';
 207                  },
 208                  $breadcrumb_items
 209              )
 210          )
 211      );
 212  
 213      return $breadcrumb_html;
 214  }
 215  
 216  /**
 217   * Checks if we're on a paginated view (page 2 or higher).
 218   *
 219   * @since 7.0.0
 220   *
 221   * @return bool True if paged > 1, false otherwise.
 222   */
 223  function block_core_breadcrumbs_is_paged() {
 224      $paged = (int) get_query_var( 'paged' );
 225      return $paged > 1;
 226  }
 227  
 228  /**
 229   * Creates a "Page X" breadcrumb item for paginated views.
 230   *
 231   * @since 7.0.0
 232   * @param string $query_var Optional. Query variable to get current page number. Default 'paged'.
 233   * @return array The "Page X" breadcrumb item data.
 234   */
 235  function block_core_breadcrumbs_create_page_number_item( $query_var = 'paged' ) {
 236      $paged = (int) get_query_var( $query_var );
 237  
 238      if ( 'cpage' === $query_var ) {
 239          return array(
 240              'label' => sprintf(
 241                  /* translators: %s: comment page number */
 242                  __( 'Comments Page %s' ),
 243                  number_format_i18n( $paged )
 244              ),
 245          );
 246      }
 247  
 248      return array(
 249          'label' => sprintf(
 250              /* translators: %s: page number */
 251              __( 'Page %s' ),
 252              number_format_i18n( $paged )
 253          ),
 254      );
 255  }
 256  
 257  
 258  /**
 259   * Creates a breadcrumb item that's either a link or current page item.
 260   *
 261   * When paginated (is_paged is true), creates a link to page 1.
 262   * Otherwise, creates a span marked as the current page.
 263   *
 264   * @since 7.0.0
 265   *
 266   * @param string $text     The text content.
 267   * @param bool   $is_paged Whether we're on a paginated view.
 268   *
 269   * @return array The breadcrumb item data.
 270   */
 271  function block_core_breadcrumbs_create_item( $text, $is_paged = false ) {
 272      $item = array( 'label' => $text );
 273      if ( $is_paged ) {
 274          $item['url'] = get_pagenum_link( 1 );
 275      }
 276      return $item;
 277  }
 278  
 279  /**
 280   * Gets a post title with fallback for empty titles.
 281   *
 282   * @since 7.0.0
 283   *
 284   * @param int|WP_Post $post_id_or_object The post ID or post object.
 285   *
 286   * @return string The post title or fallback text.
 287   */
 288  function block_core_breadcrumbs_get_post_title( $post_id_or_object ) {
 289      $title = get_the_title( $post_id_or_object );
 290      if ( strlen( $title ) === 0 ) {
 291          $title = __( '(no title)' );
 292      }
 293      return $title;
 294  }
 295  
 296  /**
 297   * Generates breadcrumb items from hierarchical post type ancestors.
 298   *
 299   * @since 7.0.0
 300   *
 301   * @param int $post_id The post ID.
 302   *
 303   * @return array Array of breadcrumb item data.
 304   */
 305  function block_core_breadcrumbs_get_hierarchical_post_type_breadcrumbs( $post_id ) {
 306      $breadcrumb_items = array();
 307      $ancestors        = get_post_ancestors( $post_id );
 308      $ancestors        = array_reverse( $ancestors );
 309  
 310      foreach ( $ancestors as $ancestor_id ) {
 311          $breadcrumb_items[] = array(
 312              'label'      => block_core_breadcrumbs_get_post_title( $ancestor_id ),
 313              'url'        => get_permalink( $ancestor_id ),
 314              'allow_html' => true,
 315          );
 316      }
 317      return $breadcrumb_items;
 318  }
 319  
 320  /**
 321   * Generates breadcrumb items for hierarchical term ancestors.
 322   *
 323   * For hierarchical taxonomies, retrieves and formats ancestor terms as breadcrumb links.
 324   *
 325   * @since 7.0.0
 326   *
 327   * @param int    $term_id  The term ID.
 328   * @param string $taxonomy The taxonomy name.
 329   *
 330   * @return array Array of breadcrumb item data for ancestors.
 331   */
 332  function block_core_breadcrumbs_get_term_ancestors_items( $term_id, $taxonomy ) {
 333      $breadcrumb_items = array();
 334  
 335      // Check if taxonomy is hierarchical and add ancestor term links.
 336      if ( is_taxonomy_hierarchical( $taxonomy ) ) {
 337          $term_ancestors = get_ancestors( $term_id, $taxonomy, 'taxonomy' );
 338          $term_ancestors = array_reverse( $term_ancestors );
 339          foreach ( $term_ancestors as $ancestor_id ) {
 340              $ancestor_term = get_term( $ancestor_id, $taxonomy );
 341              if ( $ancestor_term && ! is_wp_error( $ancestor_term ) ) {
 342                  $breadcrumb_items[] = array(
 343                      'label' => $ancestor_term->name,
 344                      'url'   => get_term_link( $ancestor_term ),
 345                  );
 346              }
 347          }
 348      }
 349  
 350      return $breadcrumb_items;
 351  }
 352  
 353  /**
 354   * Generates breadcrumb items for archive pages.
 355   *
 356   * Handles taxonomy archives, post type archives, date archives, and author archives.
 357   * For hierarchical taxonomies, includes ancestor terms in the breadcrumb trail.
 358   *
 359   * @since 7.0.0
 360   *
 361   * @return array Array of breadcrumb item data.
 362   */
 363  function block_core_breadcrumbs_get_archive_breadcrumbs() {
 364      $breadcrumb_items = array();
 365  
 366      // Date archive (check first since it doesn't have a queried object).
 367      if ( is_date() ) {
 368          $year  = get_query_var( 'year' );
 369          $month = get_query_var( 'monthnum' );
 370          $day   = get_query_var( 'day' );
 371  
 372          // Fallback to 'm' query var for plain permalinks.
 373          // Plain permalinks use ?m=YYYYMMDD format instead of separate query vars.
 374          if ( ! $year ) {
 375              $m = get_query_var( 'm' );
 376              if ( $m ) {
 377                  $year  = substr( $m, 0, 4 );
 378                  $month = substr( $m, 4, 2 );
 379                  $day   = (int) substr( $m, 6, 2 );
 380              }
 381          }
 382  
 383          $is_paged = block_core_breadcrumbs_is_paged();
 384  
 385          if ( $year ) {
 386              if ( $month ) {
 387                  // Year is linked if we have month.
 388                  $breadcrumb_items[] = array(
 389                      'label' => $year,
 390                      'url'   => get_year_link( $year ),
 391                  );
 392  
 393                  if ( $day ) {
 394                      // Month is linked if we have day.
 395                      $breadcrumb_items[] = array(
 396                          'label' => date_i18n( 'F', mktime( 0, 0, 0, $month, 1, $year ) ),
 397                          'url'   => get_month_link( $year, $month ),
 398                      );
 399                      // Add day (current if not paginated, link if paginated).
 400                      $breadcrumb_items[] = block_core_breadcrumbs_create_item(
 401                          $day,
 402                          $is_paged
 403                      );
 404                  } else {
 405                      // Add month (current if not paginated, link if paginated).
 406                      $breadcrumb_items[] = block_core_breadcrumbs_create_item(
 407                          date_i18n( 'F', mktime( 0, 0, 0, $month, 1, $year ) ),
 408                          $is_paged
 409                      );
 410                  }
 411              } else {
 412                  // Add year (current if not paginated, link if paginated).
 413                  $breadcrumb_items[] = block_core_breadcrumbs_create_item(
 414                      $year,
 415                      $is_paged
 416                  );
 417              }
 418          }
 419  
 420          // Add pagination breadcrumb if on a paged date archive.
 421          if ( $is_paged ) {
 422              $breadcrumb_items[] = block_core_breadcrumbs_create_page_number_item();
 423          }
 424  
 425          return $breadcrumb_items;
 426      }
 427  
 428      // For other archive types, we need a queried object.
 429      $queried_object = get_queried_object();
 430  
 431      if ( ! $queried_object ) {
 432          return array();
 433      }
 434  
 435      $is_paged = block_core_breadcrumbs_is_paged();
 436  
 437      // Taxonomy archive (category, tag, custom taxonomy).
 438      if ( $queried_object instanceof WP_Term ) {
 439          $term     = $queried_object;
 440          $taxonomy = $term->taxonomy;
 441  
 442          // Add hierarchical term ancestors if applicable.
 443          $breadcrumb_items = array_merge(
 444              $breadcrumb_items,
 445              block_core_breadcrumbs_get_term_ancestors_items( $term->term_id, $taxonomy )
 446          );
 447  
 448          // Add current term (current if not paginated, link if paginated).
 449          $breadcrumb_items[] = block_core_breadcrumbs_create_item(
 450              $term->name,
 451              $is_paged
 452          );
 453      } elseif ( is_post_type_archive() ) {
 454          // Post type archive.
 455          $post_type = get_query_var( 'post_type' );
 456          if ( is_array( $post_type ) ) {
 457              $post_type = reset( $post_type );
 458          }
 459          $post_type_object = get_post_type_object( $post_type );
 460  
 461          /** This filter is documented in wp-includes/general-template.php */
 462          $title = apply_filters( 'post_type_archive_title', $post_type_object->labels->archives, $post_type ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 463  
 464          if ( $post_type_object ) {
 465              // Add post type (current if not paginated, link if paginated).
 466              $breadcrumb_items[] = block_core_breadcrumbs_create_item(
 467                  $title ? $title : $post_type_object->labels->archives,
 468                  $is_paged
 469              );
 470          }
 471      } elseif ( is_author() ) {
 472          // Author archive.
 473          $author = $queried_object;
 474          // Add author (current if not paginated, link if paginated).
 475          $breadcrumb_items[] = block_core_breadcrumbs_create_item(
 476              $author->display_name,
 477              $is_paged
 478          );
 479      }
 480  
 481      // Add pagination breadcrumb if on a paged archive.
 482      if ( $is_paged ) {
 483          $breadcrumb_items[] = block_core_breadcrumbs_create_page_number_item();
 484      }
 485  
 486      return $breadcrumb_items;
 487  }
 488  
 489  /**
 490   * Generates breadcrumb items from taxonomy terms.
 491   *
 492   * Finds the first publicly queryable taxonomy with terms assigned to the post
 493   * and generates breadcrumb links, including hierarchical term ancestors if applicable.
 494   *
 495   * @since 7.0.0
 496   *
 497   * @param int    $post_id   The post ID.
 498   * @param string $post_type The post type name.
 499   *
 500   * @return array Array of breadcrumb item data.
 501   */
 502  function block_core_breadcrumbs_get_terms_breadcrumbs( $post_id, $post_type ) {
 503      $breadcrumb_items = array();
 504  
 505      // Get public taxonomies for this post type.
 506      $taxonomies = wp_filter_object_list(
 507          get_object_taxonomies( $post_type, 'objects' ),
 508          array(
 509              'publicly_queryable' => true,
 510              'show_in_rest'       => true,
 511          )
 512      );
 513  
 514      if ( empty( $taxonomies ) ) {
 515          return $breadcrumb_items;
 516      }
 517  
 518      /**
 519       * Filters breadcrumb settings (taxonomy and term selection) for a post or post type.
 520       *
 521       * Allows developers to specify which taxonomy and term should be used in the
 522       * breadcrumb trail when a post type has multiple taxonomies or when a post is
 523       * assigned to multiple terms within a taxonomy.
 524       *
 525       * @since 7.0.0
 526       *
 527       * @param array  $settings  {
 528       *     Array of breadcrumb settings. Default empty array.
 529       *
 530       *     @type string $taxonomy Optional. Taxonomy slug to use for breadcrumbs.
 531       *                            The taxonomy must be registered for the post type and have
 532       *                            terms assigned to the post. If not found or has no terms,
 533       *                            fall back to the first available taxonomy with terms.
 534       *     @type string $term     Optional. Term slug to use when the post has multiple terms
 535       *                            in the selected taxonomy. If the term is not found or not
 536       *                            assigned to the post, fall back to the first term. If the
 537       *                            post has only one term, that term is used regardless.
 538       * }
 539       * @param string $post_type The post type slug.
 540       * @param int    $post_id   The post ID.
 541       */
 542      $settings = apply_filters( 'block_core_breadcrumbs_post_type_settings', array(), $post_type, $post_id );
 543  
 544      $taxonomy_name = null;
 545      $terms         = array();
 546  
 547      // Try preferred taxonomy first if specified.
 548      if ( ! empty( $settings['taxonomy'] ) ) {
 549          foreach ( $taxonomies as $taxonomy ) {
 550              if ( $taxonomy->name === $settings['taxonomy'] ) {
 551                  $post_terms = get_the_terms( $post_id, $taxonomy->name );
 552                  if ( ! empty( $post_terms ) && ! is_wp_error( $post_terms ) ) {
 553                      $taxonomy_name = $taxonomy->name;
 554                      $terms         = $post_terms;
 555                  }
 556                  break;
 557              }
 558          }
 559      }
 560  
 561      // If no preferred taxonomy or it didn't have terms, find the first taxonomy with terms.
 562      if ( empty( $terms ) ) {
 563          foreach ( $taxonomies as $taxonomy ) {
 564              $post_terms = get_the_terms( $post_id, $taxonomy->name );
 565              if ( ! empty( $post_terms ) && ! is_wp_error( $post_terms ) ) {
 566                  $taxonomy_name = $taxonomy->name;
 567                  $terms         = $post_terms;
 568                  break;
 569              }
 570          }
 571      }
 572  
 573      if ( ! empty( $terms ) ) {
 574          // Select which term to use.
 575          $term = reset( $terms );
 576  
 577          // Try preferred term if specified and post has multiple terms.
 578          if ( ! empty( $settings['term'] ) && count( $terms ) > 1 ) {
 579              foreach ( $terms as $candidate_term ) {
 580                  if ( $candidate_term->slug === $settings['term'] ) {
 581                      $term = $candidate_term;
 582                      break;
 583                  }
 584              }
 585          }
 586  
 587          // Add hierarchical term ancestors if applicable.
 588          $breadcrumb_items   = array_merge(
 589              $breadcrumb_items,
 590              block_core_breadcrumbs_get_term_ancestors_items( $term->term_id, $taxonomy_name )
 591          );
 592          $breadcrumb_items[] = array(
 593              'label' => $term->name,
 594              'url'   => get_term_link( $term ),
 595          );
 596      }
 597      return $breadcrumb_items;
 598  }
 599  
 600  /**
 601   * Registers the `core/breadcrumbs` block on the server.
 602   *
 603   * @since 7.0.0
 604   */
 605  function register_block_core_breadcrumbs() {
 606      register_block_type_from_metadata(
 607          __DIR__ . '/breadcrumbs',
 608          array(
 609              'render_callback' => 'render_block_core_breadcrumbs',
 610          )
 611      );
 612  }
 613  add_action( 'init', 'register_block_core_breadcrumbs' );


Generated : Wed Sep 23 08:20:35 2026 Cross-referenced by PHPXref