| @@ -12,8 +12,9 @@ | ||
| 12 | 12 | use FluentCart\App\Modules\Data\ProductQuery; |
| 13 | 13 | use FluentCart\App\Services\Renderer\ProductListRenderer; |
| 14 | 14 | use FluentCart\App\Services\Renderer\ProductRenderer; |
| 15 | 15 | use FluentCart\App\Services\Renderer\ShopAppRenderer; |
| 16 | +use FluentCart\App\Services\Schema\ProductSchema; | |
| 16 | 17 | use FluentCart\App\Services\TemplateService; |
| 17 | 18 | use FluentCart\Framework\Support\Arr; |
| 18 | 19 | |
| 19 | 20 | class TemplateActions |
| @@ -22,8 +23,16 @@ | ||
| 22 | 23 | { |
| 23 | 24 | add_action('fluent_cart/template/main_content', [$this, 'renderMainContent']); |
| 24 | 25 | add_action('fluent_cart/template/product_archive', [$this, 'renderProductArchive']); |
| 25 | 26 | add_action('fluent_cart/product/render_product_header', [$this, 'renderProductHeader']); |
| 27 | + // Block themes render reviews from the product template, so the | |
| 28 | + // built-in listener is not attached there — the hook still fires for | |
| 29 | + // third-party listeners. Safe to decide here: the theme is resolved | |
| 30 | + // at after_setup_theme, before init runs this. | |
| 31 | + if (!function_exists('wp_is_block_theme') || !wp_is_block_theme()) { | |
| 32 | + add_action('fluent_cart/product/after_product_content', [$this, 'renderProductReviews']); | |
| 33 | + } | |
| 34 | + add_action('wp_head', [$this, 'renderProductJsonLd']); | |
| 26 | 35 | |
| 27 | 36 | // Universal hook for after_product_content — fires on both classic and block themes |
| 28 | 37 | // Block themes render <!-- wp:post-content --> which runs the_content filter |
| 29 | 38 | add_filter('the_content', function ($content) { |
| @@ -46,9 +55,9 @@ | ||
| 46 | 55 | } |
| 47 | 56 | |
| 48 | 57 | return $content; |
| 49 | 58 | }, 999); |
| 50 | - | |
| 59 | + | |
| 51 | 60 | add_shortcode('fluent_cart_product_header', function ($atts = []) { |
| 52 | 61 | $atts = shortcode_atts([ |
| 53 | 62 | 'id' => 0, |
| 54 | 63 | ], $atts); |
| @@ -120,9 +129,10 @@ | ||
| 120 | 129 | ob_start(); |
| 121 | 130 | (new ProductListRenderer( |
| 122 | 131 | $products, |
| 123 | 132 | __('Related Products', 'fluent-cart'), |
| 124 | - 'fct-similar-product-list-container' | |
| 133 | + 'fct-similar-product-list-container', | |
| 134 | + ['rating_context' => 'relevant'] | |
| 125 | 135 | ))->render(); |
| 126 | 136 | |
| 127 | 137 | $content = ob_get_clean(); |
| 128 | 138 | |
| @@ -299,9 +309,10 @@ | ||
| 299 | 309 | ob_start(); |
| 300 | 310 | (new ProductListRenderer( |
| 301 | 311 | $products, |
| 302 | 312 | __('Related Products', 'fluent-cart'), |
| 303 | - 'fct-similar-product-list-container' | |
| 313 | + 'fct-similar-product-list-container', | |
| 314 | + ['rating_context' => 'relevant'] | |
| 304 | 315 | ))->render(); |
| 305 | 316 | |
| 306 | 317 | $relevantProducts = ob_get_clean(); |
| 307 | 318 | $content .= $relevantProducts; |
| @@ -348,7 +359,59 @@ | ||
| 348 | 359 | ], '', $content); |
| 349 | 360 | |
| 350 | 361 | // Remove extra whitespace between tags to clean up the output |
| 351 | 362 | return preg_replace('/>\s+</', '><', $cleaned); |
| 363 | + } | |
| 364 | + | |
| 365 | + /** | |
| 366 | + * Whether the product page should carry its review section at all. | |
| 367 | + * | |
| 368 | + * Sits beside the relevant-products switch in Product Page settings and | |
| 369 | + * works the same way, so one control governs the section wherever it is | |
| 370 | + * rendered from — a review block, or the append below. | |
| 371 | + * | |
| 372 | + * @param int $postId | |
| 373 | + * @return bool | |
| 374 | + */ | |
| 375 | + public static function shouldShowReviewsOnProductPage($postId): bool | |
| 376 | + { | |
| 377 | + $show = (new StoreSettings())->get('show_reviews_in_single_page', 'yes') == 'yes'; | |
| 378 | + | |
| 379 | + return (bool) apply_filters('fluent_cart/single_product_page/show_reviews', $show, $postId); | |
| 380 | + } | |
| 381 | + | |
| 382 | + private function isReviewModuleAvailable() | |
| 383 | + { | |
| 384 | + return \FluentCart\Api\ModuleSettings::isActive('reviews') | |
| 385 | + && class_exists(\FluentCart\App\Services\ProductReviewService::class); | |
| 386 | + } | |
| 387 | + | |
| 388 | + public function renderProductReviews($postId) | |
| 389 | + { | |
| 390 | + if (!$postId || !$this->isReviewModuleAvailable()) { | |
| 391 | + return; | |
| 392 | + } | |
| 393 | + | |
| 394 | + $renderer = new \FluentCart\App\Services\Renderer\ProductReviewRenderer($postId); | |
| 395 | + $renderer->render(); | |
| 396 | + $renderer->renderForm(); | |
| 397 | + } | |
| 398 | + | |
| 399 | + public function renderProductJsonLd() | |
| 400 | + { | |
| 401 | + if (!is_singular(\FluentCart\App\CPT\FluentProducts::CPT_NAME)) { | |
| 402 | + return; | |
| 403 | + } | |
| 404 | + | |
| 405 | + $postId = get_the_ID(); | |
| 406 | + if (!$postId) { | |
| 407 | + return; | |
| 408 | + } | |
| 409 | + | |
| 410 | + // The node itself — product, offers, aggregate rating, the reviews | |
| 411 | + // the page shows — is ProductSchema's; this is only the page gate. | |
| 412 | + // The reviews module gate lives there too, on the review parts | |
| 413 | + // alone: a product is for sale whether or not it takes reviews. | |
| 414 | + ProductSchema::render($postId); | |
| 352 | 415 | } |
| 353 | 416 | |
| 354 | 417 | } |