cache = CacheManager::for('breadcrumbs', MONTH_IN_SECONDS)->connectTo('all'); } 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 []; } $key = get_queried_object_id() ?: 'home'; $crumbs = $this->cache->get($key); if ($crumbs !== false) { return $crumbs; } $crumbs = $this->buildCrumbs(); $this->cache->set($key, $crumbs); return $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); $crumbs = $this->addSingularCrumbs($crumbs, $obj); } 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 = jvbDirectories($tax); $crumbs[] = [ 'name' => $directory['title'], 'url' => $directory['url'] ]; } // Add term hierarchy $crumbs = array_merge($crumbs, $this->buildTermHierarchy($term)); return $crumbs; } /** * Add singular post breadcrumbs */ private function addSingularCrumbs(array $crumbs, WP_Post $post): array { // Add directory if exists $directory = jvbDirectories(jvbNoBase($post->post_type)); if (!empty($directory)) { $crumbs[] = [ 'name' => $directory['title'], 'url' => $directory['url'] ]; } // Handle directory posts specially if (jvbIsDirectory()) { $pos = jvbGetDirectoryInfo(); if (!empty($pos)) { // Special case for map if ($pos['title'] == 'Map') { $crumbs[] = [ 'name' => 'Tattoo Shops', 'url' => jvbDirectories(BASE.'shop')['url'] ]; } $crumbs[] = [ 'name' => $pos['title'], 'url' => $pos['url'] ]; } } else { // 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 (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' => $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->delete($objectId); } else { $this->cache->clear(); } } }