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

@@ -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,
];
?>

View File

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