diff --git a/archive-product.php b/archive-product.php
new file mode 100644
index 0000000..f2e99c7
--- /dev/null
+++ b/archive-product.php
@@ -0,0 +1,104 @@
+
+
+
+
+
+ markup.
+ *
+ * WooCommerce's wc_get_template_part('content', 'product') uses locate_template()
+ * which finds this file in the child theme before falling back to the plugin template.
+ * Unlike wc_get_template(), wc_get_template_part() does NOT fire the
+ * woocommerce_before_template_part / woocommerce_after_template_part hooks,
+ * so the TemplateOverride class cannot intercept it — this bridge file is needed.
+ *
+ * @package WcBootstrap
+ * @since 0.1.0
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+global $product;
+
+// Ensure the product is valid and visible (same guard as WooCommerce's default template).
+if ( ! is_a( $product, WC_Product::class ) || ! $product->is_visible() ) {
+ return;
+}
+
+if ( class_exists( '\WPBootstrap\Twig\TwigService' ) ) {
+ $twig = \WPBootstrap\Twig\TwigService::getInstance();
+ echo $twig->render( 'content-product.html.twig', [] );
+} else {
+ // Fallback: include WooCommerce's default template directly.
+ include WC()->plugin_path() . '/templates/content-product.php';
+}
diff --git a/functions.php b/functions.php
index 07abe74..e94f253 100644
--- a/functions.php
+++ b/functions.php
@@ -14,6 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
+
/**
* Define theme constants.
*
@@ -223,3 +224,123 @@ function wc_bootstrap_sticky_header_script(): void {
__( 'Shop Sidebar', 'wc-bootstrap' ),
+ 'id' => 'shop-sidebar',
+ 'description' => __( 'Add widgets here to appear in the shop sidebar.', 'wc-bootstrap' ),
+ 'before_widget' => '
',
+ 'after_widget' => '
',
+ 'before_title' => '',
+ ) );
+}
+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 );
diff --git a/inc/TemplateOverride.php b/inc/TemplateOverride.php
index 20ab7ee..5406b34 100644
--- a/inc/TemplateOverride.php
+++ b/inc/TemplateOverride.php
@@ -78,8 +78,17 @@ class TemplateOverride {
}
try {
- $twig = TwigService::getInstance();
- echo $twig->render( $twigTemplate, $args );
+ $twig = TwigService::getInstance();
+ $context = $args;
+
+ // Inject the global $product into the Twig context.
+ // WooCommerce PHP templates access it via `global $product;` but Twig
+ // templates have isolated variable scopes and need it passed explicitly.
+ if ( ! isset( $context['product'] ) && ! empty( $GLOBALS['product'] ) ) {
+ $context['product'] = $GLOBALS['product'];
+ }
+
+ echo $twig->render( $twigTemplate, $context );
// Buffer the upcoming PHP include so we can discard it.
ob_start();
diff --git a/templates/components/pagination.html.twig b/templates/components/pagination.html.twig
index bda0663..b004f5c 100644
--- a/templates/components/pagination.html.twig
+++ b/templates/components/pagination.html.twig
@@ -16,7 +16,7 @@