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:
@@ -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 />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
];
|
];
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user