<?php
|
namespace JVBase\managers\SEO;
|
|
use JVBase\managers\Cache;
|
use JVBase\registrar\Registrar;
|
use JVBase\base\Site;
|
use WP_Post;
|
use WP_Term;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Breadcrumb Manager
|
*
|
* Generates breadcrumb navigation arrays and HTML output
|
* Integrates with SchemaOutputManager for structured data
|
*/
|
class BreadcrumbManager
|
{
|
private Cache $cache;
|
private static ?self $instance = null;
|
|
private function __construct()
|
{
|
$this->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;
|
case is_home():
|
$obj = get_queried_object();
|
$key = $obj->post_type;
|
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() || is_home()) {
|
$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
|
{
|
if (is_singular('page') || is_home()) {
|
return $crumbs;
|
}
|
$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 {
|
$postTypeObject = get_post_type_object($type);
|
$crumbs[] = [
|
'name' => $postTypeObject->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 = '<nav id="breadcrumbs">';
|
$out .= '<ol itemscope itemtype="https://schema.org/BreadcrumbList">';
|
|
$position = 1;
|
$total = count($crumbs);
|
foreach ($crumbs as $crumb) {
|
$label = '<span itemprop="name">' . strtolower($crumb['name']) . '</span>';
|
|
// Replace label with icon if present
|
if (isset($crumb['icon'])) {
|
$label = $crumb['icon'] . '<span class="screen-reader-text" itemprop="name">' . $crumb['name'] . '</span>';
|
}
|
|
$aOpen = $aClose = '';
|
|
// Add link if URL exists and not current page
|
if ($crumb['url'] !== false) {
|
if ($total !== $position) {
|
$aOpen = '<a itemprop="item" href="' . esc_url($crumb['url']) . '" title="' . esc_attr($crumb['name']) . '">';
|
$aClose = '</a>';
|
}
|
}
|
|
$out .= '<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">';
|
$out .= $aOpen . $label . $aClose;
|
$out .= '<meta itemprop="position" content="' . $position . '" />';
|
$out .= '</li>';
|
|
$position++;
|
}
|
|
$out .= '</ol>';
|
$out .= '</nav>';
|
|
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;
|
}
|
}
|