' . esc_html__( 'This block must be placed inside a Terms Query Loop (Term Template) so it can receive a term.', 'gutenify' ) . '
';
}
return;
}
$term = get_term( $term_id, $taxonomy );
if ( ! $term || is_wp_error( $term ) ) {
if ( $is_editor_context ) {
echo '' . esc_html__( 'This term could not be found.', 'gutenify' ) . '
';
}
return;
}
// get_term() already resolves the real taxonomy from the term object even
// when the `$taxonomy` arg above was empty/wrong — use its value from here
// on for the WooCommerce fallback check and the term link below.
$taxonomy = $term->taxonomy;
$allowed_sizes = array_values(
array_unique(
array_merge( array( 'thumbnail', 'medium', 'medium_large', 'large', 'full' ), get_intermediate_image_sizes() )
)
);
$image_size = isset( $attributes['imageSize'] ) && in_array( $attributes['imageSize'], $allowed_sizes, true ) ? $attributes['imageSize'] : 'thumbnail';
// Same three values as CSS's own `object-fit` keywords, matching core's
// Image block "Scale" control (Cover/Contain/Fill) 1:1.
$allowed_scales = array( 'cover', 'contain', 'fill' );
$scale = isset( $attributes['scale'] ) && in_array( $attributes['scale'], $allowed_scales, true ) ? $attributes['scale'] : 'cover';
// Same preset values as core's Image block "Aspect ratio" control —
// whitelisted rather than passed through raw since this becomes a CSS
// custom property value below.
//
// Default is "Square" (1), not "Original" (auto) — this block is meant
// for a Terms Query Loop grid, where each term's own thumbnail almost
// certainly has a different natural size/orientation (different source
// photos per category). "Original" produced a visibly broken grid out
// of the box; a uniform square crop is the standard choice for exactly
// this reason.
$allowed_aspect_ratios = array( 'auto', '1', '4/3', '3/2', '16/9', '3/4', '2/3', '9/16' );
$aspect_ratio = isset( $attributes['aspectRatio'] ) ? (string) $attributes['aspectRatio'] : '1';
if ( ! in_array( $aspect_ratio, $allowed_aspect_ratios, true ) ) {
$aspect_ratio = '1';
}
// `focalPoint` mirrors core's Image block attribute shape ({x, y}, each
// 0-1) — clamped here since it becomes a CSS percentage below.
$focal_point = isset( $attributes['focalPoint'] ) && is_array( $attributes['focalPoint'] ) ? $attributes['focalPoint'] : array();
$focal_point_x = isset( $focal_point['x'] ) ? min( 1, max( 0, (float) $focal_point['x'] ) ) : 0.5;
$focal_point_y = isset( $focal_point['y'] ) ? min( 1, max( 0, (float) $focal_point['y'] ) ) : 0.5;
$link_to_term = ! isset( $attributes['linkToTerm'] ) || ! empty( $attributes['linkToTerm'] );
// Border support is skip-serialized in block.json (see the helper above),
// so it never lands on get_block_wrapper_attributes()'s wrapper
// below — apply it directly to the
![]()
/placeholder
![]()
(and, further
// down, the dim-overlay
) instead, same as core's own Post Featured
// Image block does for its
/overlay pair.
$border_attributes = gutenify_term_thumbnail_get_border_attributes( $attributes );
$border_class = ! empty( $border_attributes['class'] ) ? (string) $border_attributes['class'] : '';
$border_style = ! empty( $border_attributes['style'] ) ? (string) $border_attributes['style'] : '';
// Same WooCommerce term-thumbnail meta convention category-list already
// reads.
$image_id = absint( get_term_meta( $term_id, 'thumbnail_id', true ) );
$image_html = '';
if ( $image_id ) {
$attachment_title = get_the_title( $image_id );
$image_attr = array(
'loading' => 'lazy',
'alt' => $attachment_title ? $attachment_title : $term->name,
);
// wp_get_attachment_image() esc_attr()'s every $attr value itself
// before echoing, so class/style don't need re-escaping here — only
// merged in when actually set, so the default attachment/size classes
// are left untouched when there's no border to apply.
if ( $border_class ) {
$image_attr['class'] = $border_class;
}
if ( $border_style ) {
$image_attr['style'] = $border_style;
}
$image_html = wp_get_attachment_image( $image_id, $image_size, false, $image_attr );
} elseif ( function_exists( 'wc_placeholder_img' ) && 'product_cat' === $taxonomy ) {
// Generic, not WooCommerce-gated: this fallback only fires for the one
// case category-list already handles the same way — a product_cat term
// with no thumbnail_id meta, while WooCommerce is active. Any other
// taxonomy with no thumbnail simply renders nothing below, matching
// core's own Post Featured Image block when a post has no featured
// image.
//
// wc_placeholder_img()'s own default $attr (`wp_parse_args( $attr,
// $default_attr )`) lets a passed-in `class` key win outright rather
// than merging strings, so its default
// `woocommerce-placeholder wp-post-image` class has to be concatenated
// in here explicitly or it's silently lost the moment a border class
// exists. wc_placeholder_img() also esc_attr()'s every attribute
// itself before echoing, same as wp_get_attachment_image() above.
$placeholder_attr = array(
'class' => trim( 'woocommerce-placeholder wp-post-image' . ( $border_class ? ' ' . $border_class : '' ) ),
);
if ( $border_style ) {
$placeholder_attr['style'] = $border_style;
}
$image_html = wc_placeholder_img( $image_size, $placeholder_attr );
}
if ( '' === $image_html ) {
if ( $is_editor_context ) {
echo '' . esc_html__( 'This term does not have a thumbnail image.', 'gutenify' ) . '
';
}
return;
}
$has_aspect_ratio = 'auto' !== $aspect_ratio;
$style = '--term-thumbnail-scale:' . esc_attr( $scale ) . ';';
if ( $has_aspect_ratio ) {
$style .= sprintf(
'--term-thumbnail-aspect-ratio:%1$s;--term-thumbnail-focal-x:%2$s%%;--term-thumbnail-focal-y:%3$s%%;',
esc_attr( $aspect_ratio ),
esc_attr( $focal_point_x * 100 ),
esc_attr( $focal_point_y * 100 )
);
}
$wrapper_attributes = get_block_wrapper_attributes(
array(
'class' => 'gutenify--term-thumbnail' . ( $has_aspect_ratio ? ' has-aspect-ratio' : '' ),
'style' => $style,
)
);
// A link is never rendered for an editor-context render (classic-editor
// is_admin(), or ServerSideRender's block-renderer REST call) regardless of
// the `linkToTerm` attribute's value — clicking a link inside the editor
// canvas is bad UX (nested-link/click-target problems), and core's own
// Post Featured Image block never renders its link in the editor either,
// only on the real frontend. get_term_link() is also skipped entirely in
// that case since its result would never be used.
$term_link = ( ! $is_editor_context && $link_to_term ) ? get_term_link( $term ) : '';
$has_link = ! $is_editor_context && $link_to_term && $term_link && ! is_wp_error( $term_link );
$link_target = isset( $attributes['linkTarget'] ) && '_blank' === $attributes['linkTarget'] ? '_blank' : '_self';
$link_rel = isset( $attributes['rel'] ) ? (string) $attributes['rel'] : '';
// Overlay: dim/color-or-gradient layer on top of the image, matching core's
// own Post Featured Image "Overlay" panel (dimRatio/overlayColor/
// customOverlayColor/gradient/customGradient). Snapped to the nearest 10 to
// match the discrete has-background-dim-{n} opacity classes shipped in
// style.scss (also guards against an out-of-step value ever reaching here
// via a raw REST/attributes payload, since RangeControl's step is only a
// display hint).
$dim_ratio = isset( $attributes['dimRatio'] ) ? absint( $attributes['dimRatio'] ) : 0;
$dim_ratio = (int) ( round( $dim_ratio / 10 ) * 10 );
$dim_ratio = max( 0, min( 100, $dim_ratio ) );
$overlay_color = isset( $attributes['overlayColor'] ) ? sanitize_html_class( $attributes['overlayColor'] ) : '';
$custom_overlay_color = isset( $attributes['customOverlayColor'] ) ? (string) $attributes['customOverlayColor'] : '';
$gradient_slug = isset( $attributes['gradient'] ) ? sanitize_html_class( $attributes['gradient'] ) : '';
$custom_gradient = isset( $attributes['customGradient'] ) ? (string) $attributes['customGradient'] : '';
$overlay_html = '';
if ( $dim_ratio > 0 ) {
$overlay_classes = array( 'gutenify--term-thumbnail__overlay', 'has-background-dim', 'has-background-dim-' . $dim_ratio );
$overlay_styles = array();
if ( $overlay_color ) {
$overlay_classes[] = 'has-' . $overlay_color . '-background-color';
}
if ( $gradient_slug || $custom_gradient ) {
$overlay_classes[] = 'has-background-gradient';
}
if ( $gradient_slug ) {
$overlay_classes[] = 'has-' . $gradient_slug . '-gradient-background';
}
// Give the overlay the same border radius/width/color as the image
// beneath it — otherwise a square overlay pokes out past the image's
// rounded corners, same fix core's own Post Featured Image block
// applies via its overlay markup builder.
if ( $border_class ) {
$overlay_classes[] = $border_class;
}
if ( $border_style ) {
$overlay_styles[] = $border_style;
}
// Mirrors core's own overlay markup builder: build the raw declarations
// first, then run the combined string through safecss_filter_attr()
// (rejects anything that isn't a safe CSS value, e.g. a javascript: URI
// smuggled into a gradient) before esc_attr()'ing the whole style
// attribute — the same two-layer defense already used above for
// aspect-ratio/height/width/object-fit.
if ( $custom_gradient ) {
$overlay_styles[] = 'background-image:' . $custom_gradient . ';';
} elseif ( $custom_overlay_color ) {
$overlay_styles[] = 'background-color:' . $custom_overlay_color . ';';
}
$overlay_html = sprintf(
'',
esc_attr( implode( ' ', $overlay_classes ) ),
esc_attr( safecss_filter_attr( implode( '', $overlay_styles ) ) )
);
}
$image_html .= $overlay_html;
?>