feat: one breadcrumb, same place and style on every inner page

- Plugin: vc_breadcrumb_trail()/vc_breadcrumb_html(); tours, region, city
  and tag pages show Home / (Region) / Page in the banner instead of the
  generic 'Tour & Travel' label; the tour page uses the same helper
- Theme: blog list shows Home / Blog; blog post title, breadcrumb and
  meta move into the banner like the other pages
- Shared style: title's colour (white in photo banners, black on About),
  softer links, current page bold and shortened if long - also on phones
- Plugin 1.9.6, theme 1.0.16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 21:32:09 +03:30
parent 023b998694
commit 466ea0ac5f
14 changed files with 148 additions and 69 deletions

Binary file not shown.

View File

@@ -62,13 +62,6 @@ body.hoho-fullwidth{overflow-x:hidden;}
/* A page's own Featured Image banner is shown as-is: no brand-teal tint. */ /* A page's own Featured Image banner is shown as-is: no brand-teal tint. */
.hoho-page.hoho-page .hero--has-thumb .hero__media::after{display:none;} .hoho-page.hoho-page .hero--has-thumb .hero__media::after{display:none;}
/* Blog post header: real titles are long — use the full width (the design's
28rem cap wrapped them to four lines) with the meta underneath, and a
normal gap before the article instead of the design's ~100px. */
.hoho-page.hoho-page .post__head{flex-direction:column;align-items:flex-start;gap:var(--sp-2);margin-block-end:0;}
.hoho-page.hoho-page .post__title{max-width:none;}
.hoho-page.hoho-page .post__head + .post__body{margin-block-start:calc(var(--sp-5) - clamp(3rem, 2rem + 3vw, 6.25rem));}
/* Article headings by level (imported posts use h2–h6). */ /* Article headings by level (imported posts use h2–h6). */
.hoho-page.hoho-page .hoho-prose h2{margin:var(--sp-4) 0 0;font-size:clamp(1.25rem,1.1rem + .6vw,1.625rem);font-weight:700;line-height:1.25;} .hoho-page.hoho-page .hoho-prose h2{margin:var(--sp-4) 0 0;font-size:clamp(1.25rem,1.1rem + .6vw,1.625rem);font-weight:700;line-height:1.25;}
.hoho-page.hoho-page .hoho-prose h3{margin:var(--sp-3) 0 0;font-size:1.25rem;font-weight:700;line-height:1.3;} .hoho-page.hoho-page .hoho-prose h3{margin:var(--sp-3) 0 0;font-size:1.25rem;font-weight:700;line-height:1.3;}
@@ -82,3 +75,22 @@ body.hoho-fullwidth{overflow-x:hidden;}
/* Embedded maps/videos: full width, sensible height. */ /* Embedded maps/videos: full width, sensible height. */
.hoho-page.hoho-page .hoho-prose iframe{display:block;width:100%;height:auto;aspect-ratio:16/9;border:0;border-radius:var(--r-card);} .hoho-page.hoho-page .hoho-prose iframe{display:block;width:100%;height:auto;aspect-ratio:16/9;border:0;border-radius:var(--r-card);}
/* Breadcrumb — same rules as the plugin pages (VC_Display::astra_overrides):
first thing above the page title, in the title's colour; links softer,
current page bold and shortened if long. */
.hoho-page.hoho-page .crumbs{color:inherit;}
.hoho-page.hoho-page .crumbs__list{color:inherit;font-size:.8125rem;font-weight:500;}
.hoho-page.hoho-page .crumbs__list a{opacity:.75;}
.hoho-page.hoho-page .crumbs__list a:hover{opacity:1;}
.hoho-page.hoho-page .crumbs__list [aria-current]{max-width:min(40ch,60vw);overflow:hidden;font-weight:700;text-overflow:ellipsis;white-space:nowrap;}
/* the design paints .hero--page breadcrumbs black; follow the title instead */
.hoho-page.hoho-page .hero .crumbs,.hoho-page.hoho-page .hero .crumbs__list{color:var(--c-white);}
.hoho-page.hoho-page .hero--about .crumbs,.hoho-page.hoho-page .hero--about .crumbs__list{color:var(--c-black);}
.hoho-page.hoho-page .hero__content>.crumbs{margin-block-end:calc(var(--sp-1) * -1);}
/* Blog post: title, breadcrumb and meta sit in the banner like every page. */
.hoho-page.hoho-page .hero--post .hero__title{max-width:none;font-size:clamp(1.75rem,1.2rem + 2vw,3rem);line-height:1.15;}
.hoho-page.hoho-page .hero--post .tmeta{order:3;color:var(--c-white);}
.hoho-page.hoho-page .hero--post .tmeta__rule{background:rgba(255,255,255,.4);}
.hoho-page.hoho-page .hero--post .hero__content{gap:var(--sp-3);}

View File

@@ -23,10 +23,10 @@ add_action( 'wp_enqueue_scripts', function () {
if ( wp_style_is( 'astra-theme-css', 'registered' ) ) { if ( wp_style_is( 'astra-theme-css', 'registered' ) ) {
$deps[] = 'astra-theme-css'; $deps[] = 'astra-theme-css';
} }
wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', $deps, '1.0.15' ); wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', $deps, '1.0.16' );
// Header interactions: burger, submenu, sticky, back-to-top, search toggle. // Header interactions: burger, submenu, sticky, back-to-top, search toggle.
wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.15', true ); wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.16', true );
wp_add_inline_style( wp_add_inline_style(
'hoho-chrome', 'hoho-chrome',

View File

@@ -64,6 +64,35 @@ function hoho_banner_url( $fallback ) {
return $url ? $url : hoho_asset( 'img/' . $fallback . '.webp' ); return $url ? $url : hoho_asset( 'img/' . $fallback . '.webp' );
} }
/**
* Breadcrumb in the same markup as the plugin's vc_breadcrumb_html(), so
* every page shares one style. $trail: list of [label, url]; Home is added.
*/
function hoho_breadcrumb_html( array $trail, $current ) {
$trail = array_merge( array( array( __( 'Home', 'hoponhopoff' ), home_url( '/' ) ) ), $trail );
$out = '<nav class="crumbs" aria-label="' . esc_attr__( 'Breadcrumb', 'hoponhopoff' ) . '"><ol class="crumbs__list">';
foreach ( $trail as $item ) {
$out .= '<li><a href="' . esc_url( $item[1] ) . '">' . esc_html( $item[0] ) . '</a></li>';
}
return $out . '<li aria-current="page">' . esc_html( $current ) . '</li></ol></nav>';
}
/**
* URL of the blog list (the Posts page, else a page slugged blogs/blog).
*/
function hoho_blog_url() {
if ( get_option( 'page_for_posts' ) ) {
return get_permalink( get_option( 'page_for_posts' ) );
}
foreach ( hoho_page_slugs()['blog'] as $slug ) {
$page = get_page_by_path( $slug );
if ( $page ) {
return get_permalink( $page );
}
}
return home_url( '/blogs/' );
}
/** /**
* Social profile links (footer + contact page). Override with the * Social profile links (footer + contact page). Override with the
* `hoho_socials` filter. * `hoho_socials` filter.

View File

@@ -5,7 +5,7 @@ Description: Astra child theme that replaces the Astra header and footer with th
Author: Amir Bagheri Author: Amir Bagheri
Author URI: https://ravisite.com Author URI: https://ravisite.com
Template: astra Template: astra
Version: 1.0.15 Version: 1.0.16
Text Domain: hoponhopoff Text Domain: hoponhopoff
*/ */

View File

@@ -45,7 +45,7 @@ for ( $n = 1; $n <= $hoho_max; $n++ ) {
</div> </div>
<div class="container hero__content"> <div class="container hero__content">
<p class="pill hero__eyebrow"><?php echo esc_html__( 'Tour & Travel Articles', 'hoponhopoff' ); ?></p> <?php echo hoho_breadcrumb_html( array(), __( 'Blog', 'hoponhopoff' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<h1 class="hero__title"><?php echo esc_html__( 'HOHO Weblog', 'hoponhopoff' ); ?></h1> <h1 class="hero__title"><?php echo esc_html__( 'HOHO Weblog', 'hoponhopoff' ); ?></h1>
<form class="filters filters--blog" id="filters" method="get" action="<?php echo esc_url( $hoho_base ); ?>" role="search"> <form class="filters filters--blog" id="filters" method="get" action="<?php echo esc_url( $hoho_base ); ?>" role="search">

View File

@@ -38,7 +38,7 @@ if ( count( $hoho_related ) < 4 ) {
) )
); );
} }
$hoho_blog_url = get_option( 'page_for_posts' ) ? get_permalink( get_option( 'page_for_posts' ) ) : home_url( '/blogs/' ); $hoho_blog_url = hoho_blog_url();
/** /**
* One review card in the design's markup. * One review card in the design's markup.
@@ -63,19 +63,11 @@ $hoho_review = function ( $comment, $classes ) use ( $hoho_post_id ) {
<section class="hero hero--post"> <section class="hero hero--post">
<div class="hero__media"> <div class="hero__media">
<img class="hero__img" src="<?php echo esc_url( hoho_post_image_url( $hoho_post_id, 'full' ) ); ?>" alt="" fetchpriority="high" /> <img class="hero__img" src="<?php echo esc_url( hoho_post_image_url( $hoho_post_id, 'full' ) ); ?>" alt="" fetchpriority="high" />
<div class="hero__scrim" aria-hidden="true"></div>
</div> </div>
<?php <div class="container hero__content">
if ( function_exists( 'vc_ring_badge' ) ) { <?php echo hoho_breadcrumb_html( array( array( __( 'Blog', 'hoponhopoff' ), hoho_blog_url() ) ), get_the_title() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
echo vc_ring_badge( 'SCROLL DOWN · SCROLL DOWN · ', '#post', 'scroll' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped <h1 class="hero__title post__title"><?php the_title(); ?></h1>
}
?>
</section>
<article class="section post" id="post">
<div class="container">
<div class="post__inner">
<header class="tour-head post__head">
<h1 class="post__title"><?php the_title(); ?></h1>
<div class="tmeta"> <div class="tmeta">
<?php if ( $hoho_cats ) : ?> <?php if ( $hoho_cats ) : ?>
<span class="tmeta__item"><?php echo hoho_icon( 'location', 'tmeta__icon' ); // phpcs:ignore ?><?php echo esc_html( $hoho_cats[0]->name ); ?></span> <span class="tmeta__item"><?php echo hoho_icon( 'location', 'tmeta__icon' ); // phpcs:ignore ?><?php echo esc_html( $hoho_cats[0]->name ); ?></span>
@@ -88,8 +80,17 @@ $hoho_review = function ( $comment, $classes ) use ( $hoho_post_id ) {
echo esc_html( sprintf( __( 'from %s', 'hoponhopoff' ), get_the_author() ) ); echo esc_html( sprintf( __( 'from %s', 'hoponhopoff' ), get_the_author() ) );
?></span> ?></span>
</div> </div>
</header> </div>
<?php
if ( function_exists( 'vc_ring_badge' ) ) {
echo vc_ring_badge( 'SCROLL DOWN · SCROLL DOWN · ', '#post', 'scroll' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
</section>
<article class="section post" id="post">
<div class="container">
<div class="post__inner">
<div class="post__body"> <div class="post__body">
<div class="hoho-prose"> <div class="hoho-prose">
<?php the_content(); ?> <?php the_content(); ?>

Binary file not shown.

View File

@@ -12,13 +12,6 @@
color: var(--c-ink); color: var(--c-ink);
} }
.vc-app.vc-app .tour__crumbs li[aria-current] {
overflow: hidden;
max-width: 28ch;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ---------- gallery: one large photo + two, the rest in the lightbox ---------- */ /* ---------- gallery: one large photo + two, the rest in the lightbox ---------- */
.vc-app.vc-app .tgal { .vc-app.vc-app .tgal {
position: relative; position: relative;
@@ -162,12 +155,6 @@
display: none; display: none;
} }
/* the tour name is the page title right below — keep the trail short */
.vc-app.vc-app .tour__crumbs li[aria-current] {
display: none;
}
/* Compact 2 × 2 facts instead of four stacked full-width cards. */ /* Compact 2 × 2 facts instead of four stacked full-width cards. */
.vc-app.vc-app .tour--top .facts { .vc-app.vc-app .tour--top .facts {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));

View File

@@ -266,6 +266,17 @@ class VC_Display {
. '.vc-fullwidth .site-content,.vc-fullwidth .ast-container,' . '.vc-fullwidth .site-content,.vc-fullwidth .ast-container,'
. '.vc-fullwidth .entry-content,.vc-fullwidth .site-main>article' . '.vc-fullwidth .entry-content,.vc-fullwidth .site-main>article'
. '{overflow:visible!important;}', . '{overflow:visible!important;}',
// Breadcrumb — one style on every page (the theme's design pages use
// the same rules): takes the colour of the title it sits above,
// links softer, current page bold and shortened if long.
'.vc-app.vc-app .crumbs{color:inherit;}',
'.vc-app.vc-app .crumbs__list{color:inherit;font-size:.8125rem;font-weight:500;}',
'.vc-app.vc-app .crumbs__list a{opacity:.75;}',
'.vc-app.vc-app .crumbs__list a:hover{opacity:1;}',
'.vc-app.vc-app .crumbs__list [aria-current]{max-width:min(40ch,60vw);overflow:hidden;font-weight:700;text-overflow:ellipsis;white-space:nowrap;}',
'.vc-app.vc-app .hero__content>.crumbs{margin-block-end:calc(var(--sp-1) * -1);}',
// In a banner the title is white, so the breadcrumb is too.
'.vc-app.vc-app .hero .crumbs{color:var(--c-white);}',
// FAQ question buttons must stay flat — Astra gives <button> a shadow. // FAQ question buttons must stay flat — Astra gives <button> a shadow.
'.vc-app.vc-app .acc__q{box-shadow:none!important;}', '.vc-app.vc-app .acc__q{box-shadow:none!important;}',
// Astra colours every heading on the element itself (h1–h6 and // Astra colours every heading on the element itself (h1–h6 and

View File

@@ -617,6 +617,64 @@ function vc_icon( $name, $extra_class = '' ) {
return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>'; return '<svg class="' . esc_attr( $class ) . '" aria-hidden="true"><use href="#' . esc_attr( $name ) . '"></use></svg>';
} }
/**
* Breadcrumb trail for the current Viator view: list of [label, url] links
* (Home first); the current page itself is not included.
* tours archive Home
* region / city Home › Region (for a city)
* tag / category Home › Tours
* single tour Home › Region › City (its canonical city)
* other pages Home
*/
function vc_breadcrumb_trail() {
$trail = array( array( __( 'Home', 'viator-collector' ), home_url( '/' ) ) );
$city = null;
if ( is_tax( 'viator_destination' ) ) {
$term = get_queried_object();
if ( $term instanceof WP_Term && $term->parent ) {
$city = $term; // its parent (region) is added below
}
} elseif ( is_tax( array( 'viator_tag', 'viator_category' ) ) ) {
$trail[] = array( __( 'Tours', 'viator-collector' ), get_post_type_archive_link( 'viator_tour' ) );
} elseif ( is_singular( 'viator_tour' ) ) {
$post_id = get_queried_object_id();
$city_slug = class_exists( 'VC_Routing' ) ? VC_Routing::product_city( $post_id ) : '';
$city = $city_slug ? get_term_by( 'slug', $city_slug, 'viator_destination' ) : null;
if ( ! $city ) {
$terms = get_the_terms( $post_id, 'viator_destination' );
$city = ( $terms && ! is_wp_error( $terms ) ) ? $terms[0] : null;
}
}
if ( $city instanceof WP_Term ) {
if ( $city->parent ) {
$region = get_term( $city->parent, 'viator_destination' );
if ( $region instanceof WP_Term && ! is_wp_error( get_term_link( $region ) ) ) {
$trail[] = array( $region->name, get_term_link( $region ) );
}
}
// On a city archive the city is the current page, not a link.
if ( is_singular( 'viator_tour' ) && ! is_wp_error( get_term_link( $city ) ) ) {
$trail[] = array( $city->name, get_term_link( $city ) );
}
}
return apply_filters( 'vc_breadcrumb_trail', $trail );
}
/**
* Breadcrumb markup shared by every page (the theme's design pages print the
* same structure): links, then the current page in bold.
*/
function vc_breadcrumb_html( array $trail, $current, $extra_class = '' ) {
$out = '<nav class="crumbs' . ( $extra_class ? ' ' . esc_attr( $extra_class ) : '' ) . '" aria-label="' . esc_attr__( 'Breadcrumb', 'viator-collector' ) . '"><ol class="crumbs__list">';
foreach ( $trail as $item ) {
$out .= '<li><a href="' . esc_url( $item[1] ) . '">' . esc_html( $item[0] ) . '</a></li>';
}
return $out . '<li aria-current="page">' . esc_html( $current ) . '</li></ol></nav>';
}
/** /**
* Rotating ring badge (the circular "BOOK NOW ·" anchor in the design). * Rotating ring badge (the circular "BOOK NOW ·" anchor in the design).
* *

View File

@@ -87,7 +87,12 @@ $vc_hero_img = vc_hero_image_source();
</div> </div>
<div class="container hero__content"> <div class="container hero__content">
<p class="pill hero__eyebrow"><?php echo esc_html__( 'Tour & Travel', 'viator-collector' ); ?></p> <?php
// Short current-page label: "Tours" on the archive, the page's own
// title on a page hosting [viator_listing], else the heading.
$vc_crumb_current = ! empty( $vc_crumb_label ) ? $vc_crumb_label : ( is_page() ? get_the_title() : $vc_title );
echo vc_breadcrumb_html( vc_breadcrumb_trail(), $vc_crumb_current ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
<h1 class="hero__title"><?php echo esc_html( $vc_title ); ?></h1> <h1 class="hero__title"><?php echo esc_html( $vc_title ); ?></h1>
<form class="filters" id="vc-filter-form" method="get" action="<?php echo esc_url( $vc_action ); ?>" novalidate> <form class="filters" id="vc-filter-form" method="get" action="<?php echo esc_url( $vc_action ); ?>" novalidate>

View File

@@ -38,37 +38,13 @@ while ( have_posts() ) :
$lead = has_post_thumbnail( $post_id ) ? (string) get_the_post_thumbnail_url( $post_id, 'large' ) : $primary; $lead = has_post_thumbnail( $post_id ) ? (string) get_the_post_thumbnail_url( $post_id, 'large' ) : $primary;
$all = array_values( array_unique( array_filter( array_merge( array( $lead, $primary ), $gallery ) ) ) ); $all = array_values( array_unique( array_filter( array_merge( array( $lead, $primary ), $gallery ) ) ) );
// Breadcrumb: Home › Region › City › Tour (the tour's canonical city).
$crumbs = array( array( __( 'Home', 'viator-collector' ), home_url( '/' ) ) );
$city_slug = class_exists( 'VC_Routing' ) ? VC_Routing::product_city( $post_id ) : '';
$city = $city_slug ? get_term_by( 'slug', $city_slug, 'viator_destination' ) : null;
if ( ! $city && $dest_terms && ! is_wp_error( $dest_terms ) ) {
$city = $dest_terms[0];
}
if ( $city && $city->parent ) {
$region = get_term( $city->parent, 'viator_destination' );
if ( $region instanceof WP_Term && ! is_wp_error( get_term_link( $region ) ) ) {
$crumbs[] = array( $region->name, get_term_link( $region ) );
}
}
if ( $city && ! is_wp_error( get_term_link( $city ) ) ) {
$crumbs[] = array( $city->name, get_term_link( $city ) );
}
$book_href = $booking_url ? $booking_url : '#'; $book_href = $booking_url ? $booking_url : '#';
$book_attrs = $booking_url ? ' target="_blank" rel="nofollow noopener"' : ''; $book_attrs = $booking_url ? ' target="_blank" rel="nofollow noopener"' : '';
?> ?>
<div class="vc-app vc-single"> <div class="vc-app vc-single">
<section class="section tour tour--top"> <section class="section tour tour--top">
<div class="container"> <div class="container">
<nav class="crumbs tour__crumbs" aria-label="<?php echo esc_attr__( 'Breadcrumb', 'viator-collector' ); ?>"> <?php echo vc_breadcrumb_html( vc_breadcrumb_trail(), get_the_title(), 'tour__crumbs' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<ol class="crumbs__list">
<?php foreach ( $crumbs as $crumb ) : ?>
<li><a href="<?php echo esc_url( $crumb[1] ); ?>"><?php echo esc_html( $crumb[0] ); ?></a></li>
<?php endforeach; ?>
<li aria-current="page"><?php the_title(); ?></li>
</ol>
</nav>
</div> </div>
<div class="container tour__layout"> <div class="container tour__layout">
<div class="tour__main"> <div class="tour__main">

View File

@@ -4,13 +4,13 @@ defined('ABSPATH') || exit;
/** /**
* Plugin Name: Viator Collector * Plugin Name: Viator Collector
* Description: Import Viator tours using the Viator Partner API and display them with native WordPress post types, templates, and shortcodes. * Description: Import Viator tours using the Viator Partner API and display them with native WordPress post types, templates, and shortcodes.
* Version: 1.9.5 * Version: 1.9.6
* Author: Amir Bagheri * Author: Amir Bagheri
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
* Text Domain: viator-collector * Text Domain: viator-collector
*/ */
define('VC_VERSION', '1.9.5'); define('VC_VERSION', '1.9.6');
define('VC_PLUGIN_FILE', __FILE__); define('VC_PLUGIN_FILE', __FILE__);
define('VC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('VC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('VC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('VC_PLUGIN_URL', plugin_dir_url(__FILE__));