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 ); // Product gallery thumbnail handler for single product pages. if ( function_exists( 'is_product' ) && is_product() ) { wp_enqueue_script( 'wc-bootstrap-gallery', get_stylesheet_directory_uri() . '/assets/js/product-gallery.js', array(), $theme_version, true ); } } add_action( 'wp_enqueue_scripts', 'wc_bootstrap_enqueue_scripts' ); /** * Build the parent theme context for a page render. * * Caches the ContextBuilder result per request to avoid redundant database * queries when multiple WooCommerce rendering functions need the same context. * * @since 0.1.1 * * @return array Theme context array. */ function wc_bootstrap_get_theme_context(): array { static $cached_context = null; if ( null === $cached_context ) { $context_builder = new \WPBootstrap\Template\ContextBuilder(); $cached_context = $context_builder->build(); } return $cached_context; } /** * Render content inside the parent theme's page shell. * * Injects the given HTML content into the parent theme's page template, * replacing the post content. Title and thumbnail are blanked so the * parent theme does not render its own headings — the content handles that. * * @since 0.1.1 * * @param string $content HTML content to render inside the page shell. */ function wc_bootstrap_render_in_page_shell( string $content ): void { $theme_context = wc_bootstrap_get_theme_context(); $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 ); } /** * 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; } wc_bootstrap_render_in_page_shell( $content ); 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 3; } 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' ); /** * Replace WooCommerce's content wrapper with a no-op. * * The parent theme's page shell already wraps content in a .container, * so WooCommerce's default wrapper (another .container + #primary +
) * creates a double-nested container that constrains width. Remove it and * let the parent theme handle the outer layout. * * @since 0.1.0 */ function wc_bootstrap_remove_content_wrappers(): void { remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); } add_action( 'init', 'wc_bootstrap_remove_content_wrappers' ); /** * Prevent the parent theme from rendering WooCommerce pages. * * The parent theme's TemplateController hooks template_redirect at priority 10 * and renders its own templates for all requests (then exits). For WooCommerce * pages we need to render our own Bootstrap layouts 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_template( 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; } if ( function_exists( 'is_product' ) && is_product() ) { return false; } return $should_render; } add_filter( 'wp_bootstrap_should_render_template', 'wc_bootstrap_skip_parent_template' ); /** * Disable WooCommerce's block template compatibility layer. * * The parent theme has theme.json which makes wp_is_block_theme() return true. * WooCommerce detects this and removes classic template hooks (title, price, * add-to-cart, etc.) from single product and archive pages, expecting blocks * to handle rendering instead. Since we render via classic hooks + Twig, we * need the hooks to stay registered. * * @param bool $disabled Whether the compatibility layer is disabled. * @return bool * * @since 0.1.0 */ function wc_bootstrap_disable_block_compatibility( bool $disabled ): bool { return true; } add_filter( 'woocommerce_disable_compatibility_layer', 'wc_bootstrap_disable_block_compatibility' ); /** * 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; } ob_start(); include get_stylesheet_directory() . '/woocommerce/archive-product.php'; $content = ob_get_clean(); wc_bootstrap_render_in_page_shell( $content ); exit; } add_action( 'template_redirect', 'wc_bootstrap_render_product_archive', 11 ); /** * Render single product pages with Bootstrap 5 layout. * * Since the parent theme's TemplateController is blocked for single products * (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 single-product.php file fires WooCommerce hooks and is captured via output * buffering, then injected into the parent theme's page template — the same * pattern as wc_bootstrap_render_product_archive(). * * @since 0.1.0 */ function wc_bootstrap_render_single_product(): void { if ( ! function_exists( 'is_product' ) || ! is_product() ) { return; } if ( ! class_exists( '\WPBootstrap\Twig\TwigService' ) || ! class_exists( '\WPBootstrap\Template\ContextBuilder' ) ) { return; } ob_start(); include get_stylesheet_directory() . '/woocommerce/single-product.php'; $content = ob_get_clean(); wc_bootstrap_render_in_page_shell( $content ); exit; } add_action( 'template_redirect', 'wc_bootstrap_render_single_product', 11 );