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;
}
}