From 45fcbf9d2982ac9112fbb41a440336f9b7fcf100 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Sat, 3 Jan 2026 20:01:32 +0700 Subject: [PATCH] feat: migrate from HashRouter to BrowserRouter for SEO Phase 1: WordPress Rewrite Rules - Add rewrite rule for /store/* to serve SPA page - Add use_browser_router setting toggle (default: true) - Flush rewrite rules on settings change Phase 2: React Router Migration - Add BrowserRouter with basename from WordPress config - Pass basePath and useBrowserRouter to frontend - Conditional router based on setting Phase 3: Hash Route Migration - Update EmailManager.php reset password URL - Update EmailRenderer.php login URL - Update TemplateOverride.php WC redirects - All routes now use path format by default This enables proper SEO indexing as search engines can now crawl individual product/page URLs. --- customer-spa/src/App.tsx | 26 ++++++- includes/Core/Notifications/EmailManager.php | 13 +++- includes/Core/Notifications/EmailRenderer.php | 12 +++- includes/Frontend/Assets.php | 11 +++ includes/Frontend/TemplateOverride.php | 69 +++++++++++++++++-- 5 files changed, 118 insertions(+), 13 deletions(-) diff --git a/customer-spa/src/App.tsx b/customer-spa/src/App.tsx index 591da3b..202d879 100644 --- a/customer-spa/src/App.tsx +++ b/customer-spa/src/App.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { HashRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { HashRouter, BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'sonner'; @@ -102,6 +102,26 @@ function AppRoutes() { ); } +// Get router config from WordPress +const getRouterConfig = () => { + const config = (window as any).woonoowCustomer; + return { + useBrowserRouter: config?.useBrowserRouter ?? true, + basePath: config?.basePath || '/store', + }; +}; + +// Router wrapper that conditionally uses BrowserRouter or HashRouter +function RouterProvider({ children }: { children: React.ReactNode }) { + const { useBrowserRouter, basePath } = getRouterConfig(); + + if (useBrowserRouter) { + return {children}; + } + + return {children}; +} + function App() { const themeConfig = getThemeConfig(); const appearanceSettings = getAppearanceSettings(); @@ -110,9 +130,9 @@ function App() { return ( - + - + {/* Toast notifications - position from settings */} diff --git a/includes/Core/Notifications/EmailManager.php b/includes/Core/Notifications/EmailManager.php index dac6657..aab9bfb 100644 --- a/includes/Core/Notifications/EmailManager.php +++ b/includes/Core/Notifications/EmailManager.php @@ -331,6 +331,7 @@ class EmailManager { // The SPA page (e.g., /store/) loads customer-spa which has /reset-password route $appearance_settings = get_option('woonoow_appearance_settings', []); $spa_page_id = $appearance_settings['general']['spa_page'] ?? 0; + $use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true; if ($spa_page_id > 0) { $spa_url = get_permalink($spa_page_id); @@ -339,9 +340,15 @@ class EmailManager { $spa_url = home_url('/'); } - // Build SPA reset password URL with hash router format - // Format: /store/#/reset-password?key=KEY&login=LOGIN - $reset_link = rtrim($spa_url, '/') . '#/reset-password?key=' . $key . '&login=' . rawurlencode($user_login); + // Build SPA reset password URL + // Use path format for BrowserRouter (SEO), hash format for HashRouter (legacy) + if ($use_browser_router) { + // Path format: /store/reset-password?key=KEY&login=LOGIN + $reset_link = trailingslashit($spa_url) . 'reset-password?key=' . $key . '&login=' . rawurlencode($user_login); + } else { + // Hash format: /store/#/reset-password?key=KEY&login=LOGIN + $reset_link = rtrim($spa_url, '/') . '#/reset-password?key=' . $key . '&login=' . rawurlencode($user_login); + } // Create a pseudo WC_Customer for template rendering $customer = null; diff --git a/includes/Core/Notifications/EmailRenderer.php b/includes/Core/Notifications/EmailRenderer.php index 157a832..27b6bbd 100644 --- a/includes/Core/Notifications/EmailRenderer.php +++ b/includes/Core/Notifications/EmailRenderer.php @@ -259,7 +259,17 @@ class EmailRenderer { // Generate login URL (pointing to SPA login instead of wp-login) $appearance_settings = get_option('woonoow_appearance_settings', []); $spa_page_id = $appearance_settings['general']['spa_page'] ?? 0; - $login_url = $spa_page_id ? get_permalink($spa_page_id) . '#/login' : wp_login_url(); + $use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true; + + if ($spa_page_id) { + $spa_url = get_permalink($spa_page_id); + // Use path format for BrowserRouter, hash format for HashRouter + $login_url = $use_browser_router + ? trailingslashit($spa_url) . 'login' + : $spa_url . '#/login'; + } else { + $login_url = wp_login_url(); + } $variables = array_merge($variables, [ 'customer_id' => $data->get_id(), diff --git a/includes/Frontend/Assets.php b/includes/Frontend/Assets.php index dc9fcb2..715c12b 100644 --- a/includes/Frontend/Assets.php +++ b/includes/Frontend/Assets.php @@ -194,18 +194,29 @@ class Assets { ]; } + // Determine SPA base path for BrowserRouter + $spa_page_id = $appearance_settings['general']['spa_page'] ?? 0; + $spa_page = $spa_page_id ? get_post($spa_page_id) : null; + $base_path = $spa_page ? '/' . $spa_page->post_name : '/store'; + + // Check if BrowserRouter is enabled (default: true for SEO) + $use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true; + $config = [ 'apiUrl' => rest_url('woonoow/v1'), 'apiRoot' => rest_url('woonoow/v1'), 'nonce' => wp_create_nonce('wp_rest'), 'siteUrl' => get_site_url(), 'siteTitle' => get_bloginfo('name'), + 'siteName' => get_bloginfo('name'), 'storeName' => get_bloginfo('name'), 'storeLogo' => $logo_url, 'user' => $user_data, 'theme' => $theme_settings, 'currency' => $currency_settings, 'appearanceSettings' => $appearance_settings, + 'basePath' => $base_path, + 'useBrowserRouter' => $use_browser_router, ]; ?> diff --git a/includes/Frontend/TemplateOverride.php b/includes/Frontend/TemplateOverride.php index ffce643..42de655 100644 --- a/includes/Frontend/TemplateOverride.php +++ b/includes/Frontend/TemplateOverride.php @@ -13,6 +13,14 @@ class TemplateOverride */ 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 appearance settings are updated + add_action('update_option_woonoow_appearance_settings', function() { + flush_rewrite_rules(); + }); + // Redirect WooCommerce pages to SPA routes early (before template loads) add_action('template_redirect', [__CLASS__, 'redirect_wc_pages_to_spa'], 5); @@ -46,6 +54,44 @@ class TemplateOverride add_action('get_header', [__CLASS__, 'remove_theme_header']); add_action('get_footer', [__CLASS__, 'remove_theme_footer']); } + + /** + * Register rewrite rules for BrowserRouter SEO + * Catches all /store/* 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; + + // Rewrite /store/anything to serve the SPA page + // 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) @@ -98,13 +144,14 @@ class TemplateOverride /** * Redirect WooCommerce pages to SPA routes - * Maps: /shop → /store/#/, /cart → /store/#/cart, etc. + * Maps: /shop → /store/, /cart → /store/cart, etc. */ public static function redirect_wc_pages_to_spa() { // Get SPA page URL $appearance_settings = get_option('woonoow_appearance_settings', []); $spa_page_id = $appearance_settings['general']['spa_page'] ?? 0; + $use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true; if (!$spa_page_id) { return; // No SPA page configured @@ -118,9 +165,19 @@ class TemplateOverride $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($spa_url . '#/', 302); + wp_redirect($build_route('shop'), 302); exit; } @@ -128,23 +185,23 @@ class TemplateOverride global $product; if ($product) { $slug = $product->get_slug(); - wp_redirect($spa_url . '#/products/' . $slug, 302); + wp_redirect($build_route('product/' . $slug), 302); exit; } } if (is_cart()) { - wp_redirect($spa_url . '#/cart', 302); + wp_redirect($build_route('cart'), 302); exit; } if (is_checkout() && !is_order_received_page()) { - wp_redirect($spa_url . '#/checkout', 302); + wp_redirect($build_route('checkout'), 302); exit; } if (is_account_page()) { - wp_redirect($spa_url . '#/account', 302); + wp_redirect($build_route('my-account'), 302); exit; } }