Programando un bloque de WordPress para un carrusel de productos de WooCommerce

En este tutorial vamos a montar un bloque personalizado a medida para un proyecto de una tienda online. El proyecto se desarrolla sin utilizar un maquetador visual.

El bloque consiste en un carrusel de productos extraídos de WooCommerce por categorías. Ya existe un bloque nativo de WooCommerce con el que se pueden hacer muchas de las funciones que vamos a integrar pero me he encontrado con algunas carencias y por eso quiero desarrollar uno propio.

El objetivo de este desarrollo a medida es ofrecer al editor de la web una herramienta muy fácil de utiliza con la que se sienta cómodo/a a la hora de añadir este tipo de bloque, que no pueda romper el diseño y que sea 100% escalable dependiendo de las necesidades de los clientes.

¿Qué necesita el cliente?

  • Que se puedan añadir varios carruseles en una misma página.
  • Que se integre con el diseño del sitio.
  • Que sea responsive.
  • Que no afecte en el rendimiento de las páginas (que esté lo más optimizado posible).

Para llevar a cabo este proyecto he decidido utilizar las siguientes herramientas.

  • Advanced Custom Fields => Utilizaré los campos personalizados y la opción para registrar el bloque (ACF Blocks).
  • WP_Query + tax_query => Clase de WordPress que nos permite consultar la base de datos, en este caso de productos de WooCommerce y hacer los filtros correspondientes. (Importante tener siempre la documentación a mano)
  • Swiper.js => Libreria de javascript para crear sliders. (Importante tener siempre la documentación a mano)
  • Tipografía Fluida => Herramienta para textos responsive.
  • Variables CSS => Utilizaré variables para controlar los colores.

Dentro de lo que es el funcionamiento del bloque, le vamos a incorporar opciones como:

  • Definir el número total de productos que llevará el carrusel. (Campo númerico)
  • Indicar la categoría o categorías del conjunto. (Campo Select con opciones)
  • Opcional: excluir productos que lleven cierta categoría o categorías. (Campo Select con opciones)
  • Opcional: ponerle autoplay al carrusel, en caso de activarlo, se debe de indicar el «delay» en milisegundos. (Campo true / false)
  • Modificar el texto de los botones en productos con variaciones. (Campo de texto)
  • Opcional: añadir un botón para redirigir a los clientes a otra página con mas productos. (Campos de enlace y texto)
Diseño del bloque «Product Carousel Slider»

Comenzamos…

Una vez tenemos claro las funcionalidades y las herramientas que vamos a necesitar, comenzaremos por registrar el bloque de ACF, no voy a explicar otra vez como hacer un bloque, tengo un tutorial que habla en profundidad de como hacerlo. (Crea estructura de carpetas acf-json, blocks y los archivos block.json, product-carousel-slider.css y product-carousel-slider.php).

El block.json de este proyecto queda de la siguiente forma:



Destacar en este block.json la preparación de variaciones de bloque mediante los argumentos en «styles» y en «Supports la opción multiple: true» para permitir que haya mas de un bloque igual por página.

Básicamente el primer paso es añadir en el functions.php del tema hijo la función para registrar el bloque.



En el archivo header.php, añadimos dentro de la etiqueta <head> el CSS y el Javascript necesario para hacer funcionar «swiper.js» en el frontend utilizando las urls del CDN indicadas en la documentación de la librería. En este caso lo añadimos en este documento primero para que se carguen antes de que se ejecute el bloque y para evitar que se dupliquen los scripts cuando utilizamos varios carruseles en una misma página. Puedes utilizar la librería para darle vida a otros bloque personalizados. (Slider con CTAs, carrusel de noticias, testimonios…)






Otra acción importante sería añadir en el archivo functions.php del tema hijo, los mismos mismos enlaces para que se carguen en la parte del administrador de WordPress. Es decir, con esto vas a poder hacer uso de las funciones del carrusel mientras lo estas editando.



Existen varias formas de optimizar este código.

  1. Si introduces los scripts en una función dentro del bloque, te tienes que asegurar que se van a cargar antes de que se cargue el propio bloque (Esto puede ser un problema). Para lo que es importante usar la función de WordPress !function_exists() para comprobar si existe la misma función para de nuevo no duplicar scripts.


  1. Controlar con un condicional de páginas donde queremos que se carguen los scripts. Muy útil para mejorar el rendimiento del resto de páginas. Nosotros en producción vamos a utilizar is_front_page() porque solo vamos a poner carruseles en la página de inicio. Tienes varias opciones para hacer referencia a una página:


Creamos los campos personalizados con ACF

Añadimos los campos necesarios para controlar las opciones que llevará el bloque. Conforme vayamos añadiendo mas funcionalidades tendremos que añadir tantos campos necesitemos.

Así es como queda el grupo de campos del bloque carrusel. Destacar el campo Tiempo milisegundos lleva un condicional, solo se mostrará si el campo Autoplay esta en TRUE. Los campos SELECT de categorías no llevan opciones porque las vamos a introducir de forma dinámica.

¿Cómo hacemos campos dinámicos en ACF?

Para crear un campo SELECT dinámico en ACF con las categorías de producto que tiene tu tienda. Introducimos en el archivo functions.php del tema hijo el siguiente código que hace lo siguiente:



Es una función que utiliza get_terms() para localizar todas las categorías correspondientes a la taxonomía de WooCommerce «product_cat».

Introduce las categorías en un array que vamos a recorrer una a una y poder listarlas dentro de las opciones del campo utilizando $field[‘choices’]. También comprueba que las categorías no estén vacías y por último a través del filtro ‘acf/load_field/name=nombre_de_campo_select’ inyectamos el listado como opciones para el campo. En nuestro caso lo vamos a utilizar tanto para el campo de categorías incluidas como el de excluidas.

Código PHP

El archivo product-carousel-slider.php del bloque llevará el siguiente código:

<?php
/**
 * Block template file: product-carousel-slider.php
 *
 * Features Block Template.
 *
 * @param   array $block The block settings and attributes.
 * @param   string $content The block inner HTML (empty).
 * @param   bool $is_preview True during AJAX preview.
 * @param   (int|string) $post_id The post ID this block is saved to.
 */


 // Load values and assign defaults.
 $encabezado_principal  = get_field( 'encabezado_principal_product_carousel_slider' );
 $total_productos       = get_field( 'total_de_productos_product_carousel_slider' ); 
 $tiempo_milisegundos   = get_field( 'tiempo_milisegundos_product_carousel_slider' );
 $texto_boton_variable  = get_field( 'boton_variaciones_product_carousel_slider' );
 $autoplay              = get_field( 'autoplay_product_carousel_slider' );
 $texto_boton_ver_mas   = get_field( 'texto_boton_ver_mas_product_carousel_slider');
 
 

// Create id attribute allowing for custom "anchor" value.
$id = 'product-carousel-slider-' . $block['id'];
if ( !empty($block['anchor'] ) ) {
    $id = $block['anchor'];
}

// Create class attribute allowing for custom "className" and "align" values.
$classes = 'block-product-carousel-slider';
if ( !empty( $block['className'] ) ) {
    $classes .= ' ' . $block['className'];
}

if ( !empty( $block['align'] ) ) {
    $classes .= ' align' . $block['align'];
}

$claseUnica = 'block-product-carousel-slider-swiper-' . $block['id'];
$claseUnicaSlide = 'block-product-carousel-slider-swiper-slide-' . $block['id'];
$claseUnicaQuery= 'block-product-carousel-slider-swiper-' . $block['id'];
$buttonNext = '.swiper-button-next-'. $block['id'];
$buttonPrev = '.swiper-button-prev-'. $block['id'];

?>

<style type="text/css">
	<?php echo '#' . $id; ?> {
		/* Add styles that use ACF values here */
	}
    
</style>


<section id="<?php echo esc_attr( $id ); ?>" class="bloque__product__carousel__slider <?php echo esc_attr( $classes ); ?>">
    <h2 class="bloque__product__carousel__slider__heading"><?php echo wp_kses_post( $encabezado_principal );?></h2>
    

    <div class="swiper <?php echo esc_attr( $claseUnica ); ?>">
        <div class="swiper-container">
            <div class="swiper-button-group">
                <div class="swiper-button-next"></div>
                <div class="swiper-button-prev"></div>
            </div>
        </div>
        <div class="swiper-wrapper">
        <?php ob_start();?>
        <?php 
            
            $categorias_producto_product_carousel_slider_values = get_field('categorias_producto_product_carousel_slider'); 

            if ($categorias_producto_product_carousel_slider_values) { 
                $term_ids = array(); // Inicializamos el array donde guardaremos los IDs de términos de categoría 

                foreach ($categorias_producto_product_carousel_slider_values as $value) { 
                    // Obtenemos el objeto de término de la taxonomía 'product_cat' por su ID 
                    $term = get_term($value, 'product_cat');

                    if ($term && !is_wp_error($term)) { 
                        $term_ids[] = $term->term_id; // Añadimos el ID del término de categoría al array 
                    } 
                } 
            }

            $excluir_categorias_producto_product_carousel_slider_values = get_field('excluir_categorias_producto_product_carousel_slider'); 

            if ($excluir_categorias_producto_product_carousel_slider_values) { 
                $term_ex_ids = array(); // Inicializamos el array donde guardaremos los IDs de términos de categoría 

                foreach ($excluir_categorias_producto_product_carousel_slider_values as $value) { 
                    // Obtenemos el objeto de término de la taxonomía 'product_cat' por su ID 
                    $term_ex = get_term($value, 'product_cat'); 

                    if ($term_ex && !is_wp_error($term_ex)) { 
                        $term_ex_ids[] = $term_ex->term_id; // Añadimos el ID del término de categoría al array 
                    } 
                } 
            } 

            $args = array( 
                'post_type'      => 'product', 
                'posts_per_page' => $total_productos, // control desde las opciones del bloque
                'tax_query'      => array( 
                    'relation' => 'AND', // Usamos 'AND' para combinar las dos taxonomías
                    array( 
                        'taxonomy' => 'product_cat', 
                        'field'    => 'term_id', 
                        'terms'    => $term_ids, // Utilizamos el array de IDs de términos de categoría 
                        'operator' => 'IN', 
                    ),
                    array(
                        'taxonomy' => 'product_cat',
                        'field'    => 'term_id',
                        'terms'    => $term_ex_ids, // Utilizamos el array de IDs de términos de categoría para excluir
                        'operator' => 'NOT IN', // Usamos 'NOT IN' para excluir las categorías especificadas
                    ), 
                ), 
            );

            $claseUnicaQuery = new WP_Query($args);

            if ($claseUnicaQuery->have_posts()) :
                while ($claseUnicaQuery->have_posts()) {
                    $claseUnicaQuery->the_post();
                    $product = wc_get_product(get_the_ID());
            ?>
                    <div class="grid__product__carousel__slider__product swiper-slide">
                        <?php if ( $product->is_on_sale() ) : ?>
                            <?php echo '<span class="onsale">' . esc_html__( 'Sale!', 'woocommerce' ) . '</span>'; ?>
                        <?php endif; ?>
                        <div class="grid__product__carousel__slider__product-imagen">
                            <a href="<?php the_permalink(); ?>">
                                <?php
                                if (has_post_thumbnail()) {
                                    the_post_thumbnail( 'small', array( 'loading' => 'lazy' ) );
                                } else {
                                    echo '<img src="https://dev.alcalacomics.com/wp-content/uploads/2023/12/alcala-comics-placeholder.webp" />';
                                }
                                ?>
                            </a>
                        </div>
                        <div class="grid__product__carousel__slider__product-content">
                            <?php
                            global $post, $product;

                            if ($product) {
                                // Mostrar información del producto
                                // Obtener las categorías del producto
                                $categories = get_the_terms(get_the_ID(), 'product_cat');
                                if ($categories && !is_wp_error($categories)) {
                                    echo '<p class="grid__product__carousel__slider__product-content-category">';
                                    foreach ($categories as $category) {
                                        echo '<a href="' . get_term_link($category) . '">' . $category->name . '</a>, ';
                                    }
                                    echo '</p>';
                                }
                            ?>
                                <h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
                                <div class="grid__product__carousel__slider__product-content-meta">
				
                                    <?php
                                    // Mostrar información del producto
                                    $regular_price = $product->get_regular_price();
                                    $sale_price = $product->get_sale_price();
                            
                                    // Obtener el precio con IVA incluido para el precio regular
                                    $regular_price_with_tax = wc_get_price_including_tax($product, array( 'price' => $regular_price ));

                            
                                    // Obtener el precio con IVA incluido para el precio de venta
                                    $sale_price_with_tax = wc_get_price_including_tax($product, array('price' => $sale_price));
                            
                                    if ($product->is_type('variable')) {
                                        $variation_prices = $product->get_variation_prices();
                            
                                        // Obtener los precios de las variaciones
                                        $prices = $variation_prices['price'];
                            
                                        // Obtener el precio más bajo y más alto de las variaciones con tasas de impuestos incluidas
                                        $min_price_with_tax = wc_get_price_including_tax($product, array('price' => min($prices)));
                                        $max_price_with_tax = wc_get_price_including_tax($product, array('price' => max($prices)));
                            
                                        echo '<div class="grid__product__carousel__slider__product-content-price-variation">';
                                        echo '<p class="grid__product__carousel__slider__product-content-price-sale">' . wc_price($min_price_with_tax) . ' - </p>';
                                        echo '<p class="grid__product__carousel__slider__product-content-price-sale">' . wc_price($max_price_with_tax) . '</p>';
                                        echo '</div>';
                            
                                        // Mostrar el botón de variación para "Elegir opción"
                                        echo '<p class="grid__product__carousel__slider__product-content-add-to-cart">';
                                        echo '<a class="button product_type_simple add_to_cart_button ajax_add_to_cart" href="' . get_permalink() . '">' . __($texto_boton_variable, "woocommerce") . '</a>';
                                        echo '</p>';
                                    } else {
                                        // Mostrar precio sin descuento y con descuento
                                        if ($product->is_on_sale() && $sale_price < $regular_price) {
                                            echo '<div class="grid__product__carousel__slider__product-content-prices">';
                                            echo '<p class="grid__product__carousel__slider__product-content-price-regular">' . wc_price($regular_price_with_tax) . '</p>';
                                            echo '<p class="grid__product__carousel__slider__product-content-price-sale">' . wc_price($sale_price_with_tax) . '</p>';
                                            echo '</div>';
                                        } else {
                                            echo '<p class="grid__product__carousel__slider__product-content-price-regular">' . wc_price($regular_price_with_tax) . '</p>';
                                        }
                            
                                        // Mostrar el botón "Añadir al carrito" para productos simples
                                        echo '<p class="grid__product__carousel__slider__product-content-add-to-cart">';
                                        woocommerce_template_loop_add_to_cart();
                                        echo '</p>';
                                    }
                            
                                    // Resto del código...
                                    ?>
                                </div>
                            <?php } ?>
                            
                        </div>
                    </div>
            <?php
                }
                wp_reset_postdata();
            else :
                echo '<p>No se encontraron productos.</p>';
            endif;
            ?>
            <?php echo ob_get_clean(); ?>
        </div>
        <div class="swiper-pagination"></div>
    </div>
    <div class="grid__product__carousel__slider__product__button-more">
        <?php $enlace_boton_ver_mas_product_carousel_slider = get_field( 'enlace_boton_ver_mas_product_carousel_slider' ); ?>
        <?php if ( $enlace_boton_ver_mas_product_carousel_slider ) : ?>
            <div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="<?php echo esc_url( $enlace_boton_ver_mas_product_carousel_slider); ?>"><?php echo wp_kses_post($texto_boton_ver_mas);?></a></div>
        <?php endif; ?>
    </div>
</section>
</script>

<script>
        var swiper = new Swiper(".&lt;?php echo $claseUnica; ?&gt;", {
            slidesPerView: 1,
            slidesPerGroup: 1,
            rewind: true,
            <script type="text/plain">
            <?php if ( ( $autoplay ) == 1 ) : ?>
                autoplay: {
                    delay: <?php echo $tiempo_milisegundos; ?>,
                    disableOnInteraction: false
                },
            <?php else : ?>
                
            <?php endif; ?>
            pagination: {
                el: ".swiper-pagination ",
                clickable: true,
                dynamicBullets: true,
            },
            navigation: {
                nextEl: ".swiper-button-next",
                prevEl: ".swiper-button-prev"
            },

            breakpoints: {
                500: {
                    slidesPerView: 2,
                    slidesPerGroup: 2,
                },
                767: {
                    slidesPerView: 3,
                    slidesPerGroup: 3,
                },
                900: {
                    slidesPerView: 4,
                    slidesPerGroup: 4,
                },
                1200: {
                    slidesPerView: 5,
                    slidesPerGroup: 5,
                },
            },
        });
</script>

Podemos destacar las siguiente acciones:

  • Utilizamos variables de PHP para almacenar el contenido y evitar hacer muchas llamadas a la base de datos.
  • El HTML lleva incorporada la estructura de swiper, junto con la paginación y las flechas de navegación.
  • Utilizamos la Clase WP_Query() para extraer los productos poniendo en ciertos argumentos las variables correspondientes a los campos personalizados para modificar su comportamiento.
  • Filtramos el resultado con tax_query tanto para incluir como para excluir categorías.
  • Es importante crear un identificador único de cada bloque para evitar colapsar la clase WP_Query() al insertar varios carruseles en una misma página. «$claseUnicaQuery»
  • Extraemos toda la información de un producto con la variable global «$product» con esa información podemos hacer condicionales de todo lo que necesitemos, en este caso compruebo si es un producto variable, si esta en oferta etc etc y de esa forma puedo pintar el resultado de una forma u otra además de modificar el botón de añadir al carrito. (Esto es lo más complicado de gestionar y entender).
  • Se añade el script de funcionamiento de swiper igualmente con una clase única para identificar a cada carrusel y que cada uno funcione de forma independiente. «$claseUnica»
  • Este script controla el responsive mediante el argumento «breakpoint»

* Es muy importante que consultes la documentación de las herramientas que se están utilizando porque hay muchos argumentos y funciones que puedes utilizar en este o en otros proyecto. Las posibilidades son infinitas.

Código CSS

Puedes añadir el CSS a product-carousel-slider.css, para este proyecto he utilizado este. Seguro que se puede optimizar mucho más.



¡Por fin! hemos creado un bloque completo y que cumple con todas las necesidades del proyecto. Espero haber explicado algunos tips interesantes que te ayuden con otros bloques que estés creando.

Próximas actualizaciones del bloque:

  • Estilos o variación del bloque para 4 o 5 productos por vista.
  • Estilos de las flechas o su posición. Trabajando con SVGs para controlar desde tamaños a colores.
  • Función para controlar en autoplay de cuantos en cuantos productos debe ir el delay. Por ejemplo, de 1, 4 o 5.
  • Sección de opciones creada con ACF donde le indicaremos con un campo de relación de páginas, donde queremos que se cargue el Javascript, esto es una buena técnica para mejorar el rendimiento. No queremos cargar swiper.js en páginas donde no haya un carrusel.
  • WP_Query tiene cientos de argumentos y combinaciones que se pueden integrar dentro del bloque para darle mas opciones al editor de la web. Por ejemplo controlar el orden del contenido, filtrar por etiquetas, ordenar por precio etc etc.
  • Para una futura versión es interesante añadir una integración con la API View transitions, para darle un estilo mas profesional haciendo transiciones de imágenes y textos.

Sobre el autor

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *