feat: Page Editor Phase 3 - SSR integration and navigation

- Implement serve_ssr_content with full PageSSR rendering
  - SEO meta tags (title, description, og:*)
  - Minimal CSS for bot-friendly presentation
  - Yoast/Rank Math SEO data integration
- Add maybe_serve_ssr_for_bots hook (priority 2 on template_redirect)
  - Serves SSR for structural pages with WooNooW structure
  - Serves SSR for CPT items with templates
- Add use statements for PageSSR and PlaceholderRenderer
- Add Pages link to Appearance submenu in NavigationRegistry
- Bump NAV_VERSION to 1.1.0
This commit is contained in:
Dwindi Ramadhana
2026-01-11 22:55:16 +07:00
parent bdded61221
commit f3540a8448
2 changed files with 126 additions and 8 deletions

View File

@@ -13,7 +13,7 @@ if ( ! defined('ABSPATH') ) exit;
*/ */
class NavigationRegistry { class NavigationRegistry {
const NAV_OPTION = 'wnw_nav_tree'; const NAV_OPTION = 'wnw_nav_tree';
const NAV_VERSION = '1.0.9'; // Added Help menu const NAV_VERSION = '1.1.0'; // Added Pages (Page Editor)
/** /**
* Initialize hooks * Initialize hooks
@@ -169,6 +169,7 @@ class NavigationRegistry {
'icon' => 'palette', 'icon' => 'palette',
'children' => [ 'children' => [
['label' => __('General', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/general'], ['label' => __('General', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/general'],
['label' => __('Pages', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/pages'],
['label' => __('Header', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/header'], ['label' => __('Header', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/header'],
['label' => __('Footer', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/footer'], ['label' => __('Footer', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/footer'],
['label' => __('Shop', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/shop'], ['label' => __('Shop', 'woonoow'), 'mode' => 'spa', 'path' => '/appearance/shop'],

View File

@@ -1,6 +1,9 @@
<?php <?php
namespace WooNooW\Frontend; namespace WooNooW\Frontend;
use WooNooW\Frontend\PageSSR;
use WooNooW\Frontend\PlaceholderRenderer;
/** /**
* Template Override * Template Override
* Overrides WooCommerce templates to use WooNooW SPA * Overrides WooCommerce templates to use WooNooW SPA
@@ -38,6 +41,9 @@ class TemplateOverride
// Serve SPA directly for frontpage routes (priority 1 = very early, before WC) // Serve SPA directly for frontpage routes (priority 1 = very early, before WC)
add_action('template_redirect', [__CLASS__, 'serve_spa_for_frontpage_routes'], 1); 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) // Hook to wp_loaded with priority 10 (BEFORE WooCommerce's priority 20)
// This ensures we process add-to-cart before WooCommerce does // This ensures we process add-to-cart before WooCommerce does
add_action('wp_loaded', [__CLASS__, 'intercept_add_to_cart'], 10); add_action('wp_loaded', [__CLASS__, 'intercept_add_to_cart'], 10);
@@ -723,15 +729,15 @@ class TemplateOverride
* *
* @param int $page_id Page ID to render * @param int $page_id Page ID to render
* @param string $type 'page' or 'template' * @param string $type 'page' or 'template'
* @param array|null $post_data Post data for template rendering (CPT items) * @param \WP_Post|null $post_obj Post object for template rendering (CPT items)
*/ */
public static function serve_ssr_content($page_id, $type = 'page', $post_data = null) public static function serve_ssr_content($page_id, $type = 'page', $post_obj = null)
{ {
// Get page structure // Get page structure
if ($type === 'page') { if ($type === 'page') {
$structure = get_post_meta($page_id, '_wn_page_structure', true); $structure = get_post_meta($page_id, '_wn_page_structure', true);
} else { } else {
// CPT template // CPT template - type is the post_type like 'post', 'portfolio', etc.
$structure = get_option("wn_template_{$type}", null); $structure = get_option("wn_template_{$type}", null);
} }
@@ -739,9 +745,120 @@ class TemplateOverride
return false; // No structure, let normal WP handle it return false; // No structure, let normal WP handle it
} }
// Will be implemented in PageSSR class // Render using PageSSR
// For now, return false to let normal WP handle $post_data = null;
// TODO: Implement PageSSR::render($structure, $post_data) if ($post_obj && $type !== 'page') {
return false; $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) ?: '';
} 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);
}
// Output SSR HTML
?>
<!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
exit;
}
/**
* Handle SSR for structural pages and CPT items when bot detected
* Should be called from template_redirect hook
*/
public static function maybe_serve_ssr_for_bots()
{
// Only serve SSR for bots
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);
}
}
} }
} }