finalizing subscription moduile, ready to test

This commit is contained in:
Dwindi Ramadhana
2026-01-29 11:54:42 +07:00
parent 6d2136d3b5
commit d80f34c8b9
34 changed files with 5619 additions and 468 deletions

View File

@@ -1,4 +1,5 @@
<?php
namespace WooNooW\Frontend;
use WooNooW\Frontend\PageSSR;
@@ -18,32 +19,32 @@ class TemplateOverride
{
// Register rewrite rules for BrowserRouter SEO (must be on 'init')
add_action('init', [__CLASS__, 'register_spa_rewrite_rules']);
// Flush rewrite rules when relevant settings change
add_action('update_option_woonoow_appearance_settings', function($old_value, $new_value) {
add_action('update_option_woonoow_appearance_settings', function ($old_value, $new_value) {
$old_general = $old_value['general'] ?? [];
$new_general = $new_value['general'] ?? [];
// Only flush if spa_mode, spa_page, or use_browser_router changed
$needs_flush =
$needs_flush =
($old_general['spa_mode'] ?? '') !== ($new_general['spa_mode'] ?? '') ||
($old_general['spa_page'] ?? '') !== ($new_general['spa_page'] ?? '') ||
($old_general['use_browser_router'] ?? true) !== ($new_general['use_browser_router'] ?? true);
if ($needs_flush) {
flush_rewrite_rules();
}
}, 10, 2);
// Redirect WooCommerce pages to SPA routes early (before template loads)
add_action('template_redirect', [__CLASS__, 'redirect_wc_pages_to_spa'], 5);
// Serve SPA directly for frontpage routes (priority 1 = very early, before WC)
add_action('template_redirect', [__CLASS__, 'serve_spa_for_frontpage_routes'], 1);
// Serve SSR for bots on pages/CPT with WooNooW structure (priority 2 = after frontpage check)
add_action('template_redirect', [__CLASS__, 'maybe_serve_ssr_for_bots'], 2);
// Hook to wp_loaded with priority 10 (BEFORE WooCommerce's priority 20)
// This ensures we process add-to-cart before WooCommerce does
add_action('wp_loaded', [__CLASS__, 'intercept_add_to_cart'], 10);
@@ -74,7 +75,7 @@ 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 SPA routes and serves the SPA page
@@ -83,25 +84,25 @@ class TemplateOverride
{
$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;
// Check if SPA page is set as WordPress frontpage
$frontpage_id = (int) get_option('page_on_front');
$is_spa_frontpage = $frontpage_id && $frontpage_id === (int) $spa_page_id;
if ($is_spa_frontpage) {
// When SPA is frontpage, add root-level routes
// /shop, /shop/* → SPA page
@@ -115,28 +116,33 @@ class TemplateOverride
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=shop/$matches[1]',
'top'
);
// /product/* → SPA page
add_rewrite_rule(
'^product/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=product/$matches[1]',
'top'
);
// /cart → SPA page
add_rewrite_rule(
'^cart/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=cart',
'top'
);
// /checkout → SPA page
// /checkout, /checkout/* → SPA page
add_rewrite_rule(
'^checkout/?$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=checkout',
'top'
);
add_rewrite_rule(
'^checkout/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=checkout/$matches[1]',
'top'
);
// /my-account, /my-account/* → SPA page
add_rewrite_rule(
'^my-account/?$',
@@ -148,7 +154,7 @@ class TemplateOverride
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=my-account/$matches[1]',
'top'
);
// /login, /register, /reset-password → SPA page
add_rewrite_rule(
'^login/?$',
@@ -165,6 +171,24 @@ class TemplateOverride
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=reset-password',
'top'
);
// /order-pay/* → SPA page
add_rewrite_rule(
'^order-pay/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=order-pay/$matches[1]',
'top'
);
// /order-pay/* → SPA page
add_rewrite_rule(
'^order-pay/(.*)$',
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=order-pay/$matches[1]',
'top'
);
// /order-pay/* → SPA page (moved to checkout/pay/ in new structure)
// Removed direct order-pay rule to favor checkout subpath
} else {
// Rewrite /slug to serve the SPA page (base URL)
add_rewrite_rule(
@@ -172,7 +196,7 @@ class TemplateOverride
'index.php?page_id=' . $spa_page_id,
'top'
);
// Rewrite /slug/anything to serve the SPA page with path
// React Router handles the path after that
add_rewrite_rule(
@@ -181,9 +205,9 @@ class TemplateOverride
'top'
);
}
// Register query var for the SPA path
add_filter('query_vars', function($vars) {
add_filter('query_vars', function ($vars) {
$vars[] = 'woonoow_spa_path';
return $vars;
});
@@ -249,32 +273,32 @@ class TemplateOverride
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
// Only redirect when SPA mode is 'full'
if ($spa_mode !== 'full') {
return;
}
if (!$spa_page_id) {
return; // No SPA page configured
}
// Skip if SPA is set as frontpage (serve_spa_for_frontpage_routes handles it)
$frontpage_id = (int) get_option('page_on_front');
if ($frontpage_id && $frontpage_id === (int) $spa_page_id) {
return;
}
// Already on SPA page, don't redirect
global $post;
if ($post && $post->ID == $spa_page_id) {
return;
}
$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) {
$build_route = function ($path) use ($spa_url, $use_browser_router) {
if ($use_browser_router) {
// Path format: /store/cart
return $spa_url . ltrim($path, '/');
@@ -282,13 +306,13 @@ class TemplateOverride
// Hash format: /store/#/cart
return rtrim($spa_url, '/') . '#/' . ltrim($path, '/');
};
// Check which WC page we're on and redirect
if (is_shop()) {
wp_redirect($build_route('shop'), 302);
exit;
}
if (is_product()) {
// Use get_queried_object() which returns the WP_Post, then get slug
$product_post = get_queried_object();
@@ -298,29 +322,37 @@ class TemplateOverride
exit;
}
}
if (is_cart()) {
wp_redirect($build_route('cart'), 302);
exit;
}
if (is_checkout() && !is_order_received_page()) {
// Check for order-pay endpoint
if (is_wc_endpoint_url('order-pay')) {
global $wp;
$order_id = $wp->query_vars['order-pay'];
wp_redirect($build_route('order-pay/' . $order_id), 302);
exit;
}
wp_redirect($build_route('checkout'), 302);
exit;
}
if (is_account_page()) {
wp_redirect($build_route('my-account'), 302);
exit;
}
// Redirect structural pages with WooNooW sections to SPA
if (is_singular('page') && $post) {
// Skip the SPA page itself and frontpage
if ($post->ID == $spa_page_id || $post->ID == $frontpage_id) {
return;
}
// Check if page has WooNooW structure
$structure = get_post_meta($post->ID, '_wn_page_structure', true);
if (!empty($structure) && !empty($structure['sections'])) {
@@ -348,22 +380,22 @@ class TemplateOverride
$appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
// Only run in full SPA mode
if ($spa_mode !== 'full' || !$spa_page_id) {
return;
}
// Check if SPA page is set as WordPress frontpage
$frontpage_id = (int) get_option('page_on_front');
if (!$frontpage_id || $frontpage_id !== (int) $spa_page_id) {
return; // SPA is not frontpage, let normal routing handle it
}
// Get the current request path relative to site root
$request_uri = $_SERVER['REQUEST_URI'] ?? '/';
$home_path = parse_url(home_url(), PHP_URL_PATH);
// Normalize request URI for subdirectory installs
if ($home_path && $home_path !== '/') {
$home_path = rtrim($home_path, '/');
@@ -375,7 +407,7 @@ class TemplateOverride
$path = parse_url($request_uri, PHP_URL_PATH);
$path = '/' . trim($path, '/');
// Define SPA routes that should be intercepted when SPA is frontpage
$spa_routes = [
'/', // Frontpage itself
@@ -385,37 +417,39 @@ class TemplateOverride
'/my-account', // Account page
'/login', // Login page
'/register', // Register page
'/register', // Register page
'/reset-password', // Password reset
'/order-pay', // Order pay page
];
// Check for exact matches or path prefixes
$should_serve_spa = false;
// Check exact matches
if (in_array($path, $spa_routes)) {
$should_serve_spa = true;
}
// Check path prefixes (for sub-routes)
$prefix_routes = ['/shop/', '/my-account/', '/product/'];
$prefix_routes = ['/shop/', '/my-account/', '/product/', '/checkout/'];
foreach ($prefix_routes as $prefix) {
if (strpos($path, $prefix) === 0) {
$should_serve_spa = true;
break;
}
}
// Check for structural pages with WooNooW sections
if (!$should_serve_spa && !empty($path) && $path !== '/') {
// Try to find a page by slug matching the path
$slug = trim($path, '/');
// Handle nested slugs (get the last part as the page slug)
if (strpos($slug, '/') !== false) {
$slug_parts = explode('/', $slug);
$slug = end($slug_parts);
}
$page = get_page_by_path($slug);
if ($page) {
// Check if this page has WooNooW structure
@@ -425,26 +459,26 @@ class TemplateOverride
}
}
}
// Not a SPA route
if (!$should_serve_spa) {
return;
}
// Prevent caching for dynamic SPA content
nocache_headers();
// Load the SPA template directly and exit
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
if (file_exists($spa_template)) {
// Set up minimal WordPress environment for the template
status_header(200);
// Define constant to tell Assets to load unconditionally
if (!defined('WOONOOW_SERVE_SPA')) {
define('WOONOOW_SERVE_SPA', true);
}
// Include the SPA template
include $spa_template;
exit;
@@ -487,12 +521,12 @@ class TemplateOverride
// Check spa_mode from appearance settings FIRST
$appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
// If SPA is disabled, return original template immediately
if ($spa_mode === 'disabled') {
return $template;
}
// Check if current page is a designated SPA page
if (self::is_spa_page()) {
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
@@ -511,7 +545,7 @@ class TemplateOverride
}
}
}
// For spa_mode = 'checkout_only'
if ($spa_mode === 'checkout_only') {
if (is_checkout() || is_order_received_page() || is_account_page() || is_cart()) {
@@ -634,7 +668,7 @@ class TemplateOverride
private static function is_spa_page()
{
global $post;
// Get SPA settings from appearance
$appearance_settings = get_option('woonoow_appearance_settings', []);
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
@@ -716,7 +750,7 @@ class TemplateOverride
return $template;
}
/**
* Detect if current request is from a bot/crawler
* Used to serve SSR content for SEO instead of SPA redirect
@@ -730,10 +764,10 @@ class TemplateOverride
if (empty($user_agent)) {
return false;
}
// Convert to lowercase for case-insensitive matching
$user_agent = strtolower($user_agent);
// Known bot patterns
$bot_patterns = [
// Search engine crawlers
@@ -745,13 +779,13 @@ class TemplateOverride
'yandexbot',
'sogou',
'exabot',
// Generic patterns
'crawler',
'spider',
'robot',
'scraper',
// Social media bots (for link previews)
'facebookexternalhit',
'twitterbot',
@@ -760,7 +794,7 @@ class TemplateOverride
'slackbot',
'telegrambot',
'discordbot',
// Other known bots
'applebot',
'semrushbot',
@@ -769,22 +803,22 @@ class TemplateOverride
'dotbot',
'petalbot',
'bytespider',
// Prerender services
'prerender',
'headlesschrome',
];
// Check if User-Agent contains any bot pattern
foreach ($bot_patterns as $pattern) {
if (strpos($user_agent, $pattern) !== false) {
return true;
}
}
return false;
}
/**
* Serve SSR content for bots
* Renders page structure as static HTML for search engine indexing
@@ -798,17 +832,17 @@ class TemplateOverride
// Generate cache key
$cache_id = $post_obj ? $post_obj->ID : $page_id;
$cache_key = "wn_ssr_{$type}_{$cache_id}";
// Check cache TTL (default 1 hour, filterable)
$cache_ttl = apply_filters('woonoow_ssr_cache_ttl', HOUR_IN_SECONDS);
// Try to get cached content
$cached = get_transient($cache_key);
if ($cached !== false) {
echo $cached;
exit;
}
// Get page structure
if ($type === 'page') {
$structure = get_post_meta($page_id, '_wn_page_structure', true);
@@ -816,103 +850,155 @@ class TemplateOverride
// CPT template - type is the post_type like 'post', 'portfolio', etc.
$structure = get_option("wn_template_{$type}", null);
}
if (empty($structure) || empty($structure['sections'])) {
return false; // No structure, let normal WP handle it
}
// Render using PageSSR
$post_data = null;
if ($post_obj && $type !== 'page') {
$placeholder_renderer = new PlaceholderRenderer();
$post_data = $placeholder_renderer->build_post_data($post_obj);
}
$ssr = new PageSSR();
$html = $ssr->render($structure['sections'], $post_data);
if (empty($html)) {
return false;
}
// Get page title
$title = $type === 'page' ? get_the_title($page_id) : '';
if ($post_obj) {
$title = get_the_title($post_obj);
}
// SEO data
$seo_title = $title . ' - ' . get_bloginfo('name');
$seo_description = '';
// Try to get Yoast/Rank Math SEO data
if ($type === 'page') {
$seo_title = get_post_meta($page_id, '_yoast_wpseo_title', true) ?:
get_post_meta($page_id, 'rank_math_title', true) ?: $seo_title;
$seo_description = get_post_meta($page_id, '_yoast_wpseo_metadesc', true) ?:
get_post_meta($page_id, 'rank_math_description', true) ?: '';
$seo_title = get_post_meta($page_id, '_yoast_wpseo_title', true) ?:
get_post_meta($page_id, 'rank_math_title', true) ?: $seo_title;
$seo_description = get_post_meta($page_id, '_yoast_wpseo_metadesc', true) ?:
get_post_meta($page_id, 'rank_math_description', true) ?: '';
} elseif ($post_obj) {
$seo_title = get_post_meta($post_obj->ID, '_yoast_wpseo_title', true) ?:
get_post_meta($post_obj->ID, 'rank_math_title', true) ?: $seo_title;
$seo_description = get_post_meta($post_obj->ID, '_yoast_wpseo_metadesc', true) ?:
get_post_meta($post_obj->ID, 'rank_math_description', true) ?:
wp_trim_words(wp_strip_all_tags($post_obj->post_content), 30);
$seo_title = get_post_meta($post_obj->ID, '_yoast_wpseo_title', true) ?:
get_post_meta($post_obj->ID, 'rank_math_title', true) ?: $seo_title;
$seo_description = get_post_meta($post_obj->ID, '_yoast_wpseo_metadesc', true) ?:
get_post_meta($post_obj->ID, 'rank_math_description', true) ?:
wp_trim_words(wp_strip_all_tags($post_obj->post_content), 30);
}
// Output SSR HTML - start output buffering for caching
ob_start();
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo esc_html($seo_title); ?></title>
<?php if ($seo_description): ?>
<meta name="description" content="<?php echo esc_attr($seo_description); ?>">
<?php endif; ?>
<link rel="canonical" href="<?php echo esc_url(get_permalink($post_obj ?: $page_id)); ?>">
<meta property="og:title" content="<?php echo esc_attr($seo_title); ?>">
<meta property="og:type" content="website">
<meta property="og:url" content="<?php echo esc_url(get_permalink($post_obj ?: $page_id)); ?>">
<?php if ($seo_description): ?>
<meta property="og:description" content="<?php echo esc_attr($seo_description); ?>">
<?php endif; ?>
<style>
/* Minimal SSR styles for bots */
body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; margin: 0; padding: 0; }
.wn-ssr { max-width: 1200px; margin: 0 auto; padding: 20px; }
.wn-section { padding: 40px 0; }
.wn-section h1, .wn-section h2 { margin-bottom: 16px; }
.wn-section p { margin-bottom: 12px; }
.wn-section img { max-width: 100%; height: auto; }
.wn-hero { background: #f5f5f5; padding: 60px 20px; text-align: center; }
.wn-cta-banner { background: #4f46e5; color: white; padding: 40px 20px; text-align: center; }
.wn-cta-banner a { color: white; text-decoration: underline; }
.wn-feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 24px; }
.wn-feature-item { padding: 20px; border: 1px solid #e5e5e5; border-radius: 8px; }
</style>
<?php wp_head(); ?>
</head>
<body <?php body_class('wn-ssr-page'); ?>>
<div class="wn-ssr">
<?php echo $html; ?>
</div>
<?php wp_footer(); ?>
</body>
</html>
<?php
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo esc_html($seo_title); ?></title>
<?php if ($seo_description): ?>
<meta name="description" content="<?php echo esc_attr($seo_description); ?>">
<?php endif; ?>
<link rel="canonical" href="<?php echo esc_url(get_permalink($post_obj ?: $page_id)); ?>">
<meta property="og:title" content="<?php echo esc_attr($seo_title); ?>">
<meta property="og:type" content="website">
<meta property="og:url" content="<?php echo esc_url(get_permalink($post_obj ?: $page_id)); ?>">
<?php if ($seo_description): ?>
<meta property="og:description" content="<?php echo esc_attr($seo_description); ?>">
<?php endif; ?>
<style>
/* Minimal SSR styles for bots */
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
}
.wn-ssr {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.wn-section {
padding: 40px 0;
}
.wn-section h1,
.wn-section h2 {
margin-bottom: 16px;
}
.wn-section p {
margin-bottom: 12px;
}
.wn-section img {
max-width: 100%;
height: auto;
}
.wn-hero {
background: #f5f5f5;
padding: 60px 20px;
text-align: center;
}
.wn-cta-banner {
background: #4f46e5;
color: white;
padding: 40px 20px;
text-align: center;
}
.wn-cta-banner a {
color: white;
text-decoration: underline;
}
.wn-feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
.wn-feature-item {
padding: 20px;
border: 1px solid #e5e5e5;
border-radius: 8px;
}
</style>
<?php wp_head(); ?>
</head>
<body <?php body_class('wn-ssr-page'); ?>>
<div class="wn-ssr">
<?php echo $html; ?>
</div>
<?php wp_footer(); ?>
</body>
</html>
<?php
// Get buffered output
$output = ob_get_clean();
// Cache the output for bots (uses cache TTL from filter)
set_transient($cache_key, $output, $cache_ttl);
// Output and exit
echo $output;
exit;
}
/**
* Handle SSR for structural pages and CPT items when bot detected
* Should be called from template_redirect hook
@@ -923,22 +1009,22 @@ class TemplateOverride
if (!self::is_bot()) {
return;
}
// Check if this is a page with WooNooW structure
if (is_singular('page')) {
$page_id = get_queried_object_id();
$structure = get_post_meta($page_id, '_wn_page_structure', true);
if (!empty($structure) && !empty($structure['sections'])) {
self::serve_ssr_content($page_id, 'page');
}
}
// Check for CPT items with templates
$post_type = get_post_type();
if ($post_type && is_singular() && $post_type !== 'page') {
$template = get_option("wn_template_{$post_type}", null);
if (!empty($template) && !empty($template['sections'])) {
$post_obj = get_queried_object();
self::serve_ssr_content($post_obj->ID, $post_type, $post_obj);
@@ -946,4 +1032,3 @@ class TemplateOverride
}
}
}