Add Bootstrap 5 product archive with card grid and sidebar

Replace WooCommerce's default shop/category page rendering with a
Bootstrap 5 card grid layout featuring responsive columns, sale badges,
star ratings, and an offcanvas sidebar for filters on mobile.

Key implementation details:

- Bypass parent theme's TemplateController for product archives via
  wp_bootstrap_should_render_template filter, render at template_redirect
  priority 11 using the same page shell injection pattern as plugin pages

- Add archive-product.php (Bootstrap layout with optional sidebar) and
  content-product.php (PHP bridge for wc_get_template_part interception)

- Inject global $product into Twig context in TemplateOverride to fix
  empty price/add-to-cart/rating/sale-flash in loop sub-templates — Twig
  has isolated variable scopes and cannot access PHP globals directly

- Fix pagination URLs: use get_pagenum_link() instead of ?page= query
  param (WordPress uses 'paged' for archive pagination, not 'page')

- Fix double-escaped – in result count by adding |raw filter

- Reset WooCommerce float-based layout CSS (woocommerce-layout.css) for
  shop pages to prevent conflicts with Bootstrap flex grid

- Register shop-sidebar widget area with Bootstrap-styled markup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 15:06:33 +01:00
parent 7034134678
commit 00872a6568
9 changed files with 416 additions and 23 deletions

View File

@@ -14,6 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Define theme constants.
*
@@ -223,3 +224,123 @@ function wc_bootstrap_sticky_header_script(): void {
<?php
}
add_action( 'wp_footer', 'wc_bootstrap_sticky_header_script' );
/**
* Register the shop sidebar widget area.
*
* Provides a widget area for product filters and shop-specific widgets.
* Uses Bootstrap-styled markup matching the parent theme's sidebar pattern.
*
* @since 0.1.0
*/
function wc_bootstrap_register_sidebars(): void {
register_sidebar( array(
'name' => __( 'Shop Sidebar', 'wc-bootstrap' ),
'id' => 'shop-sidebar',
'description' => __( 'Add widgets here to appear in the shop sidebar.', 'wc-bootstrap' ),
'before_widget' => '<div id="%1$s" class="widget mb-4 %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="sidebar-heading h6 text-uppercase fw-semibold">',
'after_title' => '</h3>',
) );
}
add_action( 'widgets_init', 'wc_bootstrap_register_sidebars' );
/**
* Set the number of product columns in the shop loop.
*
* @return int Number of columns.
* @since 0.1.0
*/
function wc_bootstrap_loop_columns(): int {
return 4;
}
add_filter( 'loop_shop_columns', 'wc_bootstrap_loop_columns' );
/**
* Remove WooCommerce's default sidebar hook.
*
* The child theme's archive-product.php renders the sidebar inline within the
* Bootstrap grid layout, so the default woocommerce_sidebar hook must not render
* a second sidebar outside the layout.
*
* @since 0.1.0
*/
function wc_bootstrap_remove_default_sidebar(): void {
remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );
}
add_action( 'init', 'wc_bootstrap_remove_default_sidebar' );
/**
* Prevent the parent theme from rendering product archives.
*
* The parent theme's TemplateController hooks template_redirect at priority 10
* and renders pages/archive.html.twig for all archives (then exits). For product
* archives, we need to render our own WooCommerce-specific Bootstrap layout instead.
* Returning false tells the parent theme to skip rendering for this request.
*
* @param bool $should_render Whether the parent theme should render.
* @return bool
*
* @since 0.1.0
*/
function wc_bootstrap_skip_parent_archive( bool $should_render ): bool {
if ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) {
return false;
}
if ( function_exists( 'is_product_taxonomy' ) && is_product_taxonomy() ) {
return false;
}
return $should_render;
}
add_filter( 'wp_bootstrap_should_render_template', 'wc_bootstrap_skip_parent_archive' );
/**
* Render product archive pages with Bootstrap 5 layout.
*
* Since the parent theme's TemplateController is blocked for product archives
* (via wp_bootstrap_should_render_template filter), we render the page ourselves
* at priority 11 using the parent theme's TwigService and page shell.
*
* The archive-product.php file provides the Bootstrap layout (sidebar + product
* grid) and is captured via output buffering, then injected into the parent
* theme's page template — the same pattern as wc_bootstrap_render_page().
*
* @since 0.1.0
*/
function wc_bootstrap_render_product_archive(): void {
$is_shop = is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) );
$is_tax = function_exists( 'is_product_taxonomy' ) && is_product_taxonomy();
if ( ! $is_shop && ! $is_tax ) {
return;
}
if ( ! class_exists( '\WPBootstrap\Twig\TwigService' )
|| ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) {
return;
}
// Capture WooCommerce archive content via output buffering.
ob_start();
include get_stylesheet_directory() . '/archive-product.php';
$content = ob_get_clean();
// Build parent theme context and inject archive content into page shell.
$context_builder = new \WPBootstrap\Template\ContextBuilder();
$theme_context = $context_builder->build();
$twig = \WPBootstrap\Twig\TwigService::getInstance();
$theme_context['post'] = array_merge(
$theme_context['post'] ?? [],
[
'content' => $content,
'title' => '',
'thumbnail' => '',
]
);
echo $twig->render( 'pages/page.html.twig', $theme_context );
exit;
}
add_action( 'template_redirect', 'wc_bootstrap_render_product_archive', 11 );