Files
WooNooW/includes/Frontend/PageSSR.php
Dwindi Ramadhana 9331989102 feat: Page Editor Phase 1 - Core Infrastructure
- Add is_bot() detection in TemplateOverride.php (30+ bot patterns)
- Add PageSSR.php for server-side rendering of page sections
- Add PlaceholderRenderer.php for dynamic content resolution
- Add PagesController.php REST API for pages/templates CRUD
- Register PagesController routes in Routes.php

API Endpoints:
- GET /pages - list all pages/templates
- GET /pages/{slug} - get page structure
- POST /pages/{slug} - save page
- GET /templates/{cpt} - get CPT template
- POST /templates/{cpt} - save template
- GET /content/{type}/{slug} - get content with template applied
2026-01-11 22:29:30 +07:00

291 lines
9.8 KiB
PHP

<?php
namespace WooNooW\Frontend;
/**
* Page SSR (Server-Side Rendering)
* Renders page sections as static HTML for search engine crawlers
*/
class PageSSR
{
/**
* Render page structure to HTML
*
* @param array $structure Page structure with sections
* @param array|null $post_data Post data for dynamic placeholders
* @return string Rendered HTML
*/
public static function render($structure, $post_data = null)
{
if (empty($structure) || empty($structure['sections'])) {
return '';
}
$html = '';
foreach ($structure['sections'] as $section) {
$html .= self::render_section($section, $post_data);
}
return $html;
}
/**
* Render a single section to HTML
*
* @param array $section Section data
* @param array|null $post_data Post data for placeholders
* @return string Section HTML
*/
public static function render_section($section, $post_data = null)
{
$type = $section['type'] ?? 'content';
$props = $section['props'] ?? [];
$layout = $section['layoutVariant'] ?? 'default';
$color_scheme = $section['colorScheme'] ?? 'default';
// Resolve all props (replace dynamic placeholders with actual values)
$resolved_props = self::resolve_props($props, $post_data);
// Generate section ID for anchor links
$section_id = $section['id'] ?? 'section-' . uniqid();
// Render based on section type
$method = 'render_' . str_replace('-', '_', $type);
if (method_exists(__CLASS__, $method)) {
return self::$method($resolved_props, $layout, $color_scheme, $section_id);
}
// Fallback: generic section wrapper
return self::render_generic($resolved_props, $type, $section_id);
}
/**
* Resolve props - replace dynamic placeholders with actual values
*
* @param array $props Section props
* @param array|null $post_data Post data
* @return array Resolved props with actual values
*/
public static function resolve_props($props, $post_data = null)
{
$resolved = [];
foreach ($props as $key => $prop) {
if (!is_array($prop)) {
$resolved[$key] = $prop;
continue;
}
$type = $prop['type'] ?? 'static';
if ($type === 'static') {
$resolved[$key] = $prop['value'] ?? '';
} elseif ($type === 'dynamic' && $post_data) {
$source = $prop['source'] ?? '';
$resolved[$key] = PlaceholderRenderer::get_value($source, $post_data);
} else {
$resolved[$key] = $prop['value'] ?? '';
}
}
return $resolved;
}
// ========================================
// Section Renderers
// ========================================
/**
* Render Hero section
*/
public static function render_hero($props, $layout, $color_scheme, $id)
{
$title = esc_html($props['title'] ?? '');
$subtitle = esc_html($props['subtitle'] ?? '');
$image = esc_url($props['image'] ?? '');
$cta_text = esc_html($props['cta_text'] ?? '');
$cta_url = esc_url($props['cta_url'] ?? '');
$html = "<section id=\"{$id}\" class=\"wn-section wn-hero wn-hero--{$layout} wn-scheme--{$color_scheme}\">";
if ($image) {
$html .= "<img src=\"{$image}\" alt=\"{$title}\" class=\"wn-hero__image\" />";
}
$html .= '<div class="wn-hero__content">';
if ($title) {
$html .= "<h1 class=\"wn-hero__title\">{$title}</h1>";
}
if ($subtitle) {
$html .= "<p class=\"wn-hero__subtitle\">{$subtitle}</p>";
}
if ($cta_text && $cta_url) {
$html .= "<a href=\"{$cta_url}\" class=\"wn-hero__cta\">{$cta_text}</a>";
}
$html .= '</div>';
$html .= '</section>';
return $html;
}
/**
* Render Content section (for post body, rich text)
*/
public static function render_content($props, $layout, $color_scheme, $id)
{
$content = $props['content'] ?? '';
// Apply WordPress content filters (shortcodes, autop, etc.)
$content = apply_filters('the_content', $content);
return "<section id=\"{$id}\" class=\"wn-section wn-content wn-scheme--{$color_scheme}\">{$content}</section>";
}
/**
* Render Image + Text section
*/
public static function render_image_text($props, $layout, $color_scheme, $id)
{
$title = esc_html($props['title'] ?? '');
$text = wp_kses_post($props['text'] ?? '');
$image = esc_url($props['image'] ?? '');
$html = "<section id=\"{$id}\" class=\"wn-section wn-image-text wn-image-text--{$layout} wn-scheme--{$color_scheme}\">";
if ($image) {
$html .= "<div class=\"wn-image-text__image\"><img src=\"{$image}\" alt=\"{$title}\" /></div>";
}
$html .= '<div class="wn-image-text__content">';
if ($title) {
$html .= "<h2 class=\"wn-image-text__title\">{$title}</h2>";
}
if ($text) {
$html .= "<div class=\"wn-image-text__text\">{$text}</div>";
}
$html .= '</div>';
$html .= '</section>';
return $html;
}
/**
* Render Feature Grid section
*/
public static function render_feature_grid($props, $layout, $color_scheme, $id)
{
$heading = esc_html($props['heading'] ?? '');
$items = $props['items'] ?? [];
$html = "<section id=\"{$id}\" class=\"wn-section wn-feature-grid wn-feature-grid--{$layout} wn-scheme--{$color_scheme}\">";
if ($heading) {
$html .= "<h2 class=\"wn-feature-grid__heading\">{$heading}</h2>";
}
$html .= '<div class="wn-feature-grid__items">';
foreach ($items as $item) {
$item_title = esc_html($item['title'] ?? '');
$item_desc = esc_html($item['description'] ?? '');
$item_icon = esc_html($item['icon'] ?? '');
$html .= '<div class="wn-feature-grid__item">';
if ($item_icon) {
$html .= "<span class=\"wn-feature-grid__icon\">{$item_icon}</span>";
}
if ($item_title) {
$html .= "<h3 class=\"wn-feature-grid__item-title\">{$item_title}</h3>";
}
if ($item_desc) {
$html .= "<p class=\"wn-feature-grid__item-desc\">{$item_desc}</p>";
}
$html .= '</div>';
}
$html .= '</div>';
$html .= '</section>';
return $html;
}
/**
* Render CTA Banner section
*/
public static function render_cta_banner($props, $layout, $color_scheme, $id)
{
$title = esc_html($props['title'] ?? '');
$text = esc_html($props['text'] ?? '');
$button_text = esc_html($props['button_text'] ?? '');
$button_url = esc_url($props['button_url'] ?? '');
$html = "<section id=\"{$id}\" class=\"wn-section wn-cta-banner wn-cta-banner--{$layout} wn-scheme--{$color_scheme}\">";
$html .= '<div class="wn-cta-banner__content">';
if ($title) {
$html .= "<h2 class=\"wn-cta-banner__title\">{$title}</h2>";
}
if ($text) {
$html .= "<p class=\"wn-cta-banner__text\">{$text}</p>";
}
if ($button_text && $button_url) {
$html .= "<a href=\"{$button_url}\" class=\"wn-cta-banner__button\">{$button_text}</a>";
}
$html .= '</div>';
$html .= '</section>';
return $html;
}
/**
* Render Contact Form section
*/
public static function render_contact_form($props, $layout, $color_scheme, $id)
{
$title = esc_html($props['title'] ?? '');
$webhook_url = esc_url($props['webhook_url'] ?? '');
$redirect_url = esc_url($props['redirect_url'] ?? '');
$fields = $props['fields'] ?? ['name', 'email', 'message'];
$html = "<section id=\"{$id}\" class=\"wn-section wn-contact-form wn-scheme--{$color_scheme}\">";
if ($title) {
$html .= "<h2 class=\"wn-contact-form__title\">{$title}</h2>";
}
// Form is rendered but won't work for bots (they just see the structure)
$html .= '<form class="wn-contact-form__form" method="post">';
foreach ($fields as $field) {
$field_label = ucfirst(str_replace('_', ' ', $field));
$html .= '<div class="wn-contact-form__field">';
$html .= "<label>{$field_label}</label>";
if ($field === 'message') {
$html .= "<textarea name=\"{$field}\" placeholder=\"{$field_label}\"></textarea>";
} else {
$html .= "<input type=\"text\" name=\"{$field}\" placeholder=\"{$field_label}\" />";
}
$html .= '</div>';
}
$html .= '<button type="submit">Submit</button>';
$html .= '</form>';
$html .= '</section>';
return $html;
}
/**
* Generic section fallback
*/
public static function render_generic($props, $type, $id)
{
$content = '';
foreach ($props as $key => $value) {
if (is_string($value)) {
$content .= "<div class=\"wn-{$type}__{$key}\">" . wp_kses_post($value) . "</div>";
}
}
return "<section id=\"{$id}\" class=\"wn-section wn-{$type}\">{$content}</section>";
}
}