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 → SPA page add_rewrite_rule( '^checkout/?$', 'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=checkout', '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' ); } 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()) { 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 '/reset-password', // Password reset ]; // 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/']; 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 '
'; echo '
'; echo '

' . esc_html__('Loading...', 'woonoow') . '

'; echo '
'; echo '
'; // Hide WooCommerce content echo '
'; } /** * End SPA wrapper */ public static function end_spa_wrapper() { if (!self::should_use_spa()) { return; } // Close hidden wrapper echo '
'; } /** * 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(); ?> > <?php echo esc_html($seo_title); ?> >
ID, $post_type, $post_obj); } } } }