You've already forked wc-composable-product
Add custom page template for composable products, bump to v1.3.2
All checks were successful
All checks were successful
- Custom WooCommerce template with compact header + full-width selector - Twig layout template (single-product-composable.html.twig) + PHP loader - Body class 'single-product-composable' for CSS scoping - Renamed *.twig to *.html.twig (proper naming convention) - Refreshed .pot with accurate file refs, merged all .po files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
83
templates/content-single-product-composable.php
Normal file
83
templates/content-single-product-composable.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom single product template for composable products.
|
||||
*
|
||||
* Replaces the standard WooCommerce two-column layout (image + summary) with
|
||||
* a compact header and full-width product selector.
|
||||
*
|
||||
* This is a thin PHP loader that captures WooCommerce hook output and passes
|
||||
* it to the Twig template for rendering.
|
||||
*
|
||||
* @package Magdev\WcComposableProduct
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
global $product;
|
||||
|
||||
if ( ! $product || ! is_a( $product, 'WC_Product' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook.
|
||||
do_action( 'woocommerce_before_single_product' );
|
||||
|
||||
if ( post_password_required() ) {
|
||||
echo get_the_password_form(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WP core function.
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily remove standard WooCommerce summary hooks — we render title,
|
||||
// price, and description in the compact header instead. Our product selector
|
||||
// (CartHandler::render_product_selector at priority 25) stays attached.
|
||||
$hooks_to_remove = array(
|
||||
array( 'woocommerce_template_single_title', 5 ),
|
||||
array( 'woocommerce_template_single_rating', 10 ),
|
||||
array( 'woocommerce_template_single_price', 10 ),
|
||||
array( 'woocommerce_template_single_excerpt', 20 ),
|
||||
array( 'woocommerce_template_single_add_to_cart', 30 ),
|
||||
array( 'woocommerce_template_single_meta', 40 ),
|
||||
array( 'woocommerce_template_single_sharing', 50 ),
|
||||
);
|
||||
|
||||
foreach ( $hooks_to_remove as $hook ) {
|
||||
remove_action( 'woocommerce_single_product_summary', $hook[0], $hook[1] );
|
||||
}
|
||||
|
||||
// Capture product selector output (our hook at priority 25 + structured data at 60).
|
||||
ob_start();
|
||||
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook.
|
||||
do_action( 'woocommerce_single_product_summary' );
|
||||
$selector_html = ob_get_clean();
|
||||
|
||||
// Restore removed hooks.
|
||||
foreach ( $hooks_to_remove as $hook ) {
|
||||
add_action( 'woocommerce_single_product_summary', $hook[0], $hook[1] );
|
||||
}
|
||||
|
||||
// Capture after-summary output (product tabs, related products, etc.).
|
||||
ob_start();
|
||||
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook.
|
||||
do_action( 'woocommerce_after_single_product_summary' );
|
||||
$after_summary_html = ob_get_clean();
|
||||
|
||||
// Build template context.
|
||||
$image_id = $product->get_image_id();
|
||||
$context = array(
|
||||
'product_id' => $product->get_id(),
|
||||
'product_name' => $product->get_name(),
|
||||
'price_html' => $product->get_price_html(),
|
||||
'short_description' => $product->get_short_description(),
|
||||
'image_html' => $image_id ? wp_get_attachment_image( $image_id, array( 100, 100 ), false, array( 'class' => 'composable-header-image' ) ) : '',
|
||||
'permalink' => get_permalink( $product->get_id() ),
|
||||
'product_class' => implode( ' ', wc_get_product_class( 'composable-product-layout', $product ) ),
|
||||
'selector_html' => $selector_html,
|
||||
'after_summary_html' => $after_summary_html,
|
||||
);
|
||||
|
||||
$plugin = \Magdev\WcComposableProduct\Plugin::instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped by Twig template.
|
||||
echo $plugin->render_template( 'single-product-composable.html.twig', $context );
|
||||
|
||||
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook.
|
||||
do_action( 'woocommerce_after_single_product' );
|
||||
37
templates/single-product-composable.html.twig
Normal file
37
templates/single-product-composable.html.twig
Normal file
@@ -0,0 +1,37 @@
|
||||
{# Custom single product template for composable products #}
|
||||
<div id="product-{{ product_id }}" class="{{ product_class }}">
|
||||
|
||||
{# Compact product header — replaces the large image gallery #}
|
||||
<div class="composable-product-header">
|
||||
{% if image_html %}
|
||||
<div class="composable-header-thumbnail">
|
||||
{{ image_html|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="composable-header-info">
|
||||
<h1 class="product_title entry-title">{{ product_name|esc_html }}</h1>
|
||||
|
||||
{% if price_html %}
|
||||
<div class="composable-header-price price">
|
||||
{{ price_html|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if short_description %}
|
||||
<div class="composable-header-description">
|
||||
{{ short_description|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Full-width product selector #}
|
||||
<div class="composable-selector-area">
|
||||
{{ selector_html|raw }}
|
||||
</div>
|
||||
|
||||
{# WooCommerce after-summary content (tabs, related products) #}
|
||||
{{ after_summary_html|raw }}
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user