perf/fix: externalize images to WP media; normalize Astra leaks; full-width

Plugin (1.8.2):
- remove all bundled raster images (~9.3MB) from assets/img; hero + single
  hero figure now come from the WP Media Library (Settings), shortcode args
  ([viator_hero image_id]), or the vc_testimonials filter — rendered responsive
- vc_hero_image_source() prefers an attachment ID (srcset); listing/hero use it
- settings: store hero_image_id + single_hero_figure_id (media pickers)
- enqueue at priority 20 + depend on astra-theme-css so our CSS wins ties
- stronger Astra overrides (!important): kill page-builder article top padding
  (gap above hero), force full-bleed containers, guard horizontal scroll
- normalize theme leaks inside .vc-app: no link underline, no list bullets
  (with prose exceptions for the tour Overview); gradient hero fallback

Theme: normalize .hoho link/list leaks; rebuild installable ZIP.
Also: move optimized originals to /removed-images/; ship viator-collector.zip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4hNL1PSHxXbu94SCenpKj
This commit is contained in:
2026-09-15 19:42:49 +03:30
parent 67c88c795b
commit 35368fb5f9
26 changed files with 135 additions and 29 deletions

View File

@@ -24,7 +24,9 @@ class VC_Display {
);
public static function init() {
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
// Priority 20 so Astra has already registered/enqueued its stylesheet;
// our CSS then loads after it and wins equal-specificity ties.
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ), 20 );
add_action( 'template_redirect', array( __CLASS__, 'maybe_enrich_single' ) );
add_action( 'pre_get_posts', array( __CLASS__, 'filter_archive_query' ) );
add_filter( 'single_template', array( __CLASS__, 'single_template' ) );
@@ -170,14 +172,36 @@ class VC_Display {
'',
array(
// Full-bleed: strip Astra's page-builder article padding/width so
// the hero and sections reach the viewport edges (fixes the extra
// top gap above the hero and the non-full-width sections).
'.vc-fullwidth .site-main>article,.vc-fullwidth .ast-page-builder-template .site-main>article{padding:0;margin:0;border:0;}',
'.vc-fullwidth #primary{margin:0;padding:0;}',
'.vc-fullwidth .site-content .ast-container{max-width:none;padding-inline:0;}',
'.vc-fullwidth .ast-container{max-width:none;}',
'.vc-fullwidth .entry-content{margin-block:0;}',
'.vc-fullwidth .site-content{padding:0;}',
// the hero and sections reach the viewport edges. !important beats
// Astra's own high-specificity rules (e.g. the 2em article padding
// that leaves a gap above the hero).
'.vc-fullwidth .site-main>article,'
. '.vc-fullwidth .ast-page-builder-template .site-main>article,'
. '.vc-fullwidth.ast-page-builder-template .site-main>article'
. '{padding:0!important;margin:0!important;border:0!important;max-width:none!important;}',
'.vc-fullwidth #primary,.vc-fullwidth #content{margin:0!important;padding:0!important;}',
'.vc-fullwidth .site-content .ast-container,.vc-fullwidth .ast-container'
. '{max-width:none!important;padding-left:0!important;padding-right:0!important;}',
'.vc-fullwidth .entry-content{margin-block:0!important;}',
'.vc-fullwidth .site-content{padding:0!important;}',
// Guard against a stray horizontal scrollbar from any full-bleed child.
'body.vc-fullwidth{overflow-x:hidden;}',
// A gradient stands in when no hero image is set, so the banner is
// never blank.
'.vc-app .hero__media,.vc-app .hero-tour{background:linear-gradient(120deg,#101317,#2cafc2);}',
// Normalize: stop Astra from decorating our components. Its
// .entry-content rules (underlined links, list bullets/indent)
// out-specify our scoped base, so reset them here (!important beats
// e.g. `.ast-single-post .entry-content a`). The .vc-app.vc-app
// doubling lifts specificity without touching design class rules.
'.vc-app a,.vc-app.vc-app a{text-decoration:none;box-shadow:none;}',
'.vc-app :is(ul,ol),.vc-app.vc-app :is(ul,ol){list-style:none!important;margin:0!important;padding:0!important;}',
'.vc-app li{margin:0!important;}',
// …but keep the tour "Overview" prose (the_content) readable.
'.vc-app .tour__text a{text-decoration:underline;}',
'.vc-app .tour__text ul{list-style:disc!important;margin:1em 0!important;padding-inline-start:1.5em!important;}',
'.vc-app .tour__text ol{list-style:decimal!important;margin:1em 0!important;padding-inline-start:1.5em!important;}',
'.vc-app .tour__text li{margin:.25em 0!important;}',
// Typography: force the design font over Astra inside our scopes.
'.vc-app,.vc-app h1,.vc-app h2,.vc-app h3,.vc-app h4,.vc-app h5,.vc-app h6,'
. '.vc-app p,.vc-app a,.vc-app span,.vc-app li,.vc-app button,.vc-app input,'

View File

@@ -50,17 +50,15 @@ class VC_Sections {
self::HERO
);
// Prefer a media-library image (responsive srcset); fall back to a URL,
// the plugin's Hero image setting, then the bundled image.
$img = '';
// Prefer a media-library image (responsive srcset): shortcode image_id,
// then a shortcode URL, then the plugin's Hero image setting. No bundled
// fallback — an empty source shows the CSS gradient.
if ( '' !== $atts['image_id'] ) {
$img = absint( $atts['image_id'] );
} elseif ( '' !== $atts['image'] ) {
$img = $atts['image'];
} elseif ( vc_hero_image_url() ) {
$img = vc_hero_image_url();
} else {
$img = vc_asset_url( 'img/hero.png' );
$img = vc_hero_image_source();
}
$archive = get_post_type_archive_link( 'viator_tour' );
$archive = $archive ? $archive : home_url( '/' );
@@ -405,15 +403,15 @@ class VC_Sections {
self::REVIEWS
);
$a1 = vc_asset_url( 'img/ellipse-17.png' );
$a2 = vc_asset_url( 'img/ellipse-17-2.png' );
// Avatars are optional; supply them via the vc_testimonials filter
// ('avatar' => attachment URL) — none are bundled with the plugin.
$items = apply_filters(
'vc_testimonials',
array(
array( 'name' => 'Malika Favre', 'role' => __( 'Paris Tour', 'viator-collector' ), 'text' => __( 'An unforgettable day — the open-top ride and the guide made the whole city feel new. Booking was effortless.', 'viator-collector' ), 'avatar' => $a1, 'size' => 'lg' ),
array( 'name' => 'James Carter', 'role' => __( 'London Tour', 'viator-collector' ), 'text' => __( 'Great value and flexible timing. We hopped on and off all day and saw everything on our list.', 'viator-collector' ), 'avatar' => $a2, 'size' => '' ),
array( 'name' => 'Sofia Rossi', 'role' => __( 'Sydney Tour', 'viator-collector' ), 'text' => __( 'Smooth from start to finish. Highly recommended.', 'viator-collector' ), 'avatar' => $a1, 'size' => 'sm' ),
array( 'name' => 'Liam Nguyen', 'role' => __( 'San Diego Tour', 'viator-collector' ), 'text' => __( 'Friendly crew, comfortable seats and the best views of the coast.', 'viator-collector' ), 'avatar' => $a2, 'size' => '' ),
array( 'name' => 'Malika Favre', 'role' => __( 'Paris Tour', 'viator-collector' ), 'text' => __( 'An unforgettable day — the open-top ride and the guide made the whole city feel new. Booking was effortless.', 'viator-collector' ), 'avatar' => '', 'size' => 'lg' ),
array( 'name' => 'James Carter', 'role' => __( 'London Tour', 'viator-collector' ), 'text' => __( 'Great value and flexible timing. We hopped on and off all day and saw everything on our list.', 'viator-collector' ), 'avatar' => '', 'size' => '' ),
array( 'name' => 'Sofia Rossi', 'role' => __( 'Sydney Tour', 'viator-collector' ), 'text' => __( 'Smooth from start to finish. Highly recommended.', 'viator-collector' ), 'avatar' => '', 'size' => 'sm' ),
array( 'name' => 'Liam Nguyen', 'role' => __( 'San Diego Tour', 'viator-collector' ), 'text' => __( 'Friendly crew, comfortable seats and the best views of the coast.', 'viator-collector' ), 'avatar' => '', 'size' => '' ),
)
);
if ( empty( $items ) ) {

View File

@@ -75,6 +75,8 @@ class VC_Settings {
'timeout' => 20,
'default_post_status' => 'publish',
'hero_image' => '',
'hero_image_id' => 0,
'single_hero_figure_id' => 0,
'hero_slides' => '',
'crawl_destinations' => '',
'rate_limit_per_minute' => 25,
@@ -122,6 +124,8 @@ class VC_Settings {
'timeout' => max( 5, absint( $input['timeout'] ?? 20 ) ),
'default_post_status' => sanitize_key( $input['default_post_status'] ?? 'publish' ),
'hero_image' => esc_url_raw( $input['hero_image'] ?? '' ),
'hero_image_id' => absint( $input['hero_image_id'] ?? 0 ),
'single_hero_figure_id' => absint( $input['single_hero_figure_id'] ?? 0 ),
'hero_slides' => self::sanitize_id_list( $input['hero_slides'] ?? '' ),
'crawl_destinations' => self::sanitize_id_list( $input['crawl_destinations'] ?? '' ),
'rate_limit_per_minute' => max( 1, min( 60, absint( $input['rate_limit_per_minute'] ?? 25 ) ) ),
@@ -198,9 +202,10 @@ class VC_Settings {
<th scope="row"><label for="vc_hero_image"><?php echo esc_html__( 'Hero image', 'viator-collector' ); ?></label></th>
<td>
<input id="vc_hero_image" type="url" class="regular-text" name="vc_settings[hero_image]" value="<?php echo esc_attr( $options['hero_image'] ); ?>" placeholder="https://…">
<input id="vc_hero_image_id" type="hidden" name="vc_settings[hero_image_id]" value="<?php echo esc_attr( $options['hero_image_id'] ); ?>">
<button type="button" class="button" id="vc-hero-pick"><?php echo esc_html__( 'Select image', 'viator-collector' ); ?></button>
<button type="button" class="button" id="vc-hero-clear"><?php echo esc_html__( 'Clear', 'viator-collector' ); ?></button>
<p class="description"><?php echo esc_html__( 'Banner shown at the top of the tours listing. Leave empty for a gradient. Destination pages use their own image when one is set.', 'viator-collector' ); ?></p>
<p class="description"><?php echo esc_html__( 'Banner at the top of the tours listing / home hero. Pick from the Media Library so the browser gets responsive, device-sized images. Leave empty for a gradient. Destination pages use their own image when one is set.', 'viator-collector' ); ?></p>
<div id="vc-hero-preview" style="margin-top:8px;">
<?php if ( $options['hero_image'] ) : ?>
<img src="<?php echo esc_url( $options['hero_image'] ); ?>" alt="" style="max-width:320px;height:auto;border-radius:8px;">
@@ -221,6 +226,7 @@ class VC_Settings {
frame.on('select', function () {
var img = frame.state().get('selection').first().toJSON();
$('#vc_hero_image').val(img.url);
$('#vc_hero_image_id').val(img.id);
$('#vc-hero-preview').html($('<img>', { src: img.url, alt: '' }).css({ maxWidth: '320px', height: 'auto', borderRadius: '8px' }));
});
frame.open();
@@ -228,12 +234,56 @@ class VC_Settings {
$('#vc-hero-clear').on('click', function (e) {
e.preventDefault();
$('#vc_hero_image').val('');
$('#vc_hero_image_id').val('');
$('#vc-hero-preview').empty();
});
});
</script>
</td>
</tr>
<tr>
<th scope="row"><label><?php echo esc_html__( 'Single tour hero figure', 'viator-collector' ); ?></label></th>
<td>
<input id="vc_single_figure_id" type="hidden" name="vc_settings[single_hero_figure_id]" value="<?php echo esc_attr( $options['single_hero_figure_id'] ); ?>">
<button type="button" class="button" id="vc-figure-pick"><?php echo esc_html__( 'Select image', 'viator-collector' ); ?></button>
<button type="button" class="button" id="vc-figure-clear"><?php echo esc_html__( 'Clear', 'viator-collector' ); ?></button>
<p class="description"><?php echo esc_html__( 'Optional decorative cut-out shown over the single-tour hero (e.g. a landmark PNG with a transparent background). Leave empty to hide it.', 'viator-collector' ); ?></p>
<div id="vc-figure-preview" style="margin-top:8px;">
<?php
if ( $options['single_hero_figure_id'] ) {
echo wp_get_attachment_image( (int) $options['single_hero_figure_id'], 'medium', false, array( 'style' => 'max-width:200px;height:auto;' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
</div>
<script>
jQuery(function ($) {
var frame;
$('#vc-figure-pick').on('click', function (e) {
e.preventDefault();
if (frame) { frame.open(); return; }
frame = wp.media({
title: <?php echo wp_json_encode( __( 'Select hero figure', 'viator-collector' ) ); ?>,
button: { text: <?php echo wp_json_encode( __( 'Use this image', 'viator-collector' ) ); ?> },
library: { type: 'image' },
multiple: false
});
frame.on('select', function () {
var img = frame.state().get('selection').first().toJSON();
$('#vc_single_figure_id').val(img.id);
var src = (img.sizes && img.sizes.medium) ? img.sizes.medium.url : img.url;
$('#vc-figure-preview').html($('<img>', { src: src, alt: '' }).css({ maxWidth: '200px', height: 'auto' }));
});
frame.open();
});
$('#vc-figure-clear').on('click', function (e) {
e.preventDefault();
$('#vc_single_figure_id').val('');
$('#vc-figure-preview').empty();
});
});
</script>
</td>
</tr>
<tr>
<th scope="row"><label for="vc_hero_slides"><?php echo esc_html__( 'Hero slider images', 'viator-collector' ); ?></label></th>
<td>

View File

@@ -138,6 +138,34 @@ function vc_hero_image_url() {
return apply_filters( 'vc_hero_image_url', $url );
}
/**
* Hero image *source* for the current view: an attachment ID when available
* (so it can be rendered responsively), otherwise a URL. Resolution order:
* 1. On a term archive, that term's own image.
* 2. The "Hero image" chosen in Settings (attachment ID preferred).
* 3. Nothing.
*
* @return int|string Attachment ID, image URL, or '' .
*/
function vc_hero_image_source() {
if ( is_tax( array( 'viator_destination', 'viator_tag', 'viator_category' ) ) ) {
$term = get_queried_object();
if ( $term && ! is_wp_error( $term ) ) {
$url = vc_term_image_url( $term->term_id, $term->taxonomy );
if ( $url ) {
return apply_filters( 'vc_hero_image_source', $url );
}
}
}
$id = (int) vc_get_option( 'hero_image_id', 0 );
if ( $id ) {
return apply_filters( 'vc_hero_image_source', $id );
}
return apply_filters( 'vc_hero_image_source', (string) vc_get_option( 'hero_image', '' ) );
}
function vc_term_image_url( $term_id, $taxonomy = '' ) {
$term_id = absint( $term_id );
if ( ! $term_id ) {