cache = Cache::for('breadcrumbs', MONTH_IN_SECONDS)->connect('post')->connect('taxonomy')->connect('user'); 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' => 'Home', '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); $config = Features::getConfig($tax, 'term'); // Add parent content archive if taxonomy is for single content type if (count($config['for_content']) === 1) { $contentConfig = JVB_CONTENT[$config['for_content'][0]]; $crumbs[] = [ 'name' => $contentConfig['breadcrumb'] ?? $contentConfig['plural'], 'url' => get_post_type_archive_link(jvbCheckBase($config['for_content'][0])), ]; $crumbs[] = [ 'name' => 'By ' . $config['singular'], 'url' => false, ]; } // Add directory if exists if (Features::forTaxonomy($tax)->has('directory')) { $directory = JVB()->directories()?->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); if(Features::forContent($content)->has('show_directory')) { $directory = JVB()->directories()->getDirectoryList()[$content]??[]; if (!empty($directory)) { $crumbs[] = [ 'name' => $directory['title'], 'url' =>$directory['url'] ]; } } // Handle directory posts specially if (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 { $name = jvbNoBase($post->post_type); if (array_key_exists($name, JVB_CONTENT) && array_key_exists('addCrumb', JVB_CONTENT[$name])) { $crumbs = $this->addTaxToCrumbs($crumbs, JVB_CONTENT[$name]['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); if (Features::forSite()->has('is_directory') && $name === 'directory') { $crumbs[] = [ 'name' => JVB()->directories()->referAs(true), 'url' => get_post_type_archive_link($type) ]; } elseif ((is_post_type_archive() || !Features::forContent($name)->has('show_directory')) && array_key_exists($name, JVB_CONTENT)) { $crumbs[] = [ 'name' => JVB_CONTENT[$name]['breadcrumb'] ?? JVB_CONTENT[$name]['plural'], '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; foreach ($crumbs as $crumb) { // Schema requires a URL if ($crumb['url'] === false) { $crumb['url'] = get_permalink(); } $items[] = [ '@type' => 'ListItem', 'position' => $position, 'name' => $crumb['name'], 'item' => $crumb['url'], ]; $position++; } return [ '@type' => 'BreadcrumbList', '@id' => get_permalink() . '/#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 $taxonomy):array { $ID = get_the_ID(); $taxonomy = jvbCheckBase($taxonomy); $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; } }