getEnvironment(); // Add child theme templates directory to the Twig loader so {% include %} // directives in Twig templates can find other child theme templates. $loader = $env->getLoader(); if ( $loader instanceof \Twig\Loader\FilesystemLoader ) { $template_dir = WC_BOOTSTRAP_PATH . 'templates'; if ( is_dir( $template_dir ) ) { $loader->prependPath( $template_dir ); } } // Register WooCommerce functions and filters as Twig extensions. $env->addExtension( new \WcBootstrap\Twig\WooCommerceExtension() ); // Register template interception hooks. $override = new \WcBootstrap\TemplateOverride(); $override->register(); } add_action( 'init', 'wc_bootstrap_init_twig_bridge', 20 ); /** * Enqueue child theme styles. * * Loads parent theme stylesheet first, then child theme overrides. * CSS cascade order: * 1. wp-bootstrap (parent) * 2. woocommerce (plugin styles) * 3. wc-bootstrap-style (child theme style.css) * 4. wc-bootstrap-overrides (plugin CSS overrides) */ function wc_bootstrap_enqueue_styles(): void { $theme_version = wp_get_theme()->get( 'Version' ); // Enqueue parent theme stylesheet. wp_enqueue_style( 'wp-bootstrap-style', get_template_directory_uri() . '/assets/css/style.min.css', array(), wp_get_theme( 'wp-bootstrap' )->get( 'Version' ) ); // Enqueue child theme stylesheet. wp_enqueue_style( 'wc-bootstrap-style', get_stylesheet_directory_uri() . '/style.css', array( 'wp-bootstrap-style' ), $theme_version ); // Enqueue plugin Bootstrap override styles. // Depend on plugin stylesheets so overrides always load after plugin CSS. wp_enqueue_style( 'wc-bootstrap-overrides', get_stylesheet_directory_uri() . '/assets/css/wc-bootstrap.css', array( 'wc-bootstrap-style', 'woocommerce-general' ), $theme_version ); } add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_styles' ); /** * Enqueue child theme scripts. * * @since 0.1.0 */ function wc_bootstrap_enqueue_scripts(): void { $theme_version = wp_get_theme()->get( 'Version' ); // Quantity +/- button handler for Bootstrap input-group widget. wp_enqueue_script( 'wc-bootstrap-quantity', get_stylesheet_directory_uri() . '/assets/js/quantity.js', array(), $theme_version, true ); } add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' ); /** * Handle plugin page rendering via plugin render filter. * * Delegates page rendering to the parent theme's TwigService so that plugin pages * share the same page shell (header, footer, layout) as native WordPress pages. * Falls back to letting the plugin handle rendering if the parent theme is not available. * * @param bool $rendered Whether the page has been rendered. * @param string $content Pre-rendered plugin HTML content. * @param array $context Plugin template context. * @return bool True if rendering was handled, false to let plugin use fallback. * * @since 0.1.0 */ function wc_bootstrap_render_page( bool $rendered, string $content, array $context ): bool { if ( ! class_exists( '\WPBootstrap\Twig\TwigService' ) || ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) { return false; // Can't render, let plugin use its own fallback } $context_builder = new \WPBootstrap\Template\ContextBuilder(); $theme_context = $context_builder->build(); $twig = \WPBootstrap\Twig\TwigService::getInstance(); // Inject plugin content as the page post content so page.html.twig renders it // inside the standard content block. Title is empty so the parent theme does not // render its own

— plugin templates handle their own headings. $theme_context['post'] = array_merge( $theme_context['post'] ?? [], [ 'content' => $content, 'title' => '', 'thumbnail' => '', ] ); echo $twig->render( 'pages/page.html.twig', $theme_context ); return true; } add_filter( 'woocommerce_render_page', 'wc_bootstrap_render_page', 10, 3 ); /** * Signal to the plugin that its content will be wrapped by the parent theme. * * When the parent theme's TwigService is available, the plugin templates should skip * their outer wrapper elements (breadcrumbs, sidebar, page shell) to avoid double-wrapping. * * @param bool $wrapped Whether content is theme-wrapped. * @return bool * * @since 0.1.0 */ function wc_bootstrap_is_wrapped( bool $wrapped ): bool { if ( class_exists( '\WPBootstrap\Twig\TwigService' ) ) { return true; } return $wrapped; } add_filter( 'woocommerce_is_theme_wrapped', 'wc_bootstrap_is_wrapped' ); /** * Add sticky header scroll shadow behavior. * * Toggles an 'is-stuck' class on the navbar when the page is scrolled, * adding a subtle box-shadow to visually separate the header from content. * * @since 0.1.0 */ 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 );