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.
This commit is contained in:
Dwindi Ramadhana
2026-01-03 20:01:32 +07:00
parent 0421e5010f
commit 45fcbf9d29
5 changed files with 118 additions and 13 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react'; 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Toaster } from 'sonner'; 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 <BrowserRouter basename={basePath}>{children}</BrowserRouter>;
}
return <HashRouter>{children}</HashRouter>;
}
function App() { function App() {
const themeConfig = getThemeConfig(); const themeConfig = getThemeConfig();
const appearanceSettings = getAppearanceSettings(); const appearanceSettings = getAppearanceSettings();
@@ -110,9 +130,9 @@ function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeProvider config={themeConfig}> <ThemeProvider config={themeConfig}>
<HashRouter> <RouterProvider>
<AppRoutes /> <AppRoutes />
</HashRouter> </RouterProvider>
{/* Toast notifications - position from settings */} {/* Toast notifications - position from settings */}
<Toaster position={toastPosition} richColors /> <Toaster position={toastPosition} richColors />

View File

@@ -331,6 +331,7 @@ class EmailManager {
// The SPA page (e.g., /store/) loads customer-spa which has /reset-password route // The SPA page (e.g., /store/) loads customer-spa which has /reset-password route
$appearance_settings = get_option('woonoow_appearance_settings', []); $appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0; $spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
if ($spa_page_id > 0) { if ($spa_page_id > 0) {
$spa_url = get_permalink($spa_page_id); $spa_url = get_permalink($spa_page_id);
@@ -339,9 +340,15 @@ class EmailManager {
$spa_url = home_url('/'); $spa_url = home_url('/');
} }
// Build SPA reset password URL with hash router format // Build SPA reset password URL
// Format: /store/#/reset-password?key=KEY&login=LOGIN // Use path format for BrowserRouter (SEO), hash format for HashRouter (legacy)
$reset_link = rtrim($spa_url, '/') . '#/reset-password?key=' . $key . '&login=' . rawurlencode($user_login); 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 // Create a pseudo WC_Customer for template rendering
$customer = null; $customer = null;

View File

@@ -259,7 +259,17 @@ class EmailRenderer {
// Generate login URL (pointing to SPA login instead of wp-login) // Generate login URL (pointing to SPA login instead of wp-login)
$appearance_settings = get_option('woonoow_appearance_settings', []); $appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0; $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, [ $variables = array_merge($variables, [
'customer_id' => $data->get_id(), 'customer_id' => $data->get_id(),

View File

@@ -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 = [ $config = [
'apiUrl' => rest_url('woonoow/v1'), 'apiUrl' => rest_url('woonoow/v1'),
'apiRoot' => rest_url('woonoow/v1'), 'apiRoot' => rest_url('woonoow/v1'),
'nonce' => wp_create_nonce('wp_rest'), 'nonce' => wp_create_nonce('wp_rest'),
'siteUrl' => get_site_url(), 'siteUrl' => get_site_url(),
'siteTitle' => get_bloginfo('name'), 'siteTitle' => get_bloginfo('name'),
'siteName' => get_bloginfo('name'),
'storeName' => get_bloginfo('name'), 'storeName' => get_bloginfo('name'),
'storeLogo' => $logo_url, 'storeLogo' => $logo_url,
'user' => $user_data, 'user' => $user_data,
'theme' => $theme_settings, 'theme' => $theme_settings,
'currency' => $currency_settings, 'currency' => $currency_settings,
'appearanceSettings' => $appearance_settings, 'appearanceSettings' => $appearance_settings,
'basePath' => $base_path,
'useBrowserRouter' => $use_browser_router,
]; ];
?> ?>

View File

@@ -13,6 +13,14 @@ class TemplateOverride
*/ */
public static function init() 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) // Redirect WooCommerce pages to SPA routes early (before template loads)
add_action('template_redirect', [__CLASS__, 'redirect_wc_pages_to_spa'], 5); add_action('template_redirect', [__CLASS__, 'redirect_wc_pages_to_spa'], 5);
@@ -47,6 +55,44 @@ class TemplateOverride
add_action('get_footer', [__CLASS__, 'remove_theme_footer']); 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) * Intercept add-to-cart redirect (NOT the add-to-cart itself)
* Let WooCommerce handle the cart operation properly, we just redirect afterward * Let WooCommerce handle the cart operation properly, we just redirect afterward
@@ -98,13 +144,14 @@ class TemplateOverride
/** /**
* Redirect WooCommerce pages to SPA routes * 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() public static function redirect_wc_pages_to_spa()
{ {
// Get SPA page URL // Get SPA page URL
$appearance_settings = get_option('woonoow_appearance_settings', []); $appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0; $spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
if (!$spa_page_id) { if (!$spa_page_id) {
return; // No SPA page configured return; // No SPA page configured
@@ -118,9 +165,19 @@ class TemplateOverride
$spa_url = trailingslashit(get_permalink($spa_page_id)); $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 // Check which WC page we're on and redirect
if (is_shop()) { if (is_shop()) {
wp_redirect($spa_url . '#/', 302); wp_redirect($build_route('shop'), 302);
exit; exit;
} }
@@ -128,23 +185,23 @@ class TemplateOverride
global $product; global $product;
if ($product) { if ($product) {
$slug = $product->get_slug(); $slug = $product->get_slug();
wp_redirect($spa_url . '#/products/' . $slug, 302); wp_redirect($build_route('product/' . $slug), 302);
exit; exit;
} }
} }
if (is_cart()) { if (is_cart()) {
wp_redirect($spa_url . '#/cart', 302); wp_redirect($build_route('cart'), 302);
exit; exit;
} }
if (is_checkout() && !is_order_received_page()) { if (is_checkout() && !is_order_received_page()) {
wp_redirect($spa_url . '#/checkout', 302); wp_redirect($build_route('checkout'), 302);
exit; exit;
} }
if (is_account_page()) { if (is_account_page()) {
wp_redirect($spa_url . '#/account', 302); wp_redirect($build_route('my-account'), 302);
exit; exit;
} }
} }