1035 lines
35 KiB
PHP
1035 lines
35 KiB
PHP
<?php
|
|
|
|
namespace WooNooW\Frontend;
|
|
|
|
use WooNooW\Frontend\PageSSR;
|
|
use WooNooW\Frontend\PlaceholderRenderer;
|
|
|
|
/**
|
|
* Template Override
|
|
* Overrides WooCommerce templates to use WooNooW SPA
|
|
*/
|
|
class TemplateOverride
|
|
{
|
|
|
|
/**
|
|
* Initialize
|
|
*/
|
|
public static function init()
|
|
{
|
|
// Register rewrite rules for BrowserRouter SEO (must be on 'init')
|
|
add_action('init', [__CLASS__, 'register_spa_rewrite_rules']);
|
|
|
|
// Flush rewrite rules when relevant settings change
|
|
add_action('update_option_woonoow_appearance_settings', function ($old_value, $new_value) {
|
|
$old_general = $old_value['general'] ?? [];
|
|
$new_general = $new_value['general'] ?? [];
|
|
|
|
// Only flush if spa_mode, spa_page, or use_browser_router changed
|
|
$needs_flush =
|
|
($old_general['spa_mode'] ?? '') !== ($new_general['spa_mode'] ?? '') ||
|
|
($old_general['spa_page'] ?? '') !== ($new_general['spa_page'] ?? '') ||
|
|
($old_general['use_browser_router'] ?? true) !== ($new_general['use_browser_router'] ?? true);
|
|
|
|
if ($needs_flush) {
|
|
flush_rewrite_rules();
|
|
}
|
|
}, 10, 2);
|
|
|
|
// Redirect WooCommerce pages to SPA routes early (before template loads)
|
|
add_action('template_redirect', [__CLASS__, 'redirect_wc_pages_to_spa'], 5);
|
|
|
|
// Serve SPA directly for frontpage routes (priority 1 = very early, before WC)
|
|
add_action('template_redirect', [__CLASS__, 'serve_spa_for_frontpage_routes'], 1);
|
|
|
|
// Serve SSR for bots on pages/CPT with WooNooW structure (priority 2 = after frontpage check)
|
|
add_action('template_redirect', [__CLASS__, 'maybe_serve_ssr_for_bots'], 2);
|
|
|
|
// Hook to wp_loaded with priority 10 (BEFORE WooCommerce's priority 20)
|
|
// This ensures we process add-to-cart before WooCommerce does
|
|
add_action('wp_loaded', [__CLASS__, 'intercept_add_to_cart'], 10);
|
|
|
|
// Use blank template for full-page SPA
|
|
add_filter('template_include', [__CLASS__, 'use_spa_template'], 999);
|
|
|
|
// Disable canonical redirects for SPA routes
|
|
add_filter('redirect_canonical', [__CLASS__, 'disable_canonical_redirect'], 10, 2);
|
|
|
|
// Override WooCommerce shop page
|
|
add_filter('woocommerce_show_page_title', '__return_false');
|
|
|
|
// Replace WooCommerce content with our SPA
|
|
add_action('woocommerce_before_main_content', [__CLASS__, 'start_spa_wrapper'], 5);
|
|
add_action('woocommerce_after_main_content', [__CLASS__, 'end_spa_wrapper'], 999);
|
|
|
|
// Remove WooCommerce default content
|
|
remove_action('woocommerce_before_shop_loop', 'woocommerce_result_count', 20);
|
|
remove_action('woocommerce_before_shop_loop', 'woocommerce_catalog_ordering', 30);
|
|
remove_action('woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10);
|
|
remove_action('woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10);
|
|
|
|
// Override single product template
|
|
add_filter('woocommerce_locate_template', [__CLASS__, 'override_template'], 10, 3);
|
|
|
|
// Remove theme header and footer when SPA is active
|
|
add_action('get_header', [__CLASS__, 'remove_theme_header']);
|
|
add_action('get_footer', [__CLASS__, 'remove_theme_footer']);
|
|
}
|
|
|
|
/**
|
|
* Register rewrite rules for BrowserRouter SEO
|
|
* Catches all SPA routes and serves the SPA page
|
|
*/
|
|
public static function register_spa_rewrite_rules()
|
|
{
|
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
|
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
|
|
|
// Check if BrowserRouter is enabled (default: true for new installs)
|
|
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
|
|
|
|
if (!$spa_page_id || !$use_browser_router) {
|
|
return;
|
|
}
|
|
|
|
$spa_page = get_post($spa_page_id);
|
|
if (!$spa_page) {
|
|
return;
|
|
}
|
|
|
|
$spa_slug = $spa_page->post_name;
|
|
|
|
// Check if SPA page is set as WordPress frontpage
|
|
$frontpage_id = (int) get_option('page_on_front');
|
|
$is_spa_frontpage = $frontpage_id && $frontpage_id === (int) $spa_page_id;
|
|
|
|
if ($is_spa_frontpage) {
|
|
// When SPA is frontpage, add root-level routes
|
|
// /shop, /shop/* → SPA page
|
|
add_rewrite_rule(
|
|
'^shop/?$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=shop',
|
|
'top'
|
|
);
|
|
add_rewrite_rule(
|
|
'^shop/(.*)$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=shop/$matches[1]',
|
|
'top'
|
|
);
|
|
|
|
// /product/* → SPA page
|
|
add_rewrite_rule(
|
|
'^product/(.*)$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=product/$matches[1]',
|
|
'top'
|
|
);
|
|
|
|
// /cart → SPA page
|
|
add_rewrite_rule(
|
|
'^cart/?$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=cart',
|
|
'top'
|
|
);
|
|
|
|
// /checkout, /checkout/* → SPA page
|
|
add_rewrite_rule(
|
|
'^checkout/?$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=checkout',
|
|
'top'
|
|
);
|
|
add_rewrite_rule(
|
|
'^checkout/(.*)$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=checkout/$matches[1]',
|
|
'top'
|
|
);
|
|
|
|
// /my-account, /my-account/* → SPA page
|
|
add_rewrite_rule(
|
|
'^my-account/?$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=my-account',
|
|
'top'
|
|
);
|
|
add_rewrite_rule(
|
|
'^my-account/(.*)$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=my-account/$matches[1]',
|
|
'top'
|
|
);
|
|
|
|
// /login, /register, /reset-password → SPA page
|
|
add_rewrite_rule(
|
|
'^login/?$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=login',
|
|
'top'
|
|
);
|
|
add_rewrite_rule(
|
|
'^register/?$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=register',
|
|
'top'
|
|
);
|
|
add_rewrite_rule(
|
|
'^reset-password/?$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=reset-password',
|
|
'top'
|
|
);
|
|
|
|
// /order-pay/* → SPA page
|
|
add_rewrite_rule(
|
|
'^order-pay/(.*)$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=order-pay/$matches[1]',
|
|
'top'
|
|
);
|
|
|
|
// /order-pay/* → SPA page
|
|
add_rewrite_rule(
|
|
'^order-pay/(.*)$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=order-pay/$matches[1]',
|
|
'top'
|
|
);
|
|
|
|
// /order-pay/* → SPA page (moved to checkout/pay/ in new structure)
|
|
// Removed direct order-pay rule to favor checkout subpath
|
|
|
|
} else {
|
|
// Rewrite /slug to serve the SPA page (base URL)
|
|
add_rewrite_rule(
|
|
'^' . preg_quote($spa_slug, '/') . '/?$',
|
|
'index.php?page_id=' . $spa_page_id,
|
|
'top'
|
|
);
|
|
|
|
// Rewrite /slug/anything to serve the SPA page with path
|
|
// React Router handles the path after that
|
|
add_rewrite_rule(
|
|
'^' . preg_quote($spa_slug, '/') . '/(.*)$',
|
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=$matches[1]',
|
|
'top'
|
|
);
|
|
}
|
|
|
|
// Register query var for the SPA path
|
|
add_filter('query_vars', function ($vars) {
|
|
$vars[] = 'woonoow_spa_path';
|
|
return $vars;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Intercept add-to-cart redirect (NOT the add-to-cart itself)
|
|
* Let WooCommerce handle the cart operation properly, we just redirect afterward
|
|
*
|
|
* This is the proper approach - WooCommerce manages sessions correctly,
|
|
* we just customize where the redirect goes.
|
|
*/
|
|
public static function intercept_add_to_cart()
|
|
{
|
|
// Only act if add-to-cart is present
|
|
if (!isset($_GET['add-to-cart'])) {
|
|
return;
|
|
}
|
|
|
|
// Get SPA page from appearance settings
|
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
|
$spa_page_id = isset($appearance_settings['general']['spa_page']) ? $appearance_settings['general']['spa_page'] : 0;
|
|
|
|
if (!$spa_page_id) {
|
|
return; // No SPA page configured, let WooCommerce handle everything
|
|
}
|
|
|
|
// Hook into WooCommerce's redirect filter AFTER it adds to cart
|
|
// This is the proper way to customize the redirect destination
|
|
add_filter('woocommerce_add_to_cart_redirect', function ($url) use ($spa_page_id) {
|
|
// Get redirect parameter from original request
|
|
$redirect_to = isset($_GET['redirect']) ? sanitize_text_field($_GET['redirect']) : 'cart';
|
|
|
|
// Build redirect URL with hash route for SPA
|
|
$redirect_url = get_permalink($spa_page_id);
|
|
|
|
// Determine hash route based on redirect parameter
|
|
$hash_route = '/cart'; // Default
|
|
if ($redirect_to === 'checkout') {
|
|
$hash_route = '/checkout';
|
|
} elseif ($redirect_to === 'shop') {
|
|
$hash_route = '/shop';
|
|
}
|
|
|
|
// Return the SPA URL with hash route
|
|
return trailingslashit($redirect_url) . '#' . $hash_route;
|
|
}, 999);
|
|
|
|
// Prevent caching
|
|
add_action('template_redirect', function () {
|
|
nocache_headers();
|
|
}, 1);
|
|
}
|
|
|
|
/**
|
|
* Redirect WooCommerce pages to SPA routes
|
|
* Maps: /shop → /store/, /cart → /store/cart, etc.
|
|
*/
|
|
public static function redirect_wc_pages_to_spa()
|
|
{
|
|
// Get SPA settings
|
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
|
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
|
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
|
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
|
|
|
|
// Only redirect when SPA mode is 'full'
|
|
if ($spa_mode !== 'full') {
|
|
return;
|
|
}
|
|
|
|
if (!$spa_page_id) {
|
|
return; // No SPA page configured
|
|
}
|
|
|
|
// Skip if SPA is set as frontpage (serve_spa_for_frontpage_routes handles it)
|
|
$frontpage_id = (int) get_option('page_on_front');
|
|
if ($frontpage_id && $frontpage_id === (int) $spa_page_id) {
|
|
return;
|
|
}
|
|
|
|
// Already on SPA page, don't redirect
|
|
global $post;
|
|
if ($post && $post->ID == $spa_page_id) {
|
|
return;
|
|
}
|
|
|
|
$spa_url = trailingslashit(get_permalink($spa_page_id));
|
|
|
|
// Helper function to build route URL based on router type
|
|
$build_route = function ($path) use ($spa_url, $use_browser_router) {
|
|
if ($use_browser_router) {
|
|
// Path format: /store/cart
|
|
return $spa_url . ltrim($path, '/');
|
|
}
|
|
// Hash format: /store/#/cart
|
|
return rtrim($spa_url, '/') . '#/' . ltrim($path, '/');
|
|
};
|
|
|
|
// Check which WC page we're on and redirect
|
|
if (is_shop()) {
|
|
wp_redirect($build_route('shop'), 302);
|
|
exit;
|
|
}
|
|
|
|
if (is_product()) {
|
|
// Use get_queried_object() which returns the WP_Post, then get slug
|
|
$product_post = get_queried_object();
|
|
if ($product_post && isset($product_post->post_name)) {
|
|
$slug = $product_post->post_name;
|
|
wp_redirect($build_route('product/' . $slug), 302);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if (is_cart()) {
|
|
wp_redirect($build_route('cart'), 302);
|
|
exit;
|
|
}
|
|
|
|
if (is_checkout() && !is_order_received_page()) {
|
|
// Check for order-pay endpoint
|
|
if (is_wc_endpoint_url('order-pay')) {
|
|
global $wp;
|
|
$order_id = $wp->query_vars['order-pay'];
|
|
wp_redirect($build_route('order-pay/' . $order_id), 302);
|
|
exit;
|
|
}
|
|
|
|
wp_redirect($build_route('checkout'), 302);
|
|
exit;
|
|
}
|
|
|
|
if (is_account_page()) {
|
|
wp_redirect($build_route('my-account'), 302);
|
|
exit;
|
|
}
|
|
|
|
// Redirect structural pages with WooNooW sections to SPA
|
|
if (is_singular('page') && $post) {
|
|
// Skip the SPA page itself and frontpage
|
|
if ($post->ID == $spa_page_id || $post->ID == $frontpage_id) {
|
|
return;
|
|
}
|
|
|
|
// Check if page has WooNooW structure
|
|
$structure = get_post_meta($post->ID, '_wn_page_structure', true);
|
|
if (!empty($structure) && !empty($structure['sections'])) {
|
|
// Redirect to SPA with page slug route
|
|
$page_slug = $post->post_name;
|
|
wp_redirect($build_route($page_slug), 302);
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serve SPA template directly for frontpage SPA routes
|
|
* When SPA page is set as WordPress frontpage, intercept known routes
|
|
* and serve the SPA template directly (bypasses WooCommerce templates)
|
|
*/
|
|
/**
|
|
* Serve SPA template directly for frontpage SPA routes
|
|
* When SPA page is set as WordPress frontpage, intercept known routes
|
|
* and serve the SPA template directly (bypasses WooCommerce templates)
|
|
*/
|
|
public static function serve_spa_for_frontpage_routes()
|
|
{
|
|
// Get SPA settings
|
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
|
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
|
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
|
|
|
// Only run in full SPA mode
|
|
if ($spa_mode !== 'full' || !$spa_page_id) {
|
|
return;
|
|
}
|
|
|
|
// Check if SPA page is set as WordPress frontpage
|
|
$frontpage_id = (int) get_option('page_on_front');
|
|
if (!$frontpage_id || $frontpage_id !== (int) $spa_page_id) {
|
|
return; // SPA is not frontpage, let normal routing handle it
|
|
}
|
|
|
|
// Get the current request path relative to site root
|
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '/';
|
|
$home_path = parse_url(home_url(), PHP_URL_PATH);
|
|
|
|
// Normalize request URI for subdirectory installs
|
|
if ($home_path && $home_path !== '/') {
|
|
$home_path = rtrim($home_path, '/');
|
|
if (strpos($request_uri, $home_path) === 0) {
|
|
$request_uri = substr($request_uri, strlen($home_path));
|
|
if (empty($request_uri)) $request_uri = '/';
|
|
}
|
|
}
|
|
|
|
$path = parse_url($request_uri, PHP_URL_PATH);
|
|
$path = '/' . trim($path, '/');
|
|
|
|
// Define SPA routes that should be intercepted when SPA is frontpage
|
|
$spa_routes = [
|
|
'/', // Frontpage itself
|
|
'/shop', // Shop page
|
|
'/cart', // Cart page
|
|
'/checkout', // Checkout page
|
|
'/my-account', // Account page
|
|
'/login', // Login page
|
|
'/register', // Register page
|
|
'/register', // Register page
|
|
'/reset-password', // Password reset
|
|
'/order-pay', // Order pay page
|
|
];
|
|
|
|
// Check for exact matches or path prefixes
|
|
$should_serve_spa = false;
|
|
|
|
// Check exact matches
|
|
if (in_array($path, $spa_routes)) {
|
|
$should_serve_spa = true;
|
|
}
|
|
|
|
// Check path prefixes (for sub-routes)
|
|
$prefix_routes = ['/shop/', '/my-account/', '/product/', '/checkout/'];
|
|
foreach ($prefix_routes as $prefix) {
|
|
if (strpos($path, $prefix) === 0) {
|
|
$should_serve_spa = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check for structural pages with WooNooW sections
|
|
if (!$should_serve_spa && !empty($path) && $path !== '/') {
|
|
// Try to find a page by slug matching the path
|
|
$slug = trim($path, '/');
|
|
|
|
// Handle nested slugs (get the last part as the page slug)
|
|
if (strpos($slug, '/') !== false) {
|
|
$slug_parts = explode('/', $slug);
|
|
$slug = end($slug_parts);
|
|
}
|
|
|
|
$page = get_page_by_path($slug);
|
|
if ($page) {
|
|
// Check if this page has WooNooW structure
|
|
$structure = get_post_meta($page->ID, '_wn_page_structure', true);
|
|
if (!empty($structure) && !empty($structure['sections'])) {
|
|
$should_serve_spa = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Not a SPA route
|
|
if (!$should_serve_spa) {
|
|
return;
|
|
}
|
|
|
|
// Prevent caching for dynamic SPA content
|
|
nocache_headers();
|
|
|
|
// Load the SPA template directly and exit
|
|
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
|
if (file_exists($spa_template)) {
|
|
// Set up minimal WordPress environment for the template
|
|
status_header(200);
|
|
|
|
// Define constant to tell Assets to load unconditionally
|
|
if (!defined('WOONOOW_SERVE_SPA')) {
|
|
define('WOONOOW_SERVE_SPA', true);
|
|
}
|
|
|
|
// Include the SPA template
|
|
include $spa_template;
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable canonical redirects for SPA routes
|
|
* This prevents WordPress from redirecting /product/slug URLs
|
|
*/
|
|
public static function disable_canonical_redirect($redirect_url, $requested_url)
|
|
{
|
|
$settings = get_option('woonoow_appearance_settings', []);
|
|
$mode = isset($settings['general']['spa_mode']) ? $settings['general']['spa_mode'] : 'disabled';
|
|
|
|
// Only disable redirects in full SPA mode
|
|
if ($mode !== 'full') {
|
|
return $redirect_url;
|
|
}
|
|
|
|
// Check if this is a SPA route
|
|
// We include /product/ and standard endpoints
|
|
$spa_routes = ['/product/', '/cart', '/checkout', '/my-account'];
|
|
|
|
foreach ($spa_routes as $route) {
|
|
if (strpos($requested_url, $route) !== false) {
|
|
// This is a SPA route, disable WordPress redirect
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $redirect_url;
|
|
}
|
|
|
|
/**
|
|
* Use SPA template (blank page)
|
|
*/
|
|
public static function use_spa_template($template)
|
|
{
|
|
// Check spa_mode from appearance settings FIRST
|
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
|
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
|
|
|
// If SPA is disabled, return original template immediately
|
|
if ($spa_mode === 'disabled') {
|
|
return $template;
|
|
}
|
|
|
|
// Check if current page is a designated SPA page
|
|
if (self::is_spa_page()) {
|
|
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
|
if (file_exists($spa_template)) {
|
|
return $spa_template;
|
|
}
|
|
}
|
|
|
|
// For spa_mode = 'full', override WooCommerce pages
|
|
if ($spa_mode === 'full') {
|
|
// Override all WooCommerce pages
|
|
if (is_woocommerce() || is_product() || is_cart() || is_checkout() || is_account_page()) {
|
|
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
|
if (file_exists($spa_template)) {
|
|
return $spa_template;
|
|
}
|
|
}
|
|
}
|
|
|
|
// For spa_mode = 'checkout_only'
|
|
if ($spa_mode === 'checkout_only') {
|
|
if (is_checkout() || is_order_received_page() || is_account_page() || is_cart()) {
|
|
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
|
if (file_exists($spa_template)) {
|
|
return $spa_template;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $template;
|
|
}
|
|
|
|
/**
|
|
* Start SPA wrapper
|
|
*/
|
|
public static function start_spa_wrapper()
|
|
{
|
|
// Check if we should use SPA
|
|
if (!self::should_use_spa()) {
|
|
return;
|
|
}
|
|
|
|
// Determine page type
|
|
$page_type = 'shop';
|
|
$data_attrs = 'data-page="shop"';
|
|
|
|
if (is_product()) {
|
|
$page_type = 'product';
|
|
global $post;
|
|
$data_attrs = 'data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
|
|
} elseif (is_cart()) {
|
|
$page_type = 'cart';
|
|
$data_attrs = 'data-page="cart"';
|
|
} elseif (is_checkout()) {
|
|
$page_type = 'checkout';
|
|
$data_attrs = 'data-page="checkout"';
|
|
} elseif (is_account_page()) {
|
|
$page_type = 'account';
|
|
$data_attrs = 'data-page="account"';
|
|
}
|
|
|
|
// Output SPA mount point
|
|
echo '<div id="woonoow-customer-app" ' . $data_attrs . '>';
|
|
echo '<div class="woonoow-loading">';
|
|
echo '<p>' . esc_html__('Loading...', 'woonoow') . '</p>';
|
|
echo '</div>';
|
|
echo '</div>';
|
|
|
|
// Hide WooCommerce content
|
|
echo '<div style="display: none;">';
|
|
}
|
|
|
|
/**
|
|
* End SPA wrapper
|
|
*/
|
|
public static function end_spa_wrapper()
|
|
{
|
|
if (!self::should_use_spa()) {
|
|
return;
|
|
}
|
|
|
|
// Close hidden wrapper
|
|
echo '</div>';
|
|
}
|
|
|
|
/**
|
|
* Check if we should use SPA
|
|
*/
|
|
private static function should_use_spa()
|
|
{
|
|
// Check spa_mode from appearance settings
|
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
|
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
|
|
|
// Only use SPA when mode is 'full'
|
|
if ($spa_mode !== 'full') {
|
|
return false;
|
|
}
|
|
|
|
// For full SPA mode, use SPA on WooCommerce pages
|
|
if (is_shop() || is_product() || is_cart() || is_checkout() || is_account_page()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove theme header when SPA is active
|
|
*/
|
|
public static function remove_theme_header()
|
|
{
|
|
if (self::should_remove_theme_elements()) {
|
|
remove_all_actions('wp_head');
|
|
// Re-add essential WordPress head actions
|
|
add_action('wp_head', 'wp_enqueue_scripts', 1);
|
|
add_action('wp_head', 'wp_print_styles', 8);
|
|
add_action('wp_head', 'wp_print_head_scripts', 9);
|
|
add_action('wp_head', 'wp_resource_hints', 2);
|
|
add_action('wp_head', 'wp_site_icon', 99);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove theme footer when SPA is active
|
|
*/
|
|
public static function remove_theme_footer()
|
|
{
|
|
if (self::should_remove_theme_elements()) {
|
|
remove_all_actions('wp_footer');
|
|
// Re-add essential WordPress footer actions
|
|
add_action('wp_footer', 'wp_print_footer_scripts', 20);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if current page is the designated SPA page
|
|
*/
|
|
private static function is_spa_page()
|
|
{
|
|
global $post;
|
|
|
|
// Get SPA settings from appearance
|
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
|
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
|
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
|
|
|
// Only check if spa_mode is 'full' and SPA page is configured
|
|
if ($spa_mode !== 'full' || !$spa_page_id) {
|
|
return false;
|
|
}
|
|
|
|
// Check if current page is the SPA page
|
|
if ($post && $post->ID == $spa_page_id) {
|
|
return true;
|
|
}
|
|
|
|
// Check if SPA page is set as WordPress frontpage and we're on frontpage
|
|
$frontpage_id = (int) get_option('page_on_front');
|
|
if ($frontpage_id && $frontpage_id === (int) $spa_page_id && is_front_page()) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if we should remove theme header/footer
|
|
*/
|
|
private static function should_remove_theme_elements()
|
|
{
|
|
// Remove for designated SPA pages
|
|
if (self::is_spa_page()) {
|
|
return true;
|
|
}
|
|
|
|
// Check spa_mode from appearance settings
|
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
|
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
|
|
|
// Check if we're on a WooCommerce page in full mode
|
|
if ($spa_mode === 'full') {
|
|
if (is_shop() || is_product() || is_cart() || is_checkout() || is_account_page() || is_woocommerce()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// When SPA is disabled, don't remove theme elements
|
|
if ($spa_mode === 'disabled') {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Override WooCommerce templates
|
|
*/
|
|
public static function override_template($template, $template_name, $template_path)
|
|
{
|
|
// Only override if SPA is enabled
|
|
if (!self::should_use_spa()) {
|
|
return $template;
|
|
}
|
|
|
|
// Templates to override
|
|
$override_templates = [
|
|
'archive-product.php',
|
|
'single-product.php',
|
|
'cart/cart.php',
|
|
'checkout/form-checkout.php',
|
|
];
|
|
|
|
// Check if this template should be overridden
|
|
foreach ($override_templates as $override) {
|
|
if (strpos($template_name, $override) !== false) {
|
|
// Return empty template (SPA will handle rendering)
|
|
return plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-wrapper.php';
|
|
}
|
|
}
|
|
|
|
return $template;
|
|
}
|
|
|
|
/**
|
|
* Detect if current request is from a bot/crawler
|
|
* Used to serve SSR content for SEO instead of SPA redirect
|
|
*
|
|
* @return bool True if request is from a known bot
|
|
*/
|
|
public static function is_bot()
|
|
{
|
|
// Get User-Agent
|
|
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|
if (empty($user_agent)) {
|
|
return false;
|
|
}
|
|
|
|
// Convert to lowercase for case-insensitive matching
|
|
$user_agent = strtolower($user_agent);
|
|
|
|
// Known bot patterns
|
|
$bot_patterns = [
|
|
// Search engine crawlers
|
|
'googlebot',
|
|
'bingbot',
|
|
'slurp', // Yahoo
|
|
'duckduckbot',
|
|
'baiduspider',
|
|
'yandexbot',
|
|
'sogou',
|
|
'exabot',
|
|
|
|
// Generic patterns
|
|
'crawler',
|
|
'spider',
|
|
'robot',
|
|
'scraper',
|
|
|
|
// Social media bots (for link previews)
|
|
'facebookexternalhit',
|
|
'twitterbot',
|
|
'linkedinbot',
|
|
'whatsapp',
|
|
'slackbot',
|
|
'telegrambot',
|
|
'discordbot',
|
|
|
|
// Other known bots
|
|
'applebot',
|
|
'semrushbot',
|
|
'ahrefsbot',
|
|
'mj12bot',
|
|
'dotbot',
|
|
'petalbot',
|
|
'bytespider',
|
|
|
|
// Prerender services
|
|
'prerender',
|
|
'headlesschrome',
|
|
];
|
|
|
|
// Check if User-Agent contains any bot pattern
|
|
foreach ($bot_patterns as $pattern) {
|
|
if (strpos($user_agent, $pattern) !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Serve SSR content for bots
|
|
* Renders page structure as static HTML for search engine indexing
|
|
*
|
|
* @param int $page_id Page ID to render
|
|
* @param string $type 'page' or 'template'
|
|
* @param \WP_Post|null $post_obj Post object for template rendering (CPT items)
|
|
*/
|
|
public static function serve_ssr_content($page_id, $type = 'page', $post_obj = null)
|
|
{
|
|
// Generate cache key
|
|
$cache_id = $post_obj ? $post_obj->ID : $page_id;
|
|
$cache_key = "wn_ssr_{$type}_{$cache_id}";
|
|
|
|
// Check cache TTL (default 1 hour, filterable)
|
|
$cache_ttl = apply_filters('woonoow_ssr_cache_ttl', HOUR_IN_SECONDS);
|
|
|
|
// Try to get cached content
|
|
$cached = get_transient($cache_key);
|
|
if ($cached !== false) {
|
|
echo $cached;
|
|
exit;
|
|
}
|
|
|
|
// Get page structure
|
|
if ($type === 'page') {
|
|
$structure = get_post_meta($page_id, '_wn_page_structure', true);
|
|
} else {
|
|
// CPT template - type is the post_type like 'post', 'portfolio', etc.
|
|
$structure = get_option("wn_template_{$type}", null);
|
|
}
|
|
|
|
if (empty($structure) || empty($structure['sections'])) {
|
|
return false; // No structure, let normal WP handle it
|
|
}
|
|
|
|
// Render using PageSSR
|
|
$post_data = null;
|
|
if ($post_obj && $type !== 'page') {
|
|
$placeholder_renderer = new PlaceholderRenderer();
|
|
$post_data = $placeholder_renderer->build_post_data($post_obj);
|
|
}
|
|
|
|
$ssr = new PageSSR();
|
|
$html = $ssr->render($structure['sections'], $post_data);
|
|
|
|
if (empty($html)) {
|
|
return false;
|
|
}
|
|
|
|
// Get page title
|
|
$title = $type === 'page' ? get_the_title($page_id) : '';
|
|
if ($post_obj) {
|
|
$title = get_the_title($post_obj);
|
|
}
|
|
|
|
// SEO data
|
|
$seo_title = $title . ' - ' . get_bloginfo('name');
|
|
$seo_description = '';
|
|
|
|
// Try to get Yoast/Rank Math SEO data
|
|
if ($type === 'page') {
|
|
$seo_title = get_post_meta($page_id, '_yoast_wpseo_title', true) ?:
|
|
get_post_meta($page_id, 'rank_math_title', true) ?: $seo_title;
|
|
$seo_description = get_post_meta($page_id, '_yoast_wpseo_metadesc', true) ?:
|
|
get_post_meta($page_id, 'rank_math_description', true) ?: '';
|
|
} elseif ($post_obj) {
|
|
$seo_title = get_post_meta($post_obj->ID, '_yoast_wpseo_title', true) ?:
|
|
get_post_meta($post_obj->ID, 'rank_math_title', true) ?: $seo_title;
|
|
$seo_description = get_post_meta($post_obj->ID, '_yoast_wpseo_metadesc', true) ?:
|
|
get_post_meta($post_obj->ID, 'rank_math_description', true) ?:
|
|
wp_trim_words(wp_strip_all_tags($post_obj->post_content), 30);
|
|
}
|
|
|
|
// Output SSR HTML - start output buffering for caching
|
|
ob_start();
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html <?php language_attributes(); ?>>
|
|
|
|
<head>
|
|
<meta charset="<?php bloginfo('charset'); ?>">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title><?php echo esc_html($seo_title); ?></title>
|
|
<?php if ($seo_description): ?>
|
|
<meta name="description" content="<?php echo esc_attr($seo_description); ?>">
|
|
<?php endif; ?>
|
|
<link rel="canonical" href="<?php echo esc_url(get_permalink($post_obj ?: $page_id)); ?>">
|
|
<meta property="og:title" content="<?php echo esc_attr($seo_title); ?>">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="<?php echo esc_url(get_permalink($post_obj ?: $page_id)); ?>">
|
|
<?php if ($seo_description): ?>
|
|
<meta property="og:description" content="<?php echo esc_attr($seo_description); ?>">
|
|
<?php endif; ?>
|
|
<style>
|
|
/* Minimal SSR styles for bots */
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.wn-ssr {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.wn-section {
|
|
padding: 40px 0;
|
|
}
|
|
|
|
.wn-section h1,
|
|
.wn-section h2 {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.wn-section p {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.wn-section img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.wn-hero {
|
|
background: #f5f5f5;
|
|
padding: 60px 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.wn-cta-banner {
|
|
background: #4f46e5;
|
|
color: white;
|
|
padding: 40px 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.wn-cta-banner a {
|
|
color: white;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.wn-feature-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 24px;
|
|
}
|
|
|
|
.wn-feature-item {
|
|
padding: 20px;
|
|
border: 1px solid #e5e5e5;
|
|
border-radius: 8px;
|
|
}
|
|
</style>
|
|
<?php wp_head(); ?>
|
|
</head>
|
|
|
|
<body <?php body_class('wn-ssr-page'); ?>>
|
|
<div class="wn-ssr">
|
|
<?php echo $html; ?>
|
|
</div>
|
|
<?php wp_footer(); ?>
|
|
</body>
|
|
|
|
</html>
|
|
<?php
|
|
// Get buffered output
|
|
$output = ob_get_clean();
|
|
|
|
// Cache the output for bots (uses cache TTL from filter)
|
|
set_transient($cache_key, $output, $cache_ttl);
|
|
|
|
// Output and exit
|
|
echo $output;
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Handle SSR for structural pages and CPT items when bot detected
|
|
* Should be called from template_redirect hook
|
|
*/
|
|
public static function maybe_serve_ssr_for_bots()
|
|
{
|
|
// Only serve SSR for bots
|
|
if (!self::is_bot()) {
|
|
return;
|
|
}
|
|
|
|
// Check if this is a page with WooNooW structure
|
|
if (is_singular('page')) {
|
|
$page_id = get_queried_object_id();
|
|
$structure = get_post_meta($page_id, '_wn_page_structure', true);
|
|
|
|
if (!empty($structure) && !empty($structure['sections'])) {
|
|
self::serve_ssr_content($page_id, 'page');
|
|
}
|
|
}
|
|
|
|
// Check for CPT items with templates
|
|
$post_type = get_post_type();
|
|
if ($post_type && is_singular() && $post_type !== 'page') {
|
|
$template = get_option("wn_template_{$post_type}", null);
|
|
|
|
if (!empty($template) && !empty($template['sections'])) {
|
|
$post_obj = get_queried_object();
|
|
self::serve_ssr_content($post_obj->ID, $post_type, $post_obj);
|
|
}
|
|
}
|
|
}
|
|
}
|