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
This commit is contained in:
290
includes/Frontend/PageSSR.php
Normal file
290
includes/Frontend/PageSSR.php
Normal file
@@ -0,0 +1,290 @@
|
||||
<?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>";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user