Compare commits

...

5 Commits

Author SHA1 Message Date
25debff066 feat: starter guides for all 15 structure cities
Writes guides for the nine cities with no old page and adds Best time to
visit to all. The import now fills only empty fields and re-runs once, so
sites that already imported get the new values without losing edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:10:01 +03:30
2ded000a27 feat: city guide on city pages (tagline, facts, About, seasons)
Editable per city under Viator tours > Destinations. Shown above the
tours on page 1 of an unfiltered listing only; filtering and paging jump
to the results. Text recovered from the six old /tour-destination/ pages
is imported once into empty guides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:58:54 +03:30
466ea0ac5f 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>
2026-10-29 21:32:09 +03:30
023b998694 fix(theme): blog posts no longer fade out; readable post layout
- Scroll reveal used threshold 0.15: a section taller than ~6.7 screens (a
  long article) never reached it, so after the reveal class hid it, it
  stayed hidden. Reveal as soon as any part enters (threshold 0).
- Post header: full-width title with meta below and a normal gap
- Article: heading sizes per level, photos keep their shape (no crop),
  icon-sized inline SVGs, full-width embeds
- Strip the imported 'Home' breadcrumb line from posts and card excerpts
- Theme 1.0.15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:20:34 +03:30
d89f8c2a0b chore(theme): add theme preview image and link author to ravisite.com
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:06:46 +03:30
24 changed files with 1197 additions and 74 deletions

Binary file not shown.

View File

@@ -35,12 +35,10 @@ body.hoho-fullwidth{overflow-x:hidden;}
/* Post body from the editor, styled like the design's .tour__block text. */
.hoho-page.hoho-page .hoho-prose{display:flex;flex-direction:column;gap:var(--sp-3);}
.hoho-page.hoho-page .hoho-prose>*{margin:0;}
.hoho-page.hoho-page .hoho-prose :is(h2,h3,h4){margin:var(--sp-3) 0 0;font-size:1rem;font-weight:700;}
.hoho-page.hoho-page .hoho-prose :is(p,li){font-size:1rem;font-weight:500;line-height:1.6;opacity:.7;}
.hoho-page.hoho-page .hoho-prose a{color:var(--c-teal);text-decoration:underline;}
.hoho-page.hoho-page .hoho-prose ul{list-style:disc!important;padding-inline-start:1.5em;}
.hoho-page.hoho-page .hoho-prose ol{list-style:decimal!important;padding-inline-start:1.5em;}
.hoho-page.hoho-page .hoho-prose img{width:100%;height:auto;max-height:22.1875rem;border-radius:var(--r-card);object-fit:cover;}
.hoho-page.hoho-page .hoho-prose figure{margin:0;}
.hoho-page.hoho-page .hoho-prose blockquote{margin:0;padding:var(--sp-3) var(--sp-4);border-radius:var(--r-card);background:var(--c-teal-05);}
@@ -63,3 +61,36 @@ body.hoho-fullwidth{overflow-x:hidden;}
/* 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;}
/* 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 h3{margin:var(--sp-3) 0 0;font-size:1.25rem;font-weight:700;line-height:1.3;}
.hoho-page.hoho-page .hoho-prose :is(h4,h5,h6){margin:var(--sp-3) 0 0;font-size:1.0625rem;font-weight:700;line-height:1.35;}
/* Photos keep their own shape (portrait ones were cropped to a strip). */
.hoho-page.hoho-page .hoho-prose img{display:block;width:auto;max-width:100%;height:auto;max-height:36rem;margin-block:var(--sp-2);border-radius:var(--r-card);object-fit:contain;}
/* Unsized inline icons from imported content stay icon-sized. */
.hoho-page.hoho-page .hoho-prose svg{width:1em;height:1em;vertical-align:-.125em;}
/* 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);}
/* 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

@@ -168,7 +168,9 @@
});
obs.unobserve(entry.target);
});
}, { threshold: 0.15, rootMargin: '0px 0px -8% 0px' });
// threshold 0, not a ratio: a section taller than the screen (a long
// article) can never be 15% visible and would stay hidden forever.
}, { threshold: 0, rootMargin: '0px 0px -8% 0px' });
sections.forEach(function (section) { revealObserver.observe(section); });
}

View File

@@ -23,10 +23,10 @@ add_action( 'wp_enqueue_scripts', function () {
if ( wp_style_is( 'astra-theme-css', 'registered' ) ) {
$deps[] = 'astra-theme-css';
}
wp_enqueue_style( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.css', $deps, '1.0.13' );
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.
wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.13', true );
wp_enqueue_script( 'hoho-chrome', get_stylesheet_directory_uri() . '/assets/chrome.js', array(), '1.0.16', true );
wp_add_inline_style(
'hoho-chrome',

View File

@@ -64,6 +64,35 @@ function hoho_banner_url( $fallback ) {
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
* `hoho_socials` filter.
@@ -194,6 +223,21 @@ function hoho_blog_query( array $state ) {
);
}
/**
* Imported posts start with a stray "Home" breadcrumb line (left over from the
* old page builder). Drop it from the article and from card excerpts.
*/
function hoho_strip_leading_home( $text ) {
return preg_replace( '#^\s*(?:<p>\s*Home\s*</p>|Home\b\s*)#i', '', (string) $text );
}
add_filter(
'the_content',
function ( $content ) {
return ( is_singular( 'post' ) && in_the_loop() ) ? hoho_strip_leading_home( $content ) : $content;
},
20
);
/**
* Card date chip, as in the design: relative for the last week, then a date.
*/
@@ -240,7 +284,7 @@ function hoho_article_card( $post, $wrap = 'li' ) {
</a>
</div>
</div>
<p class="acard__excerpt"><?php echo esc_html( wp_trim_words( get_the_excerpt( $post ), 24, '&hellip;' ) ); ?></p>
<p class="acard__excerpt"><?php echo esc_html( wp_trim_words( hoho_strip_leading_home( get_the_excerpt( $post ) ), 24, '&hellip;' ) ); ?></p>
<?php
$inner = ob_get_clean();
if ( 'related' === $wrap ) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@@ -3,8 +3,9 @@ Theme Name: Hop On Hop Off (Astra Child)
Theme URI: https://git.kaamyab.dev/vargha/TourWebsite
Description: Astra child theme that replaces the Astra header and footer with the "Hop On Hop Off" design. Pairs with the Viator Collector plugin.
Author: Amir Bagheri
Author URI: https://ravisite.com
Template: astra
Version: 1.0.13
Version: 1.0.16
Text Domain: hoponhopoff
*/

View File

@@ -45,7 +45,7 @@ for ( $n = 1; $n <= $hoho_max; $n++ ) {
</div>
<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>
<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.
@@ -63,6 +63,23 @@ $hoho_review = function ( $comment, $classes ) use ( $hoho_post_id ) {
<section class="hero hero--post">
<div class="hero__media">
<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 class="container hero__content">
<?php echo hoho_breadcrumb_html( array( array( __( 'Blog', 'hoponhopoff' ), hoho_blog_url() ) ), get_the_title() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<h1 class="hero__title post__title"><?php the_title(); ?></h1>
<div class="tmeta">
<?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__rule" aria-hidden="true"></span>
<?php endif; ?>
<span class="tmeta__item"><?php echo hoho_icon( 'calendar', 'tmeta__icon' ); // phpcs:ignore ?><?php echo esc_html( get_the_date() ); ?></span>
<span class="tmeta__rule" aria-hidden="true"></span>
<span class="tmeta__item"><?php echo hoho_icon( 'person', 'tmeta__icon' ); // phpcs:ignore ?><?php
/* translators: %s: author name. */
echo esc_html( sprintf( __( 'from %s', 'hoponhopoff' ), get_the_author() ) );
?></span>
</div>
</div>
<?php
if ( function_exists( 'vc_ring_badge' ) ) {
@@ -74,22 +91,6 @@ $hoho_review = function ( $comment, $classes ) use ( $hoho_post_id ) {
<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">
<?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__rule" aria-hidden="true"></span>
<?php endif; ?>
<span class="tmeta__item"><?php echo hoho_icon( 'calendar', 'tmeta__icon' ); // phpcs:ignore ?><?php echo esc_html( get_the_date() ); ?></span>
<span class="tmeta__rule" aria-hidden="true"></span>
<span class="tmeta__item"><?php echo hoho_icon( 'person', 'tmeta__icon' ); // phpcs:ignore ?><?php
/* translators: %s: author name. */
echo esc_html( sprintf( __( 'from %s', 'hoponhopoff' ), get_the_author() ) );
?></span>
</div>
</header>
<div class="post__body">
<div class="hoho-prose">
<?php the_content(); ?>

Binary file not shown.

View File

@@ -0,0 +1,270 @@
/* City guide on a destination page (VC_City_Guide): tagline in the banner,
facts bar, About and Seasons above the tours. */
.vc-app.vc-app .hero__tagline {
order: 2;
max-width: 40ch;
margin: calc(var(--sp-2) * -1) 0 0;
color: var(--c-white);
font-size: clamp(1.05rem, 0.9rem + 0.6vw, 1.375rem);
font-weight: 500;
line-height: 1.35;
opacity: 0.92;
}
/* ---------- facts bar ---------- */
.vc-app.vc-app .cty-top {
margin-block-start: var(--sp-5);
}
.vc-app.vc-app .cty-facts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
margin: 0;
padding: var(--sp-3) var(--sp-2);
list-style: none;
background: var(--c-white);
border-radius: var(--r-card);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08), 0 0 0 1px var(--c-line);
}
.vc-app.vc-app .cfact {
display: flex;
align-items: center;
gap: 0.8rem;
margin: 0;
padding: 0.25rem 1rem;
}
.vc-app.vc-app .cfact + .cfact {
border-inline-start: 1px solid var(--c-line);
}
.vc-app.vc-app .cfact__ic {
display: grid;
flex: none;
place-items: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
color: #0f7f90;
background: var(--c-teal-20);
}
.vc-app.vc-app .cfact__ic svg {
width: 1.35rem;
height: 1.35rem;
}
.vc-app.vc-app .cfact small {
display: block;
color: var(--c-muted);
font-size: 0.8rem;
font-weight: 500;
}
.vc-app.vc-app .cfact b {
display: block;
color: var(--c-ink);
font-size: 1rem;
font-weight: 700;
}
/* ---------- shared headings ---------- */
.vc-app.vc-app .cty-eyebrow {
margin: 0 0 var(--sp-2);
}
.vc-app.vc-app .cty-h2 {
margin: 0 0 var(--sp-3);
color: var(--c-ink);
font-size: var(--fs-h2);
font-weight: 800;
line-height: 1.15;
}
.vc-app.vc-app .cty-tours-h {
margin-block-end: var(--sp-4);
}
/* ---------- about ---------- */
.vc-app.vc-app .cty-about__grid {
display: grid;
grid-template-columns: 5fr 6fr;
gap: var(--sp-6);
align-items: center;
}
.vc-app.vc-app .cty-about__grid--text {
grid-template-columns: minmax(0, 48rem);
}
.vc-app.vc-app .cty-about__img {
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
object-fit: cover;
border-radius: var(--r-xl);
}
.vc-app.vc-app .cty-about__text {
position: relative;
color: rgba(0, 0, 0, 0.72);
line-height: 1.7;
}
.vc-app.vc-app .cty-about__text p {
margin: 0 0 0.8rem;
}
.vc-app.vc-app .cty-about__text.is-clamped {
max-height: 11.5rem;
overflow: hidden;
}
.vc-app.vc-app .cty-about__text.is-clamped::after {
content: "";
position: absolute;
inset: auto 0 0;
height: 4rem;
background: linear-gradient(rgba(255, 255, 255, 0), var(--c-white));
}
.vc-app.vc-app .cty-more {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-block-start: var(--sp-2);
padding: 0;
border: 0;
color: var(--c-ink);
background: none;
font-weight: 700;
text-decoration: underline;
text-underline-offset: 3px;
cursor: pointer;
}
.vc-app.vc-app .cty-more[hidden] {
display: none;
}
.vc-app.vc-app .cty-more[aria-expanded="true"] .icon {
transform: rotate(180deg);
}
/* ---------- seasons ---------- */
.vc-app.vc-app .cty-seasons {
padding-block-start: 0;
}
.vc-app.vc-app .seasons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: var(--gap);
margin: var(--sp-4) 0 0;
padding: 0;
list-style: none;
}
.vc-app.vc-app .season {
--tint: #e9f7f9;
--ink: #0f7f90;
margin: 0;
padding: var(--sp-4);
border-radius: var(--r-card);
background: var(--tint);
}
.vc-app.vc-app .season--summer { --tint: #fff8db; --ink: #a37b00; }
.vc-app.vc-app .season--fall { --tint: #fdeee6; --ink: #c2551b; }
.vc-app.vc-app .season--winter { --tint: #eef1fb; --ink: #4a64c9; }
.vc-app.vc-app .season__ic {
display: grid;
place-items: center;
width: 3rem;
height: 3rem;
margin-block-end: var(--sp-3);
border-radius: 50%;
color: var(--ink);
background: var(--c-white);
}
.vc-app.vc-app .season__ic svg {
width: 1.5rem;
height: 1.5rem;
}
.vc-app.vc-app .season h3 {
margin: 0;
color: var(--c-ink);
font-size: 1.25rem;
font-weight: 800;
}
.vc-app.vc-app .season small {
display: block;
margin: 0.15rem 0 0.7rem;
color: var(--ink);
font-size: 0.82rem;
font-weight: 700;
}
.vc-app.vc-app .season p {
margin: 0;
color: rgba(0, 0, 0, 0.7);
font-size: 0.95rem;
line-height: 1.6;
}
/* The tours follow the guide directly: no double section gap. */
.vc-app.vc-app .cty-seasons + #tour-list,
.vc-app.vc-app .cty-about + #tour-list {
padding-block-start: 0;
}
@media (max-width: 48rem) {
.vc-app.vc-app .cty-facts {
grid-template-columns: 1fr 1fr;
row-gap: var(--sp-3);
padding-inline: 0.25rem;
}
.vc-app.vc-app .cfact {
padding-inline: 0.6rem;
}
.vc-app.vc-app .cfact:nth-child(odd) {
border-inline-start: 0;
}
.vc-app.vc-app .cfact__ic {
width: 2.25rem;
height: 2.25rem;
}
.vc-app.vc-app .cfact b {
font-size: 0.875rem;
}
.vc-app.vc-app .cty-about__grid {
grid-template-columns: 1fr;
gap: var(--sp-4);
}
/* Seasons swipe sideways. */
.vc-app.vc-app .seasons {
display: flex;
margin-inline: calc(var(--gutter) * -1);
padding-inline: var(--gutter);
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
}
.vc-app.vc-app .season {
flex: 0 0 78%;
scroll-snap-align: start;
}
}

View File

@@ -12,13 +12,6 @@
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 ---------- */
.vc-app.vc-app .tgal {
position: relative;
@@ -162,12 +155,6 @@
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. */
.vc-app.vc-app .tour--top .facts {
grid-template-columns: repeat(2, minmax(0, 1fr));

View File

@@ -86,6 +86,22 @@
});
});
/* ---------- city guide: long About text folds behind "Read more" ---------- */
Array.prototype.forEach.call(app.querySelectorAll("[data-vc-clamp]"), function (text) {
var btn = text.nextElementSibling;
text.classList.add("is-clamped");
if (!btn || text.scrollHeight <= text.clientHeight + 8) {
text.classList.remove("is-clamped");
return;
}
btn.hidden = false;
btn.addEventListener("click", function () {
var open = text.classList.toggle("is-clamped") === false;
btn.setAttribute("aria-expanded", open ? "true" : "false");
btn.querySelector("span").textContent = open ? "Read less" : "Read more";
});
});
/* ---------- single tour: "view all photos" scrolls the gallery ---------- */
var viewAll = app.querySelector(".tg-viewall, .gallery__viewall");
if (viewAll) viewAll.addEventListener("click", function () {

View File

@@ -0,0 +1,236 @@
<?php
/**
* Starter city guides, imported once into empty guides (see
* VC_City_Guide::import_bundled()). The first six were recovered from the old
* /tour-destination/{city}/ pages (Wayback Machine, 14 Aug 2026); the rest
* were written for the site.
*/
defined( 'ABSPATH' ) || exit;
return array (
'new-york-city' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-5',
'best_time' => 'April – June',
'tagline' => 'NYC is the best ever!',
'about_title' => 'New York’s Past: A Quick Look',
'about' => '<p>New York began with the Lenape people on its islands and rivers. Dutch settlers arrived in the 1600s, building a trade hub. Today, it’s a global city famed for finance, art, and vibrant culture. Historic brownstones stand alongside gleaming skyscrapers, blending old and new. From iconic landmarks to diverse neighborhoods, New York offers a dynamic mix of history and energy, making it an exciting destination for visitors to explore and enjoy.</p>
<p>New York City’s hop-on hop-off bus tour, bookable at hoponhopofftickets.com , is a dynamic way to explore with operators like Big Bus Tours. Covering 15 stops, including Times Square, Empire State Building, and Central Park, buses run every 10-15 minutes with a 90-minute loop. Attracting over 60 million annual visitors, it offers multilingual audio guides and wheelchair-accessible buses, though upper decks have restrictions. A downtown loop and night tour showcase Manhattan’s iconic skyline. Pets (except service dogs) are not allowed. Book at hoponhopofftickets.com for an unforgettable NYC adventure.</p>',
'spring' => 'Stroll Central Park for vibrant blooming trees or photograph the iconic Brooklyn Bridge. Enjoy a classic bagel at Ess-a-Bagel or catch a Yankees game at the lively stadium.',
'summer' => 'Relax at Coney Island’s beach or bike the Hudson River’s scenic path. Savor street food at Smorgasburg or enjoy free summer concerts in vibrant Central Park.',
'fall' => 'Wander the High Line for stunning fall colors or explore Greenwich Village’s charming streets. Sip coffee in trendy SoHo or attend the New York Film Festival.',
'winter' => 'Skate at Rockefeller Center’s rink or admire Dyker Heights’ holiday lights. On chilly days, visit the Met Museum or shop at iconic Fifth Avenue stores.',
),
'new-orleans' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-6',
'best_time' => 'February – May',
'tagline' => 'America’s soulful city',
'about_title' => 'New Orleans Past',
'about' => '<p>New Orleans began with Chitimacha and Choctaw peoples along the Mississippi River. In the 1700s, French settlers established a vibrant port town thriving on music and trade. Today, it’s a cultural gem celebrated for jazz, Creole cuisine, and festive parties. Historic French Quarter buildings stand beside lively bars, blending old-world charm with modern energy. From iconic festivals to soulful streets, New Orleans offers a unique mix of history and revelry, making it an exciting destination to explore.</p>
<p>New Orleans’ hop-on hop-off bus tour, bookable at hoponhopofftickets.com, is a lively way to explore the Crescent City with City Sightseeing. Covering 19 stops, including the French Quarter, Garden District, and Jackson Square, buses run every 30-45 minutes with a 90-minute loop. Popular with over 17 million annual visitors, it offers live or audio-guided commentary in English, with wheelchair-accessible buses (lower deck only). A free self-guided walking tour of the French Quarter is included. Pets (except service dogs) are not allowed. Book at hoponhopofftickets.com for a vibrant Big Easy adventure.</p>',
'spring' => 'Walk the French Quarter for vibrant street music or snap photos at Jackson Square. Savor beignets at Café du Monde or catch a Mardi Gras parade with colorful floats.',
'summer' => 'Enjoy a Mississippi riverboat ride or savor gumbo in the Garden District. Experience live music at Essence Festival or cool off with sno-balls at a local stand.',
'fall' => 'Wander City Park for autumn vibes or join a spooky ghost tour. Sip cocktails on lively Bourbon Street or enjoy music and art at Voodoo Fest.',
'winter' => 'Admire holiday lights in the French Quarter or enjoy warm beignets at Café du Monde. On mild days, bike along the Mississippi River or shop at the vibrant French Market.',
),
'miami' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-5',
'best_time' => 'December – April',
'tagline' => 'Where is America’s tropical star?',
'about_title' => 'Miami’s Past',
'about' => '<p>Miami began with the Tequesta people along its ocean and rivers. In the 1800s, it emerged as a trading hub, later booming with tourism and vibrant Latin culture. Today, it’s a global city renowned for pristine beaches, dynamic art, and pulsating music. Historic Art Deco buildings stand beside sleek modern clubs, blending retro charm with contemporary energy. From colorful neighborhoods to iconic shorelines, Miami offers a lively mix of culture and history, making it an exciting destination to explore.</p>
<p>Miami’s hop-on hop-off bus tour, bookable at hoponhopofftickets.com, offers a lively way to explore with Big Bus Tours. Covering 11 stops, including South Beach, Wynwood Walls, and Little Havana, buses run every 30 minutes with a 90-minute loop. Popular with over 8 million annual visitors to Miami, it features multilingual audio guides and wheelchair-accessible buses, though upper decks have restrictions. A night tour highlights Miami’s vibrant skyline. Pets (except service dogs) are not allowed. Book at hoponhopofftickets.com for a colorful city adventure.</p>',
'spring' => 'Stroll South Beach for sun and vibrant people-watching or photograph Wynwood Walls’ colorful street art. Kayak in scenic Biscayne Bay or join Spring Break parties for lively vibes.',
'summer' => 'Swim at Key Biscayne’s beaches or sail around Biscayne Bay. Enjoy ceviche in vibrant Little Havana or dance to live music at the Ultra Music Festival.',
'fall' => 'Wander Ocean Drive for vibrant neon lights or explore Everglades National Park for alligator spotting. Sip mojitos in Coconut Grove or browse the Miami Book Fair for exciting reads.',
'winter' => 'Enjoy Miami Beach’s festive holiday lights or dance at a lively salsa club. On sunny days, bike the scenic Venetian Causeway or shop at Lincoln Road’s vibrant outdoor mall.',
),
'las-vegas' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-8',
'best_time' => 'March – May',
'tagline' => 'America’s fun desert',
'about_title' => 'Las Vegas’s Past: A Quick Look',
'about' => '<p>Las Vegas began with the Paiute people in desert oases. In the 1900s, it grew as a railway hub, later booming with casinos and vibrant shows. Today, it’s the world’s entertainment capital, famed for dazzling glitz, high-stakes gambling, and grand dreams. Retro neon signs stand beside sleek modern resorts, blending nostalgia with extravagance. From iconic Strip landmarks to electrifying nightlife, Las Vegas offers a thrilling mix of history and spectacle, making it an exciting destination to explore.</p>
<p>Las Vegas’ hop-on hop-off bus tour, bookable at hoponhopofftickets.com, is a vibrant way to explore Sin City with Big Bus Tours. The Red Route covers 9 stops, including the Welcome to Fabulous Las Vegas Sign, Bellagio Fountains, and Treasure Island, with buses every 30-60 minutes and a 2.5-hour loop. Attracting over 41 million annual visitors, it offers pre-recorded English and Spanish commentary, wheelchair-accessible lower decks, and a 2-hour night tour showcasing the Strip’s neon lights. Pets (except service dogs) are not allowed, and upper decks restrict mobility-impaired guests for safety. Book at hoponhopofftickets.com.</p>',
'spring' => 'Stroll the Strip for dazzling neon lights or photograph the iconic “Welcome to Las Vegas” sign. Hike Red Rock Canyon for stunning desert views or join a lively pool party at the Cosmopolitan.',
'summer' => 'Relax at Mandalay Bay’s beach pool or enjoy the Blue Man Group show. Savor tacos at Fremont Street’s food stalls or explore the Neon Museum’s glowing displays at night.',
'fall' => 'Wander Valley of Fire’s striking red rocks or watch Bellagio’s mesmerizing fountain show. Sip cocktails at a chic rooftop bar or enjoy the Life is Beautiful music and art festival.',
'winter' => 'Admire holiday lights at Ethel M Cactus Garden or enjoy a buffet at Caesars Palace. On mild days, golf at a nearby course or shop at the Grand Canal Shoppes.',
),
'montreal' =>
array (
'language' => 'French',
'currency' => 'Canadian Dollar (CAD)',
'timezone' => 'GMT-5',
'best_time' => 'June – September',
'tagline' => 'Enjoy Canada’s artsy star!',
'about_title' => 'Montreal’s Past',
'about' => '<p>Montreal began with the Kanien’kehá:ka thriving along the St. Lawrence River. In the 1600s, French settlers established a town that became a bustling trade hub. Today, as Quebec’s cultural heart, it blends French heritage with global influences. Historic stone buildings stand alongside vibrant modern art, with music filling the air. From lively festivals to charming cobblestone streets, Montreal’s unique fusion of old and new creates a dynamic, inviting atmosphere perfect for exploration and discovery.</p>
<p>Montreal’s hop-on hop-off bus tour, bookable at hoponhopofftickets.com, is a convenient way to explore Quebec’s vibrant metropolis with Gray Line Montreal. The double-decker buses cover 10 stops, including Notre-Dame Basilica, Old Port, Chinatown, and Mount Royal Park, with a 2-hour loop and departures every 15-30 minutes seasonally. Popular with over 7 million annual visitors to the Old Port alone, the tour offers bilingual (English/French) live commentary, though some reviews note inconsistent guide quality. Wheelchair access is limited, and strollers may face restrictions. Book at hoponhopofftickets.com for a flexible city adventure.</p>',
'spring' => 'Stroll Mount Royal for lush views and skyline shots. Savor a bagel at St-Viateur or wander Old Montreal’s historic streets. Bike the Lachine Canal for a scenic spring adventure.',
'summer' => 'Relax at Parc Jean-Drapeau for festivals like Osheaga or picnic by the St. Lawrence River. Enjoy street food at La Ronde or catch live music at the Montreal Jazz Festival.',
'fall' => 'Wander Parc La Fontaine for vibrant fall foliage or stroll the Plateau’s colorful streets. Sip coffee at a Mile End café or enjoy films at the Montreal International Documentary Festival.',
'winter' => 'Skate at Old Port’s rink or sip hot chocolate at Igloofest’s music fest. On snowy days, explore the Montreal Museum of Fine Arts or shop in the Underground City. ',
),
'vancouver' =>
array (
'language' => 'English',
'currency' => 'Canadian Dollar (CAD)',
'timezone' => 'GMT-8',
'best_time' => 'June – September',
'tagline' => 'Vancouver’s got the magic.',
'about_title' => 'Vancouver’s Story: A Quick Dive into Its Cool Past',
'about' => '<p>Vancouver blends chill and vibrant energy, rooted in Coast Salish traditions of thriving on fish and forests. The 1800s gold rush and railway transformed Gastown into a bustling hub. Now, as “Hollywood North,” it buzzes with film crews, diverse global cuisine, and eco-conscious style. Neon-lit nights and Indigenous art create a dynamic cultural mosaic. From Stanley Park’s greenery to urban energy, Vancouver invites exploration with its unique mix of nature, culture, and modern flair, perfect for diving in.</p>',
),
/*
* Written for the site (no old page existed for these cities). Review
* before relying on them; they are only imported into empty guides.
*/
'boston' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-5',
'best_time' => 'May – October',
'tagline' => 'Where America’s story began.',
'about_title' => 'Boston’s Past: A Quick Look',
'about' => '<p>Boston began on the Shawmut Peninsula, home to the Massachusett people, before Puritan settlers founded the town in 1630. It became the spark of the American Revolution, from the Boston Tea Party to Paul Revere’s midnight ride. Today it is a city of world-famous universities, historic brick streets and a lively waterfront, where colonial meeting houses sit beside glass towers.</p>
<p>A hop-on hop-off tour, bookable at hoponhopofftickets.com, is an easy way to see it all: follow the Freedom Trail past Faneuil Hall and the Old State House, cross into Cambridge for Harvard and MIT, and hop off for Beacon Hill, the North End and the Seaport. Book at hoponhopofftickets.com and explore Boston at your own pace.</p>',
'spring' => 'Watch the Public Garden bloom and ride the Swan Boats, or cheer on runners at the Boston Marathon in April. Catch an early-season Red Sox game at Fenway Park.',
'summer' => 'Walk the Freedom Trail in the sunshine, take a harbor cruise to the Boston Harbor Islands or join a whale-watching trip. Enjoy cannoli in the North End and free concerts on the Esplanade.',
'fall' => 'See New England’s famous fall colors on a day trip, stroll Harvard Yard among the leaves or visit Salem for its October festivities. Warm up with clam chowder at Quincy Market.',
'winter' => 'Skate on the Frog Pond in Boston Common and admire the holiday lights. On cold days, explore the Museum of Fine Arts or the Isabella Stewart Gardner Museum.',
),
'chicago' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-6',
'best_time' => 'May – September',
'tagline' => 'The Windy City of big skylines.',
'about_title' => 'Chicago’s Past: A Quick Look',
'about' => '<p>Chicago grew from a trading post where the Chicago River meets Lake Michigan, land long home to the Potawatomi people. After the Great Fire of 1871, the city rebuilt itself with daring new buildings and gave the world the skyscraper. Today it is famous for bold architecture, blues and jazz, deep-dish pizza and a lakefront lined with beaches and parks.</p>
<p>A hop-on hop-off tour, bookable at hoponhopofftickets.com, links the highlights: the Magnificent Mile, Millennium Park and “The Bean,” Willis Tower, Navy Pier and the Museum Campus. Pair it with an architecture river cruise for the best views of the skyline. Book at hoponhopofftickets.com for a classic Chicago day out.</p>',
'spring' => 'See the river dyed green for St. Patrick’s Day and the tulips along Michigan Avenue. Catch a Cubs game at historic Wrigley Field as the season opens.',
'summer' => 'Take an architecture boat tour on the Chicago River, relax on North Avenue Beach or ride the Ferris wheel at Navy Pier. Join outdoor festivals and free concerts in Millennium Park.',
'fall' => 'Walk Lincoln Park under golden trees, or watch the Chicago Marathon in October. Spend an afternoon at the Art Institute and finish with deep-dish pizza.',
'winter' => 'Skate at the Maggie Daley Park ribbon and browse the German-style Christkindlmarket. Admire the holiday lights on the Magnificent Mile, then warm up in a cozy jazz club.',
),
'los-angeles' =>
array (
'language' => 'English, Spanish',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-8',
'best_time' => 'March – May',
'tagline' => 'Where the stars come out to play.',
'about_title' => 'Los Angeles’ Past: A Quick Look',
'about' => '<p>Los Angeles began as the home of the Tongva people and was founded as a small Spanish pueblo in 1781. Oil, oranges and railroads brought rapid growth, and in the early 1900s Hollywood turned it into the movie capital of the world. Today it is a sprawling, sunny city of beaches, film studios, palm-lined boulevards and neighborhoods from every corner of the globe.</p>
<p>Because the city is so spread out, a hop-on hop-off tour, bookable at hoponhopofftickets.com, is one of the easiest ways to see it: the Hollywood Walk of Fame, the TCL Chinese Theatre, Beverly Hills and Rodeo Drive, with routes out to Santa Monica and the coast. Book at hoponhopofftickets.com and leave the driving to someone else.</p>',
'spring' => 'Hike Griffith Park for views of the Hollywood Sign and the city, or watch wildflowers bloom in the nearby hills. Stroll Olvera Street to see where the city began.',
'summer' => 'Spend a day at Santa Monica Pier or the boardwalk in Venice Beach. Catch an outdoor movie or a concert at the Hollywood Bowl on a warm evening.',
'fall' => 'Enjoy warm, clear days on Malibu’s beaches, or tour a working film studio. Visit the Getty Center for art, gardens and sunset views.',
'winter' => 'Expect mild sunshine for walking the Hollywood Walk of Fame and shopping on Rodeo Drive. Spot migrating gray whales on a boat trip from the coast.',
),
'san-diego' =>
array (
'language' => 'English, Spanish',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-8',
'best_time' => 'March – May',
'tagline' => 'California’s sunny sweet spot.',
'about_title' => 'San Diego’s Past: A Quick Look',
'about' => '<p>San Diego was home to the Kumeyaay people for thousands of years before Spain built its first California mission here in 1769, earning the city the title “Birthplace of California.” A large Navy presence and a mild climate helped it grow into a relaxed coastal city. Today it is known for its beaches, its harbor, Balboa Park’s museums and a laid-back, outdoor way of life.</p>
<p>A hop-on hop-off tour, bookable at hoponhopofftickets.com, connects the best of it: the Gaslamp Quarter, Old Town, Balboa Park, Little Italy, the USS Midway Museum on the harbor and the beach town of Coronado. Book at hoponhopofftickets.com for an easy, sunny day of sightseeing.</p>',
'spring' => 'Wander Balboa Park’s gardens and museums, or visit the world-famous San Diego Zoo. Sample tacos and margaritas in historic Old Town.',
'summer' => 'Swim and surf at La Jolla Shores or Pacific Beach, and watch the sea lions at La Jolla Cove. Enjoy the fair in Del Mar or a sunset on Coronado Beach.',
'fall' => 'Explore the USS Midway Museum and the waterfront while the crowds thin out. Head out for craft beer in North Park or a whale-watching trip.',
'winter' => 'Enjoy mild, sunny days by the bay and the holiday lights along the harbor. Watch gray whales migrate along the coast from Cabrillo National Monument.',
),
'san-francisco' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-8',
'best_time' => 'September – November',
'tagline' => 'The city by the bay.',
'about_title' => 'San Francisco’s Past: A Quick Look',
'about' => '<p>San Francisco was home to the Ohlone people before Spanish settlers built a mission and presidio here in 1776. The 1849 Gold Rush turned a small village into a boomtown almost overnight, and the city rebuilt with flair after the 1906 earthquake. Today it is famous for its steep hills, cable cars, painted Victorian houses, foggy mornings and creative, open-minded spirit.</p>
<p>A hop-on hop-off tour, bookable at hoponhopofftickets.com, takes the strain out of the hills: ride across the Golden Gate Bridge, then hop off at Fisherman’s Wharf, Union Square, Chinatown, Haight-Ashbury and Golden Gate Park. Add a ferry to Alcatraz for the full story. Book at hoponhopofftickets.com to see the city by the bay.</p>',
'spring' => 'Walk Golden Gate Park’s gardens or watch the Japanese Tea Garden in bloom. Hop on a cable car up to Nob Hill for views over the bay.',
'summer' => 'Bring a jacket for foggy days on a bay cruise or a walk across the Golden Gate Bridge. Spend a sunny afternoon in the Mission District and Dolores Park.',
'fall' => 'Enjoy the city’s warmest, clearest weather and outdoor festivals. Take a day trip to the vineyards of Napa or Sonoma for the harvest season.',
'winter' => 'Skate in Union Square and see the holiday window displays. Watch the sea lions at Pier 39 or tour Alcatraz on a crisp, clear day.',
),
'seattle' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-8',
'best_time' => 'June – September',
'tagline' => 'Coffee, mountains and the Space Needle.',
'about_title' => 'Seattle’s Past: A Quick Look',
'about' => '<p>Seattle takes its name from Chief Si’ahl of the Duwamish and Suquamish peoples, who lived on these shores long before settlers arrived in 1851. Logging, the Klondike Gold Rush and later aircraft and tech companies drove the city’s growth. Today it is a green, creative city set between Puget Sound and the mountains, famous for coffee, music and the Space Needle.</p>
<p>A hop-on hop-off tour, bookable at hoponhopofftickets.com, is an easy way to see the highlights: Pike Place Market, the waterfront, Pioneer Square and the Seattle Center with the Space Needle and Chihuly Garden and Glass. Book at hoponhopofftickets.com and add a harbor cruise for views of Mount Rainier on a clear day.</p>',
'spring' => 'See the tulip fields of the Skagit Valley in bloom on a day trip. Browse Pike Place Market and watch the famous fish throwers at work.',
'summer' => 'Enjoy long, dry days on a Puget Sound ferry or kayaking on Lake Union. Take in the view from the top of the Space Needle and dine on the waterfront.',
'fall' => 'Walk the Washington Park Arboretum for autumn colors, or explore the Museum of Pop Culture. Warm up with coffee in the city that made it famous.',
'winter' => 'Explore Chihuly Garden and Glass and the city’s museums on rainy days. Take a day trip to the snowy Cascade Mountains for skiing or snowshoeing.',
),
'toronto' =>
array (
'language' => 'English',
'currency' => 'Canadian Dollar (CAD)',
'timezone' => 'GMT-5',
'best_time' => 'May – September',
'tagline' => 'Canada’s big city by the lake.',
'about_title' => 'Toronto’s Past: A Quick Look',
'about' => '<p>Toronto sits on land long home to the Anishinaabe, Haudenosaunee and Wendat peoples; its name is thought to come from a word meaning “where there are trees standing in the water.” The British founded the town of York here in 1793, and it grew into Canada’s largest city. Today Toronto is one of the most diverse cities in the world, known for its skyline, neighborhoods, food and arts.</p>
<p>A hop-on hop-off tour, bookable at hoponhopofftickets.com, covers the highlights: the CN Tower, the Harbourfront, the Distillery District, St. Lawrence Market, Chinatown and Kensington Market, and the Royal Ontario Museum. Book at hoponhopofftickets.com and save a day for a trip to Niagara Falls.</p>',
'spring' => 'Watch the cherry blossoms bloom in High Park and stroll the Distillery District’s cobbled lanes. Catch a Blue Jays game as the baseball season starts.',
'summer' => 'Take a ferry to the Toronto Islands for beaches and skyline views. Join street festivals, patios and outdoor concerts all over the city.',
'fall' => 'See the city at its most glamorous during the Toronto International Film Festival. Enjoy autumn colors along the Don Valley trails or on a day trip to Niagara Falls.',
'winter' => 'Skate at Nathan Phillips Square under the lights and browse the Distillery Winter Village. Stay warm in the Royal Ontario Museum or the Art Gallery of Ontario.',
),
'victoria' =>
array (
'language' => 'English',
'currency' => 'Canadian Dollar (CAD)',
'timezone' => 'GMT-8',
'best_time' => 'May – September',
'tagline' => 'Canada’s garden city.',
'about_title' => 'Victoria’s Past: A Quick Look',
'about' => '<p>Victoria lies on the traditional territory of the Lekwungen-speaking Songhees and Esquimalt peoples. The Hudson’s Bay Company built a fort here in 1843, and the Fraser Canyon Gold Rush soon turned it into a busy port. Today the capital of British Columbia is a charming harbor city with British-style architecture, colorful gardens and a gentle, mild climate.</p>
<p>A hop-on hop-off tour, bookable at hoponhopofftickets.com, is a relaxed way to explore: the Inner Harbour and the BC Parliament Buildings, the Fairmont Empress, Chinatown, Craigdarroch Castle and the scenic waterfront drive. Book at hoponhopofftickets.com and make time for the world-famous Butchart Gardens.</p>',
'spring' => 'See Butchart Gardens burst into color with tulips and cherry blossoms. Walk the Inner Harbour as the first whale-watching trips set out.',
'summer' => 'Go whale watching for orcas and humpbacks, or kayak along the coast. Enjoy afternoon tea at the Fairmont Empress and street performers on the harbor.',
'fall' => 'Stroll Beacon Hill Park among the autumn leaves and visit the Royal BC Museum. Sample local cider and seafood at the Fisherman’s Wharf.',
'winter' => 'Enjoy one of Canada’s mildest winters, with the Parliament Buildings lit up at night. See Butchart Gardens’ holiday lights and explore historic Craigdarroch Castle.',
),
'washington-dc' =>
array (
'language' => 'English',
'currency' => 'US Dollar (USD)',
'timezone' => 'GMT-5',
'best_time' => 'March – May',
'tagline' => 'The heart of American history.',
'about_title' => 'Washington DC’s Past: A Quick Look',
'about' => '<p>Washington DC stands on land once home to the Nacotchtank people along the Potomac and Anacostia rivers. Chosen as the nation’s capital in 1790 and laid out by Pierre L’Enfant, the city grew around wide avenues, grand monuments and the National Mall. Today it is a city of world-class museums, historic neighborhoods and powerful landmarks, many of them free to visit.</p>
<p>A hop-on hop-off tour, bookable at hoponhopofftickets.com, is the easiest way to cover the distances: the White House, the Capitol, the Lincoln Memorial, the Washington Monument, the Smithsonian museums and Arlington National Cemetery. Book at hoponhopofftickets.com and see the capital without the long walks.</p>',
'spring' => 'See the famous cherry blossoms around the Tidal Basin and join the National Cherry Blossom Festival. Walk the National Mall from the Capitol to the Lincoln Memorial.',
'summer' => 'Escape the heat in the free Smithsonian museums, then watch July 4th fireworks over the Mall. Take an evening monuments tour when the landmarks are lit up.',
'fall' => 'Enjoy cooler days for exploring Georgetown’s streets and the C&O Canal. See the autumn colors at Arlington National Cemetery and along the Potomac.',
'winter' => 'Visit the National Christmas Tree near the White House and skate in the National Gallery Sculpture Garden. Spend cold days in the Air and Space Museum or the National Archives.',
),
);

View File

@@ -0,0 +1,387 @@
<?php
/**
* City guide: the editable text shown on a city page above its tours — a
* tagline in the banner, a facts bar, an "About" section and seasonal cards.
*
* Stored per destination term in one meta array (_vc_guide). Edited on the
* term screen (Viator tours → Destinations → edit a city). Shown on page 1 of
* the city's listing only, and never while filters are applied, so browsing
* and filtering land straight on the tours.
*/
defined( 'ABSPATH' ) || exit;
class VC_City_Guide {
const META = '_vc_guide';
/**
* Slugs from data/city-guides.php already imported. "_v2" re-runs the
* import once for sites that imported before "Best time to visit" was
* added to the data.
*/
const IMPORTED_OPTION = 'vc_city_guides_imported_v2';
public static function init() {
add_action( 'viator_destination_edit_form_fields', array( __CLASS__, 'edit_fields' ), 20 );
add_action( 'edited_viator_destination', array( __CLASS__, 'save' ) );
add_action( 'admin_init', array( __CLASS__, 'import_bundled' ) );
}
/**
* Field key => [label, type, help]. Pure (no WordPress calls) so tests can
* check the bundled data against it.
*/
public static function fields() {
return array(
'tagline' => array( 'Tagline', 'text', 'One short line under the city name in the banner, e.g. "Where is America’s tropical star?"' ),
'language' => array( 'Language', 'text', 'Facts bar. E.g. "English, Spanish".' ),
'currency' => array( 'Currency', 'text', 'Facts bar. E.g. "US Dollar (USD)".' ),
'timezone' => array( 'Time zone', 'text', 'Facts bar. E.g. "GMT-5".' ),
'best_time' => array( 'Best time to visit', 'text', 'Facts bar. E.g. "March – May".' ),
'about_title' => array( 'About heading', 'text', 'Heading of the About section. Empty uses "Discover {city}".' ),
'about' => array( 'About text', 'html', 'History and introduction. Long text folds behind a "Read more" button.' ),
'about_image' => array( 'About photo', 'image', 'Photo beside the About text. Empty uses one of the city\'s tour photos.' ),
'spring' => array( 'Spring', 'textarea', 'Things to do in spring (2–3 sentences).' ),
'summer' => array( 'Summer', 'textarea', 'Things to do in summer.' ),
'fall' => array( 'Fall', 'textarea', 'Things to do in fall.' ),
'winter' => array( 'Winter', 'textarea', 'Things to do in winter.' ),
'southern' => array( 'Southern hemisphere', 'checkbox', 'Tick for cities south of the equator (e.g. Sydney, Cape Town) so the season months are flipped.' ),
);
}
public static function get( $term_id ) {
$saved = get_term_meta( (int) $term_id, self::META, true );
$saved = is_array( $saved ) ? $saved : array();
$guide = array();
foreach ( self::fields() as $key => $field ) {
$guide[ $key ] = isset( $saved[ $key ] ) ? $saved[ $key ] : ( 'checkbox' === $field[1] ? false : '' );
}
return $guide;
}
public static function sanitize( array $raw ) {
$clean = array();
foreach ( self::fields() as $key => $field ) {
$value = isset( $raw[ $key ] ) ? $raw[ $key ] : '';
switch ( $field[1] ) {
case 'html':
$clean[ $key ] = trim( wp_kses_post( (string) $value ) );
break;
case 'textarea':
$clean[ $key ] = trim( sanitize_textarea_field( (string) $value ) );
break;
case 'image':
$clean[ $key ] = esc_url_raw( (string) $value );
break;
case 'checkbox':
$clean[ $key ] = ! empty( $value );
break;
default:
$clean[ $key ] = trim( sanitize_text_field( (string) $value ) );
}
}
return $clean;
}
/** True when no text field is filled (the hemisphere flag alone doesn't count). */
public static function is_empty( array $guide ) {
foreach ( self::fields() as $key => $field ) {
if ( 'checkbox' !== $field[1] && 'about_image' !== $key && ! empty( $guide[ $key ] ) ) {
return false;
}
}
return true;
}
/**
* $guide with its empty fields taken from $defaults. Pure.
*/
public static function fill_empty( array $guide, array $defaults ) {
foreach ( $defaults as $key => $value ) {
if ( empty( $guide[ $key ] ) ) {
$guide[ $key ] = $value;
}
}
return $guide;
}
/**
* Months for each season. Pure.
*/
public static function season_months( $southern ) {
$north = array(
'spring' => 'March – May',
'summer' => 'June – August',
'fall' => 'September – November',
'winter' => 'December – February',
);
if ( ! $southern ) {
return $north;
}
return array(
'spring' => $north['fall'],
'summer' => $north['winter'],
'fall' => $north['spring'],
'winter' => $north['summer'],
);
}
/* ------------------------------------------------------------------ admin */
public static function edit_fields( $term ) {
if ( get_term_meta( $term->term_id, '_vc_region', true ) ) {
return; // Regions use their own page; the guide is for cities.
}
$guide = self::get( $term->term_id );
wp_nonce_field( 'vc_guide_save', 'vc_guide_nonce' );
?>
<tr class="form-field"><th colspan="2" style="padding-bottom:0">
<h2 style="margin:1.5em 0 .25em"><?php echo esc_html__( 'City guide', 'viator-collector' ); ?></h2>
<p class="description" style="font-weight:400"><?php echo esc_html__( 'Shown on the city page above its tours. Anything left empty is hidden.', 'viator-collector' ); ?></p>
</th></tr>
<?php
foreach ( self::fields() as $key => $field ) {
list( $label, $type, $help ) = $field;
$name = 'vc_guide[' . $key . ']';
$id = 'vc-guide-' . $key;
$value = $guide[ $key ];
?>
<tr class="form-field">
<th scope="row"><label for="<?php echo esc_attr( $id ); ?>"><?php echo esc_html( $label ); ?></label></th>
<td>
<?php
if ( 'html' === $type ) {
wp_editor(
(string) $value,
'vcguideabout',
array(
'textarea_name' => $name,
'textarea_rows' => 8,
'media_buttons' => false,
'teeny' => true,
)
);
} elseif ( 'textarea' === $type ) {
echo '<textarea id="' . esc_attr( $id ) . '" name="' . esc_attr( $name ) . '" rows="3">' . esc_textarea( $value ) . '</textarea>';
} elseif ( 'checkbox' === $type ) {
echo '<label><input type="checkbox" id="' . esc_attr( $id ) . '" name="' . esc_attr( $name ) . '" value="1" ' . checked( (bool) $value, true, false ) . ' style="width:auto"> ' . esc_html__( 'Yes', 'viator-collector' ) . '</label>';
} elseif ( 'image' === $type ) {
self::image_field( $id, $name, (string) $value );
} else {
echo '<input type="text" id="' . esc_attr( $id ) . '" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '">';
}
?>
<p class="description"><?php echo esc_html( $help ); ?></p>
</td>
</tr>
<?php
}
}
private static function image_field( $id, $name, $value ) {
?>
<input type="hidden" id="<?php echo esc_attr( $id ); ?>" name="<?php echo esc_attr( $name ); ?>" value="<?php echo esc_attr( $value ); ?>">
<button type="button" class="button" id="<?php echo esc_attr( $id ); ?>-pick"><?php echo esc_html__( 'Select image', 'viator-collector' ); ?></button>
<button type="button" class="button" id="<?php echo esc_attr( $id ); ?>-clear"><?php echo esc_html__( 'Remove', 'viator-collector' ); ?></button>
<div id="<?php echo esc_attr( $id ); ?>-preview" style="margin-top:8px;">
<?php if ( $value ) : ?>
<img src="<?php echo esc_url( $value ); ?>" alt="" style="max-width:200px;height:auto;border-radius:6px;">
<?php endif; ?>
</div>
<script>
jQuery(function ($) {
var id = <?php echo wp_json_encode( $id ); ?>, frame;
$('#' + id + '-pick').on('click', function (e) {
e.preventDefault();
frame = frame || wp.media({ library: { type: 'image' }, multiple: false });
frame.off('select').on('select', function () {
var img = frame.state().get('selection').first().toJSON();
$('#' + id).val(img.url);
$('#' + id + '-preview').html($('<img>', { src: img.url }).css({ maxWidth: '200px', height: 'auto', borderRadius: '6px' }));
});
frame.open();
});
$('#' + id + '-clear').on('click', function (e) {
e.preventDefault();
$('#' + id).val('');
$('#' + id + '-preview').empty();
});
});
</script>
<?php
}
public static function save( $term_id ) {
if ( ! current_user_can( 'manage_categories' ) ) {
return;
}
if ( ! isset( $_POST['vc_guide_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['vc_guide_nonce'] ) ), 'vc_guide_save' ) ) {
return;
}
$raw = isset( $_POST['vc_guide'] ) && is_array( $_POST['vc_guide'] ) ? wp_unslash( $_POST['vc_guide'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized per field below.
$guide = self::sanitize( $raw );
if ( self::is_empty( $guide ) && ! $guide['southern'] && '' === $guide['about_image'] ) {
delete_term_meta( $term_id, self::META );
} else {
update_term_meta( $term_id, self::META, $guide );
}
}
/**
* Fill the empty fields of each city's guide from data/city-guides.php;
* text already entered is never replaced. Each slug is handled once;
* cities that don't exist yet are retried at most hourly.
*/
public static function import_bundled() {
if ( ! current_user_can( 'manage_categories' ) || get_transient( 'vc_city_guides_check' ) ) {
return;
}
$data = include VC_PLUGIN_DIR . 'data/city-guides.php';
$done = (array) get_option( self::IMPORTED_OPTION, array() );
$todo = array_diff( array_keys( $data ), $done );
foreach ( $todo as $slug ) {
$term = get_term_by( 'slug', $slug, 'viator_destination' );
if ( ! $term ) {
continue;
}
update_term_meta( $term->term_id, self::META, self::fill_empty( self::get( $term->term_id ), self::sanitize( $data[ $slug ] ) ) );
$done[] = $slug;
}
update_option( self::IMPORTED_OPTION, array_values( array_unique( $done ) ), false );
if ( array_diff( array_keys( $data ), $done ) ) {
set_transient( 'vc_city_guides_check', 1, HOUR_IN_SECONDS );
}
}
/* --------------------------------------------------------------- front end */
/** The guide is shown on page 1 of an unfiltered city listing only. */
public static function is_visible_here() {
$paged = max( (int) get_query_var( 'paged' ), isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1 ); // phpcs:ignore WordPress.Security.NonceVerification
return $paged <= 1 && ! VC_Display::filters_active( VC_Display::get_filter_state() );
}
/**
* Facts bar, About and Seasons for $term, or '' when the guide is empty.
*/
public static function render( $term ) {
$guide = self::get( $term->term_id );
if ( self::is_empty( $guide ) ) {
return '';
}
$city = $term->name;
$out = '';
$facts = array(
'language' => array( __( 'Language', 'viator-collector' ), 'lang' ),
'currency' => array( __( 'Currency', 'viator-collector' ), 'cur' ),
'timezone' => array( __( 'Time zone', 'viator-collector' ), 'time' ),
'best_time' => array( __( 'Best time to visit', 'viator-collector' ), 'sun' ),
);
$items = '';
foreach ( $facts as $key => $fact ) {
if ( '' !== $guide[ $key ] ) {
$items .= '<li class="cfact"><span class="cfact__ic">' . self::icon( $fact[1] ) . '</span><span><small>' . esc_html( $fact[0] ) . '</small><b>' . esc_html( $guide[ $key ] ) . '</b></span></li>';
}
}
if ( $items ) {
// Wrapped: direct children of .vc-app are forced full width.
$out .= '<div class="cty-top"><div class="container"><ul class="cty-facts">' . $items . '</ul></div></div>';
}
if ( '' !== $guide['about'] ) {
$img = $guide['about_image'] ? $guide['about_image'] : self::fallback_image( $term );
/* translators: %s: city name. */
$title = '' !== $guide['about_title'] ? $guide['about_title'] : sprintf( __( 'Discover %s', 'viator-collector' ), $city );
$out .= '<section class="section cty-about"><div class="container"><div class="cty-about__grid' . ( $img ? '' : ' cty-about__grid--text' ) . '">';
if ( $img ) {
$out .= '<img class="cty-about__img" src="' . esc_url( $img ) . '" alt="' . esc_attr( $city ) . '" loading="lazy" decoding="async">';
}
/* translators: %s: city name. */
$out .= '<div><p class="pill cty-eyebrow">' . esc_html( sprintf( __( 'About %s', 'viator-collector' ), $city ) ) . '</p>'
. '<h2 class="cty-h2">' . esc_html( $title ) . '</h2>'
. '<div class="cty-about__text" data-vc-clamp>' . wpautop( $guide['about'] ) . '</div>'
. '<button class="cty-more" type="button" hidden aria-expanded="false"><span>' . esc_html__( 'Read more', 'viator-collector' ) . '</span>' . vc_icon( 'chevron' ) . '</button>'
. '</div></div></div></section>';
}
$months = self::season_months( $guide['southern'] );
$seasons = array(
'spring' => array( __( 'Spring', 'viator-collector' ), 'flower' ),
'summer' => array( __( 'Summer', 'viator-collector' ), 'sun' ),
'fall' => array( __( 'Fall', 'viator-collector' ), 'leaf' ),
'winter' => array( __( 'Winter', 'viator-collector' ), 'snow' ),
);
$cards = '';
foreach ( $seasons as $key => $season ) {
if ( '' !== $guide[ $key ] ) {
$cards .= '<li class="season season--' . esc_attr( $key ) . '"><span class="season__ic">' . self::icon( $season[1] ) . '</span>'
. '<h3>' . esc_html( $season[0] ) . '</h3><small>' . esc_html( $months[ $key ] ) . '</small>'
. '<p>' . esc_html( $guide[ $key ] ) . '</p></li>';
}
}
if ( $cards ) {
/* translators: %s: city name. */
$out .= '<section class="section cty-seasons"><div class="container"><p class="pill cty-eyebrow">' . esc_html__( 'Seasonal activities', 'viator-collector' ) . '</p>'
. '<h2 class="cty-h2">' . esc_html( sprintf( __( 'What to do in %s, all year round', 'viator-collector' ), $city ) ) . '</h2>'
. '<ul class="seasons">' . $cards . '</ul></div></section>';
}
return $out;
}
public static function tagline( $term ) {
return self::get( $term->term_id )['tagline'];
}
/**
* A tour photo of the city that isn't the banner photo, cached for a day.
*/
private static function fallback_image( $term ) {
$key = 'vc_guide_img_' . $term->term_id;
$cached = get_transient( $key );
if ( false !== $cached ) {
return $cached;
}
$hero = vc_term_image_url( $term->term_id, $term->taxonomy );
$query = new WP_Query(
array(
'post_type' => 'viator_tour',
'post_status' => 'publish',
'posts_per_page' => 10,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_term_cache' => false,
'tax_query' => array(
array(
'taxonomy' => $term->taxonomy,
'field' => 'term_id',
'terms' => $term->term_id,
),
),
)
);
$url = '';
foreach ( $query->posts as $post_id ) {
$candidate = (string) get_post_meta( (int) $post_id, '_vc_primary_image_url', true );
if ( '' !== $candidate && $candidate !== $hero ) {
$url = $candidate;
break;
}
}
set_transient( $key, $url, $url ? DAY_IN_SECONDS : HOUR_IN_SECONDS );
return $url;
}
private static function icon( $name ) {
$paths = array(
'lang' => '<path d="M4 5h9M8.5 3v2m3 0c-.8 4-3.6 7-7 8.5m2.5-5c1 2 2.6 3.6 4.5 4.5M13 21l4-9 4 9m-6.8-3h5.6"/>',
'cur' => '<rect x="2.5" y="6" width="19" height="12" rx="2"/><circle cx="12" cy="12" r="2.5"/><path d="M6 9.5v5M18 9.5v5"/>',
'time' => '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
'sun' => '<circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>',
'flower' => '<circle cx="12" cy="12" r="2.5"/><path d="M12 9.5C10 6 10 4 12 3c2 1 2 3 0 6.5Zm0 5c2 3.5 2 5.5 0 6.5-2-1-2-3 0-6.5Zm2.5-2.5c3.5-2 5.5-2 6.5 0-1 2-3 2-6.5 0Zm-5 0c-3.5 2-5.5 2-6.5 0 1-2 3-2 6.5 0Z"/>',
'leaf' => '<path d="M5 19C5 10 10 5 20 4c-1 10-6 15-15 15Zm0 0 7-7"/>',
'snow' => '<path d="M12 2v20M3.3 7l17.4 10M3.3 17 20.7 7M9 4l3 2 3-2M9 20l3-2 3 2"/>',
);
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' . $paths[ $name ] . '</svg>';
}
}

View File

@@ -168,6 +168,10 @@ class VC_Display {
// Tour page top section + mobile booking bar (after the overrides above).
wp_enqueue_style( 'vc-tour', VC_PLUGIN_URL . 'assets/css/vc-tour.css', array( 'vc-design' ), VC_VERSION );
}
if ( is_tax( 'viator_destination' ) ) {
// City guide: tagline, facts bar, About and Seasons (see VC_City_Guide).
wp_enqueue_style( 'vc-city', VC_PLUGIN_URL . 'assets/css/vc-city.css', array( 'vc-design' ), VC_VERSION );
}
// Front-end interactions: card rails, FAQ accordion, testimonials
// marquee, date field, and auto-submit on quick filter toggles.
@@ -266,6 +270,17 @@ class VC_Display {
. '.vc-fullwidth .site-content,.vc-fullwidth .ast-container,'
. '.vc-fullwidth .entry-content,.vc-fullwidth .site-main>article'
. '{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.
'.vc-app.vc-app .acc__q{box-shadow:none!important;}',
// Astra colours every heading on the element itself (h1–h6 and
@@ -318,6 +333,16 @@ class VC_Display {
);
}
/**
* Whether any filter in $f (from get_filter_state()) narrows the results.
* Sorting alone doesn't count.
*/
public static function filters_active( array $f ) {
return '' !== $f['q'] || ! empty( $f['dest'] ) || ! empty( $f['cat'] ) || ! empty( $f['dur'] )
|| $f['pmin'] > 0 || $f['pmax'] < 1000 || $f['rating'] > 0
|| $f['freecancel'] || $f['bestdeal'] || $f['sellout'];
}
/**
* Apply the filter state to the main tours query (archive + taxonomy pages).
*/

View File

@@ -25,6 +25,7 @@ final class VC_Plugin {
VC_Cron::init();
VC_Display::init();
VC_Sections::init();
VC_City_Guide::init();
}
private function load_files() {
@@ -44,6 +45,7 @@ final class VC_Plugin {
'includes/class-vc-menu.php',
'includes/class-vc-display.php',
'includes/class-vc-sections.php',
'includes/class-vc-city-guide.php',
);
foreach ( $files as $file ) {

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>';
}
/**
* 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).
*

View File

@@ -7,11 +7,16 @@
* submits via GET. VC_Display::filter_archive_query() reads the same state.
*
* Args: $vc_title, $vc_subtitle, $vc_query, $vc_form_action, $vc_after_hero
* (HTML printed between the hero and the results, e.g. the region city grid).
* (HTML printed between the hero and the results, e.g. the region city grid),
* $vc_tagline (line under the title), $vc_list_heading (heading above the
* results) and $vc_jump (true: filtering and paging land on the results).
*/
defined( 'ABSPATH' ) || exit;
$vc_after_hero = isset( $vc_after_hero ) ? (string) $vc_after_hero : '';
$vc_after_hero = isset( $vc_after_hero ) ? (string) $vc_after_hero : '';
$vc_tagline = isset( $vc_tagline ) ? (string) $vc_tagline : '';
$vc_list_heading = isset( $vc_list_heading ) ? (string) $vc_list_heading : '';
$vc_anchor = empty( $vc_jump ) ? '' : '#tour-list';
$f = VC_Display::get_filter_state();
@@ -53,9 +58,7 @@ $vc_dur_labels = array(
);
// Whether any filter is active (drives the "Clear filters" control).
$vc_active = ( '' !== $f['q'] || ! empty( $f['dest'] ) || ! empty( $f['cat'] ) || ! empty( $f['dur'] )
|| $f['pmin'] > 0 || $f['pmax'] < 1000 || $f['rating'] > 0
|| $f['freecancel'] || $f['bestdeal'] || $f['sellout'] );
$vc_active = VC_Display::filters_active( $f );
// Current filters as a base URL, so pagination links keep the query string.
$vc_base_args = array();
@@ -70,12 +73,12 @@ if ( $f['bestdeal'] ) { $vc_base_args['bestdeal'] = 1; }
if ( $f['sellout'] ) { $vc_base_args['sellout'] = 1; }
if ( 'newest' !== $f['sort'] ) { $vc_base_args['sort'] = $f['sort']; }
$vc_page_url = function ( $n ) use ( $vc_action, $vc_base_args ) {
$vc_page_url = function ( $n ) use ( $vc_action, $vc_base_args, $vc_anchor ) {
$args = $vc_base_args;
if ( $n > 1 ) {
$args['paged'] = $n;
}
return empty( $args ) ? $vc_action : add_query_arg( $args, $vc_action );
return ( empty( $args ) ? $vc_action : add_query_arg( $args, $vc_action ) ) . $vc_anchor;
};
$vc_hero_img = vc_hero_image_source();
@@ -87,10 +90,18 @@ $vc_hero_img = vc_hero_image_source();
</div>
<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>
<?php if ( '' !== $vc_tagline ) : ?>
<p class="hero__tagline"><?php echo esc_html( $vc_tagline ); ?></p>
<?php endif; ?>
<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 . $vc_anchor ); ?>" novalidate>
<div class="filters__row">
<div class="fsearch">
<?php echo vc_icon( 'search', 'fsearch__icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
@@ -186,6 +197,10 @@ $vc_hero_img = vc_hero_image_source();
<section class="section" id="tour-list">
<div class="container">
<?php if ( '' !== $vc_list_heading ) : ?>
<p class="pill cty-eyebrow"><?php echo esc_html__( 'Tours', 'viator-collector' ); ?></p>
<h2 class="cty-h2 cty-tours-h"><?php echo esc_html( $vc_list_heading ); ?></h2>
<?php endif; ?>
<div class="results">
<p class="results__count" role="status">
<?php if ( $vc_total ) : ?>

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;
$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_attrs = $booking_url ? ' target="_blank" rel="nofollow noopener"' : '';
?>
<div class="vc-app vc-single">
<section class="section tour tour--top">
<div class="container">
<nav class="crumbs tour__crumbs" aria-label="<?php echo esc_attr__( 'Breadcrumb', 'viator-collector' ); ?>">
<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>
<?php echo vc_breadcrumb_html( vc_breadcrumb_trail(), get_the_title(), 'tour__crumbs' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<div class="container tour__layout">
<div class="tour__main">

View File

@@ -9,6 +9,17 @@ get_header();
$vc_term = get_queried_object();
$vc_name = ( $vc_term && ! is_wp_error( $vc_term ) ) ? $vc_term->name : __( 'Tours', 'viator-collector' );
$vc_desc = term_description();
// City guide (destinations only): tagline in the banner; facts, About and
// Seasons above the tours on page 1 of an unfiltered listing.
$vc_guide = '';
$vc_tagline = '';
if ( is_tax( 'viator_destination' ) && $vc_term && ! is_wp_error( $vc_term ) ) {
$vc_tagline = VC_City_Guide::tagline( $vc_term );
if ( VC_City_Guide::is_visible_here() ) {
$vc_guide = VC_City_Guide::render( $vc_term );
}
}
?>
<main class="vc-app vc-listing vc-listing--term">
<?php
@@ -24,6 +35,11 @@ $vc_desc = term_description();
'url' => get_post_type_archive_link( 'viator_tour' ),
),
),
'vc_tagline' => $vc_tagline,
'vc_after_hero' => $vc_guide,
/* translators: %s: city name. */
'vc_list_heading' => $vc_guide ? sprintf( __( 'Things to do in %s', 'viator-collector' ), $vc_name ) : '',
'vc_jump' => '' !== $vc_guide,
)
);
?>

View File

@@ -9,6 +9,7 @@ define( 'ABSPATH', __DIR__ . '/' );
require_once dirname( __DIR__ ) . '/includes/class-vc-structure.php';
require_once dirname( __DIR__ ) . '/includes/class-vc-tags.php';
require_once dirname( __DIR__ ) . '/includes/class-vc-city-guide.php';
function vc_assert_same( $expected, $actual, $msg = '' ) {
if ( $expected !== $actual ) {

View File

@@ -0,0 +1,55 @@
<?php
/**
* VC_City_Guide unit tests: bundled guide data and season months.
*/
function vc_test_bundled_guides() {
return include dirname( __DIR__ ) . '/data/city-guides.php';
}
function test_bundled_guides_use_known_fields() {
$fields = VC_City_Guide::fields();
foreach ( vc_test_bundled_guides() as $slug => $guide ) {
vc_assert_same( array(), array_values( array_diff( array_keys( $guide ), array_keys( $fields ) ) ), "unknown fields for $slug" );
}
}
function test_bundled_guides_have_core_text() {
foreach ( vc_test_bundled_guides() as $slug => $guide ) {
foreach ( array( 'tagline', 'language', 'currency', 'timezone', 'best_time', 'about_title', 'about' ) as $key ) {
vc_assert_same( true, ! empty( $guide[ $key ] ), "$slug.$key empty" );
}
vc_assert_same( false, (bool) preg_match( '/elementor|\{|\}/', implode( ' ', $guide ) ), "$slug has page junk" );
}
}
function test_bundled_guides_cover_structure_cities() {
vc_assert_same(
array( 'boston', 'chicago', 'las-vegas', 'los-angeles', 'miami', 'montreal', 'new-orleans', 'new-york-city', 'san-diego', 'san-francisco', 'seattle', 'toronto', 'vancouver', 'victoria', 'washington-dc' ),
( function ( $k ) { sort( $k ); return $k; } )( array_keys( vc_test_bundled_guides() ) )
);
}
function test_empty_guide_detection() {
$blank = array_fill_keys( array_keys( VC_City_Guide::fields() ), '' );
vc_assert_same( true, VC_City_Guide::is_empty( $blank ) );
vc_assert_same( true, VC_City_Guide::is_empty( array_merge( $blank, array( 'southern' => true, 'about_image' => 'https://x/y.jpg' ) ) ) );
vc_assert_same( false, VC_City_Guide::is_empty( array_merge( $blank, array( 'winter' => 'Skate.' ) ) ) );
}
function test_season_months_flip_south() {
$north = VC_City_Guide::season_months( false );
$south = VC_City_Guide::season_months( true );
vc_assert_same( 'March – May', $north['spring'] );
vc_assert_same( 'September – November', $south['spring'] );
vc_assert_same( 'June – August', $south['winter'] );
}
function test_fill_empty_keeps_entered_text() {
$guide = VC_City_Guide::fill_empty(
array( 'tagline' => 'Mine', 'best_time' => '' ),
array( 'tagline' => 'Bundled', 'best_time' => 'May – June' )
);
vc_assert_same( 'Mine', $guide['tagline'] );
vc_assert_same( 'May – June', $guide['best_time'] );
}

View File

@@ -4,13 +4,13 @@ defined('ABSPATH') || exit;
/**
* Plugin Name: Viator Collector
* 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.7
* Author: Amir Bagheri
* License: GPL-2.0-or-later
* Text Domain: viator-collector
*/
define('VC_VERSION', '1.9.5');
define('VC_VERSION', '1.9.7');
define('VC_PLUGIN_FILE', __FILE__);
define('VC_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('VC_PLUGIN_URL', plugin_dir_url(__FILE__));