cache = Cache::for('breadcrumbs', MONTH_IN_SECONDS)->connect('post')->connect('taxonomy')->connect('user'); $this->cache->flush(); if (JVB_TESTING) { $this->cache->flush(); } } public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Get breadcrumb array for current page * * @return array Array of breadcrumb items with 'name', 'url', optional 'icon' and 'id' */ public function getCrumbs(): array { if (is_front_page()) { return []; } switch (true) { case is_singular(): $key = get_queried_object_id(); break; case is_post_type_archive(): $obj = get_queried_object(); $key = $obj->name; break; case is_tax(): $obj = get_queried_object(); $key = $obj->taxonomy; break; default: $key = 'home'; break; } return $this->cache->remember( $key, function() { $crumbs = $this->buildCrumbs(); return apply_filters('jvbBreadcrumbs',$crumbs); } ); } /** * Build breadcrumb array based on current page context */ private function buildCrumbs(): array { $crumbs = []; // Always start with home $crumbs[] = [ 'name' => get_bloginfo('name'), 'icon' => jvbIcon('house'), 'url' => get_home_url(), ]; $obj = get_queried_object(); if (is_tax()) { $crumbs = $this->addTaxonomyCrumbs($crumbs, $obj); } elseif (is_singular()) { $crumbs = $this->addArchiveCrumbs($crumbs, $obj); $hierarchy = $this->addSingularCrumbs($crumbs, $obj); $crumbs = $crumbs + $hierarchy; } elseif (is_post_type_archive() && !is_post_type_archive(BASE.'dash')) { $crumbs = $this->addArchiveCrumbs($crumbs, $obj); } return $crumbs; } /** * Add taxonomy-specific breadcrumbs */ private function addTaxonomyCrumbs(array $crumbs, WP_Term $term): array { $tax = jvbNoBase($term->taxonomy); $registrar = Registrar::getInstance($tax); // Add parent content archive if taxonomy is for single content type if ($registrar) { if (count($registrar->registrar->for) === 1){ $content = is_array($registrar->registrar->for) ? $registrar->registrar->for[0] : $registrar->registrar->for; $contentRegistrar = Registrar::getInstance($content); if($contentRegistrar && $contentRegistrar->hasFeature('show_directory')) { $directory = JVB()->directories(); if ($directory && !empty($directory->directories($content)??[])){ $crumbs[] = [ 'name' => $directory->directories($content)['title'], 'url' =>$directory->directories($content)['url'] ]; } } else { $crumbs[] = [ 'name' => $contentRegistrar->getConfig('breadcrumbs')['title']??$contentRegistrar->getPlural(), 'url' => get_post_type_archive_link(jvbCheckBase($content)), ]; } // $crumbs[] = [ // 'name' => 'By ' . $registrar->getSingular(), // 'url' => false, // ]; } } // Add directory if exists if ($registrar && $registrar->hasFeature('directory')) { $directory = JVB()->directories(); if ($directory && !empty($directory->directories($tax))) { $crumbs[] = [ 'name' => $directory['title'], 'url' => $directory['url'] ]; } } // Add term hierarchy return array_merge($crumbs, $this->buildTermHierarchy($term)); } /** * Add singular post breadcrumbs */ private function addSingularCrumbs(array $crumbs, WP_Post $post): array { // Add directory if exists $content = jvbNoBase($post->post_type); $registrar = Registrar::getInstance($content); $crumbConfig = false; if ($registrar){ $crumbConfig = $registrar->getConfig('breadcrumbs'); } if($registrar && $registrar->hasFeature('show_directory')) { $directory = JVB()->directories(); if ($directory && !empty($directory->directories($content)??[])){ $crumbs[] = [ 'name' => $directory->directories($content)['title'], 'url' =>$directory->directories($content)['url'] ]; } } // Handle directory posts specially if (JVB()->directories() && JVB()->directories()->isDirectory()) { $pos = jvbGetDirectoryInfo(); if (!empty($pos)) { // Special case for map if ($pos['title'] == 'Map') { $crumbs[] = [ 'name' => 'Tattoo Shops', 'url' => JVB()->directories()->directories(BASE.'shop')['url'] ]; } $crumbs[] = [ 'name' => $pos['title'], 'url' => $pos['url'] ]; } } else { if ($registrar && !empty($crumbConfig['addCrumb'])) { $crumbs = $this->addTaxToCrumbs($crumbs, $crumbConfig['addCrumb']); } // Add post hierarchy $crumbs = array_merge($crumbs, $this->buildPostHierarchy($post)); } return $crumbs; } /** * Add archive breadcrumbs */ private function addArchiveCrumbs(array $crumbs, object $obj): array { $type = is_singular() ? $obj->post_type : $obj->name; $name = jvbNoBase($type); $registrar = Registrar::getInstance($name); if($registrar && $registrar->hasFeature('show_directory')) { $directory = JVB()->directories(); if ($directory && !empty($directory->directories($name)??[])){ $crumbs[] = [ 'name' => $directory->directories($name)['title'], 'url' =>$directory->directories($name)['url'] ]; } } elseif (Site::has('is_directory') && $name === 'directory') { $crumbs[] = [ 'name' => JVB()->directories()->referAs(true), 'url' => get_post_type_archive_link($type) ]; } elseif ($registrar) { $crumbs[] = [ 'name' => $registrar->getConfig('breadcrumbs')['title'] ?? $registrar->getPlural(), 'url' => get_post_type_archive_link($type) ]; } else { $crumbs[] = [ 'name' => $obj->label, 'url' => get_post_type_archive_link($type) ]; } return $crumbs; } /** * Build term hierarchy recursively */ private function buildTermHierarchy(WP_Term $term, array $crumbs = []): array { $url = get_term_link($term->term_id); array_unshift($crumbs, [ 'name' => html_entity_decode($term->name), 'url' => $url, 'id' => $term->term_id, ]); if ($term->parent !== 0) { $parent = get_term($term->parent, $term->taxonomy); if ($parent && !is_wp_error($parent)) { $crumbs = $this->buildTermHierarchy($parent, $crumbs); } } return $crumbs; } /** * Build post hierarchy recursively */ private function buildPostHierarchy(WP_Post $post, array $crumbs = []): array { array_unshift($crumbs, [ 'name' => $post->post_title, 'url' => get_the_permalink($post->ID), 'id' => $post->ID, ]); if ($post->post_parent !== 0) { $parent = get_post($post->post_parent); if ($parent) { $crumbs = $this->buildPostHierarchy($parent, $crumbs); } } return $crumbs; } /** * Render breadcrumb navigation HTML * * @return string HTML breadcrumb navigation */ public function renderNavigation(): string { if (is_front_page()) { return ''; } $crumbs = $this->getCrumbs(); if (empty($crumbs)) { return ''; } $out = ''; return $out; } /** * Convert breadcrumb array to schema.org format * Used by SchemaOutputManager * * @return array Schema.org BreadcrumbList */ public function toSchema(): array { $crumbs = $this->getCrumbs(); if (empty($crumbs)) { return []; } $items = []; $position = 1; global $wp; $current = home_url( add_query_arg( $_GET, $wp->request ) ); foreach ($crumbs as $crumb) { // Schema requires a URL if ($crumb['url'] === false) { $crumb['url'] = $current; } $items[] = [ '@type' => 'ListItem', '@id' => $crumb['url'], 'position' => $position, 'name' => $crumb['name'], ]; $position++; } return [ '@type' => 'BreadcrumbList', '@id' => $current . '/#breadcrumbs', 'itemListElement' => $items ]; } /** * Invalidate breadcrumb cache for specific object */ public function invalidateCache(?int $objectId = null): void { if ($objectId) { $this->cache->forget($objectId); } else { $this->cache->flush(); } } public function addTaxToCrumbs(array $crumbs, string|array $taxonomy):array { $ID = get_the_ID(); $taxonomies = is_string($taxonomy) ? [$taxonomy] : $taxonomy; foreach ($taxonomies as $tax) { $taxonomy = jvbCheckBase($tax); $terms = get_the_terms($ID, $taxonomy); if ($terms && !is_wp_error($terms)) { $term = $terms[0]; $ancestors = get_ancestors($term->term_id, $taxonomy, 'taxonomy'); $ancestors = array_reverse($ancestors); foreach ($ancestors as $ancestor) { $aTerm = get_term($ancestor, $taxonomy); if ($aTerm && !is_wp_error($aTerm)) { $crumbs[] = [ 'name' => $aTerm->name, 'url' => get_term_link($ancestor, $taxonomy) ]; } } $crumbs[] = [ 'name' => html_entity_decode($term->name), 'url' => get_term_link($term, $taxonomy) ]; } } return $crumbs; } }