- Add POST /preview/page/{slug} and /preview/template/{cpt} endpoints
- Render full HTML using PageSSR for iframe preview
- Templates use sample post for dynamic placeholder resolution
- PageSettings iframe with debounced section updates (500ms)
- Desktop/Mobile toggle with scaled iframe view
- Show/Hide preview toggle button
- Refresh button for manual preview reload
- Preview indicator banner in iframe
688 lines
22 KiB
PHP
688 lines
22 KiB
PHP
<?php
|
|
namespace WooNooW\Api;
|
|
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WP_Error;
|
|
use WooNooW\Frontend\PlaceholderRenderer;
|
|
use WooNooW\Frontend\PageSSR;
|
|
|
|
/**
|
|
* Pages Controller
|
|
* REST API for page structures and CPT templates
|
|
*/
|
|
class PagesController
|
|
{
|
|
/**
|
|
* Register API routes
|
|
*/
|
|
public static function register_routes()
|
|
{
|
|
$namespace = 'woonoow/v1';
|
|
|
|
// List all pages and templates
|
|
register_rest_route($namespace, '/pages', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_pages'],
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
|
|
// Get/Save page structure (structural pages)
|
|
register_rest_route($namespace, '/pages/(?P<slug>[a-zA-Z0-9_-]+)', [
|
|
[
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_page'],
|
|
'permission_callback' => '__return_true',
|
|
],
|
|
[
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'save_page'],
|
|
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
|
],
|
|
]);
|
|
|
|
// Get/Save CPT templates
|
|
register_rest_route($namespace, '/templates/(?P<cpt>[a-zA-Z0-9_-]+)', [
|
|
[
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_template'],
|
|
'permission_callback' => '__return_true',
|
|
],
|
|
[
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'save_template'],
|
|
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
|
],
|
|
]);
|
|
|
|
// Get post with template applied (for SPA rendering)
|
|
register_rest_route($namespace, '/content/(?P<type>[a-zA-Z0-9_-]+)/(?P<slug>[a-zA-Z0-9_-]+)', [
|
|
'methods' => 'GET',
|
|
'callback' => [__CLASS__, 'get_content_with_template'],
|
|
'permission_callback' => '__return_true',
|
|
]);
|
|
|
|
// Create new page
|
|
register_rest_route($namespace, '/pages', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'create_page'],
|
|
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
|
]);
|
|
|
|
// Preview page (render HTML for iframe)
|
|
register_rest_route($namespace, '/preview/page/(?P<slug>[a-zA-Z0-9_-]+)', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'render_page_preview'],
|
|
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
|
]);
|
|
|
|
// Preview template (render HTML for iframe)
|
|
register_rest_route($namespace, '/preview/template/(?P<cpt>[a-zA-Z0-9_-]+)', [
|
|
'methods' => 'POST',
|
|
'callback' => [__CLASS__, 'render_template_preview'],
|
|
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Check admin permission
|
|
*/
|
|
public static function check_admin_permission()
|
|
{
|
|
return current_user_can('manage_woocommerce');
|
|
}
|
|
|
|
/**
|
|
* Get all pages and templates
|
|
*/
|
|
public static function get_pages(WP_REST_Request $request)
|
|
{
|
|
$result = [];
|
|
|
|
// Get structural pages (pages with WooNooW structure)
|
|
$pages = get_posts([
|
|
'post_type' => 'page',
|
|
'posts_per_page' => -1,
|
|
'meta_query' => [
|
|
[
|
|
'key' => '_wn_page_structure',
|
|
'compare' => 'EXISTS',
|
|
],
|
|
],
|
|
]);
|
|
|
|
foreach ($pages as $page) {
|
|
$result[] = [
|
|
'id' => $page->ID,
|
|
'type' => 'page',
|
|
'slug' => $page->post_name,
|
|
'title' => $page->post_title,
|
|
'url' => get_permalink($page),
|
|
'icon' => 'page',
|
|
];
|
|
}
|
|
|
|
// Get CPT templates
|
|
$cpts = self::get_editable_post_types();
|
|
foreach ($cpts as $cpt => $label) {
|
|
$template = get_option("wn_template_{$cpt}", null);
|
|
|
|
$result[] = [
|
|
'type' => 'template',
|
|
'cpt' => $cpt,
|
|
'title' => "{$label} Template",
|
|
'icon' => 'template',
|
|
'permalink_base' => self::get_cpt_permalink_base($cpt),
|
|
'has_template' => !empty($template),
|
|
];
|
|
}
|
|
|
|
return new WP_REST_Response($result, 200);
|
|
}
|
|
|
|
/**
|
|
* Get page structure by slug
|
|
*/
|
|
public static function get_page(WP_REST_Request $request)
|
|
{
|
|
$slug = $request->get_param('slug');
|
|
|
|
// Find page by slug
|
|
$page = get_page_by_path($slug);
|
|
if (!$page) {
|
|
return new WP_Error('not_found', 'Page not found', ['status' => 404]);
|
|
}
|
|
|
|
$structure = get_post_meta($page->ID, '_wn_page_structure', true);
|
|
|
|
// Get SEO data (Yoast/Rank Math)
|
|
$seo = self::get_seo_data($page->ID);
|
|
|
|
return new WP_REST_Response([
|
|
'id' => $page->ID,
|
|
'type' => 'page',
|
|
'slug' => $page->post_name,
|
|
'title' => $page->post_title,
|
|
'url' => get_permalink($page),
|
|
'seo' => $seo,
|
|
'structure' => $structure ?: ['sections' => []],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Save page structure
|
|
*/
|
|
public static function save_page(WP_REST_Request $request)
|
|
{
|
|
$slug = $request->get_param('slug');
|
|
$body = $request->get_json_params();
|
|
|
|
// Find page by slug
|
|
$page = get_page_by_path($slug);
|
|
if (!$page) {
|
|
return new WP_Error('not_found', 'Page not found', ['status' => 404]);
|
|
}
|
|
|
|
// Validate structure
|
|
$structure = $body['sections'] ?? null;
|
|
if ($structure === null) {
|
|
return new WP_Error('invalid_data', 'Missing sections data', ['status' => 400]);
|
|
}
|
|
|
|
// Save structure
|
|
$save_data = [
|
|
'type' => 'page',
|
|
'sections' => $structure,
|
|
'updated_at' => current_time('mysql'),
|
|
];
|
|
|
|
update_post_meta($page->ID, '_wn_page_structure', $save_data);
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'page' => [
|
|
'id' => $page->ID,
|
|
'slug' => $page->post_name,
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Get CPT template
|
|
*/
|
|
public static function get_template(WP_REST_Request $request)
|
|
{
|
|
$cpt = $request->get_param('cpt');
|
|
|
|
// Validate CPT exists
|
|
if (!post_type_exists($cpt) && $cpt !== 'post') {
|
|
return new WP_Error('invalid_cpt', 'Invalid post type', ['status' => 400]);
|
|
}
|
|
|
|
$template = get_option("wn_template_{$cpt}", null);
|
|
$cpt_obj = get_post_type_object($cpt);
|
|
|
|
return new WP_REST_Response([
|
|
'type' => 'template',
|
|
'cpt' => $cpt,
|
|
'title' => $cpt_obj ? $cpt_obj->labels->singular_name . ' Template' : ucfirst($cpt) . ' Template',
|
|
'permalink_base' => self::get_cpt_permalink_base($cpt),
|
|
'available_sources' => self::get_available_sources($cpt),
|
|
'structure' => $template ?: ['sections' => []],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Save CPT template
|
|
*/
|
|
public static function save_template(WP_REST_Request $request)
|
|
{
|
|
$cpt = $request->get_param('cpt');
|
|
$body = $request->get_json_params();
|
|
|
|
// Validate CPT exists
|
|
if (!post_type_exists($cpt) && $cpt !== 'post') {
|
|
return new WP_Error('invalid_cpt', 'Invalid post type', ['status' => 400]);
|
|
}
|
|
|
|
// Validate structure
|
|
$structure = $body['sections'] ?? null;
|
|
if ($structure === null) {
|
|
return new WP_Error('invalid_data', 'Missing sections data', ['status' => 400]);
|
|
}
|
|
|
|
// Save template
|
|
$save_data = [
|
|
'type' => 'template',
|
|
'cpt' => $cpt,
|
|
'sections' => $structure,
|
|
'updated_at' => current_time('mysql'),
|
|
];
|
|
|
|
update_option("wn_template_{$cpt}", $save_data);
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'template' => [
|
|
'cpt' => $cpt,
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Get content with template applied (for SPA rendering)
|
|
*/
|
|
public static function get_content_with_template(WP_REST_Request $request)
|
|
{
|
|
$type = $request->get_param('type');
|
|
$slug = $request->get_param('slug');
|
|
|
|
// Handle structural pages
|
|
if ($type === 'page') {
|
|
return self::get_page($request);
|
|
}
|
|
|
|
// For CPT items, get post and apply template
|
|
$post = get_page_by_path($slug, OBJECT, $type);
|
|
if (!$post) {
|
|
// Try with post type 'post' if type is 'blog'
|
|
if ($type === 'blog') {
|
|
$post = get_page_by_path($slug, OBJECT, 'post');
|
|
$type = 'post';
|
|
}
|
|
}
|
|
|
|
if (!$post) {
|
|
return new WP_Error('not_found', 'Content not found', ['status' => 404]);
|
|
}
|
|
|
|
// Get template for this CPT
|
|
$template = get_option("wn_template_{$type}", null);
|
|
|
|
// Build post data
|
|
$post_data = PlaceholderRenderer::build_post_data($post);
|
|
|
|
// Get SEO data
|
|
$seo = self::get_seo_data($post->ID);
|
|
|
|
// If template exists, resolve placeholders
|
|
$rendered_sections = [];
|
|
if ($template && !empty($template['sections'])) {
|
|
foreach ($template['sections'] as $section) {
|
|
$resolved_section = $section;
|
|
$resolved_section['props'] = PageSSR::resolve_props($section['props'] ?? [], $post_data);
|
|
$rendered_sections[] = $resolved_section;
|
|
}
|
|
}
|
|
|
|
return new WP_REST_Response([
|
|
'type' => 'content',
|
|
'cpt' => $type,
|
|
'post' => $post_data,
|
|
'seo' => $seo,
|
|
'template' => $template ?: ['sections' => []],
|
|
'rendered' => [
|
|
'sections' => $rendered_sections,
|
|
],
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Create new page
|
|
*/
|
|
public static function create_page(WP_REST_Request $request)
|
|
{
|
|
$body = $request->get_json_params();
|
|
|
|
$title = sanitize_text_field($body['title'] ?? '');
|
|
$slug = sanitize_title($body['slug'] ?? $title);
|
|
|
|
if (empty($title)) {
|
|
return new WP_Error('invalid_data', 'Title is required', ['status' => 400]);
|
|
}
|
|
|
|
// Check if page already exists
|
|
if (get_page_by_path($slug)) {
|
|
return new WP_Error('exists', 'Page with this slug already exists', ['status' => 400]);
|
|
}
|
|
|
|
// Create page
|
|
$page_id = wp_insert_post([
|
|
'post_type' => 'page',
|
|
'post_title' => $title,
|
|
'post_name' => $slug,
|
|
'post_status' => 'publish',
|
|
]);
|
|
|
|
if (is_wp_error($page_id)) {
|
|
return $page_id;
|
|
}
|
|
|
|
// Initialize empty structure
|
|
$structure = [
|
|
'type' => 'page',
|
|
'sections' => [],
|
|
'created_at' => current_time('mysql'),
|
|
];
|
|
|
|
update_post_meta($page_id, '_wn_page_structure', $structure);
|
|
|
|
return new WP_REST_Response([
|
|
'success' => true,
|
|
'page' => [
|
|
'id' => $page_id,
|
|
'slug' => $slug,
|
|
'title' => $title,
|
|
'url' => get_permalink($page_id),
|
|
],
|
|
], 201);
|
|
}
|
|
|
|
// ========================================
|
|
// Helper Methods
|
|
// ========================================
|
|
|
|
/**
|
|
* Get editable post types for templates
|
|
*/
|
|
private static function get_editable_post_types()
|
|
{
|
|
$types = [
|
|
'post' => 'Blog Post',
|
|
];
|
|
|
|
// Get public custom post types
|
|
$custom_types = get_post_types([
|
|
'public' => true,
|
|
'_builtin' => false,
|
|
], 'objects');
|
|
|
|
foreach ($custom_types as $type) {
|
|
// Skip WooCommerce types (handled separately)
|
|
if (in_array($type->name, ['product', 'shop_order', 'shop_coupon'])) {
|
|
continue;
|
|
}
|
|
$types[$type->name] = $type->labels->singular_name;
|
|
}
|
|
|
|
return $types;
|
|
}
|
|
|
|
/**
|
|
* Get permalink base for a CPT
|
|
*/
|
|
private static function get_cpt_permalink_base($cpt)
|
|
{
|
|
if ($cpt === 'post') {
|
|
// Get blog permalink structure
|
|
$struct = get_option('permalink_structure');
|
|
if (strpos($struct, '%postname%') !== false) {
|
|
return '/blog/';
|
|
}
|
|
return '/';
|
|
}
|
|
|
|
$obj = get_post_type_object($cpt);
|
|
if ($obj && isset($obj->rewrite['slug'])) {
|
|
return '/' . $obj->rewrite['slug'] . '/';
|
|
}
|
|
|
|
return '/' . $cpt . '/';
|
|
}
|
|
|
|
/**
|
|
* Get available dynamic sources for a CPT
|
|
*/
|
|
private static function get_available_sources($cpt)
|
|
{
|
|
$sources = [
|
|
['value' => 'post_title', 'label' => 'Title'],
|
|
['value' => 'post_content', 'label' => 'Content'],
|
|
['value' => 'post_excerpt', 'label' => 'Excerpt'],
|
|
['value' => 'post_featured_image', 'label' => 'Featured Image'],
|
|
['value' => 'post_author', 'label' => 'Author'],
|
|
['value' => 'post_date', 'label' => 'Date'],
|
|
['value' => 'post_url', 'label' => 'Permalink'],
|
|
];
|
|
|
|
// Add taxonomy sources
|
|
$taxonomies = get_object_taxonomies($cpt, 'objects');
|
|
foreach ($taxonomies as $tax) {
|
|
if ($tax->public) {
|
|
$sources[] = [
|
|
'value' => $tax->name,
|
|
'label' => $tax->labels->name,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Add related posts source
|
|
$sources[] = ['value' => 'related_posts', 'label' => 'Related Posts'];
|
|
|
|
return $sources;
|
|
}
|
|
|
|
/**
|
|
* Get SEO data for a post (Yoast/Rank Math compatible)
|
|
*/
|
|
private static function get_seo_data($post_id)
|
|
{
|
|
$seo = [];
|
|
|
|
// Try Yoast
|
|
$seo['meta_title'] = get_post_meta($post_id, '_yoast_wpseo_title', true) ?: get_the_title($post_id);
|
|
$seo['meta_description'] = get_post_meta($post_id, '_yoast_wpseo_metadesc', true);
|
|
$seo['canonical'] = get_post_meta($post_id, '_yoast_wpseo_canonical', true) ?: get_permalink($post_id);
|
|
$seo['og_title'] = get_post_meta($post_id, '_yoast_wpseo_opengraph-title', true);
|
|
$seo['og_description'] = get_post_meta($post_id, '_yoast_wpseo_opengraph-description', true);
|
|
$seo['og_image'] = get_post_meta($post_id, '_yoast_wpseo_opengraph-image', true);
|
|
|
|
// Try Rank Math if Yoast not available
|
|
if (empty($seo['meta_description'])) {
|
|
$seo['meta_description'] = get_post_meta($post_id, 'rank_math_description', true);
|
|
}
|
|
|
|
return $seo;
|
|
}
|
|
|
|
/**
|
|
* Render page preview HTML (for editor iframe)
|
|
*/
|
|
public static function render_page_preview(WP_REST_Request $request)
|
|
{
|
|
$slug = $request->get_param('slug');
|
|
$body = $request->get_json_params();
|
|
|
|
// Get sections from POST body (unsaved changes)
|
|
$sections = $body['sections'] ?? [];
|
|
|
|
// Find page for title
|
|
$page = get_page_by_path($slug);
|
|
$title = $page ? $page->post_title : 'Preview';
|
|
|
|
// Render HTML
|
|
$html = self::render_preview_html($title, $sections, 'page');
|
|
|
|
// Return as HTML response
|
|
return new WP_REST_Response([
|
|
'html' => $html,
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Render template preview HTML (for editor iframe)
|
|
*/
|
|
public static function render_template_preview(WP_REST_Request $request)
|
|
{
|
|
$cpt = $request->get_param('cpt');
|
|
$body = $request->get_json_params();
|
|
|
|
// Get sections from POST body
|
|
$sections = $body['sections'] ?? [];
|
|
|
|
// Get sample post for dynamic placeholders
|
|
$sample_post = null;
|
|
if ($cpt && $cpt !== 'page') {
|
|
$posts = get_posts([
|
|
'post_type' => $cpt,
|
|
'posts_per_page' => 1,
|
|
'post_status' => 'publish',
|
|
]);
|
|
if (!empty($posts)) {
|
|
$sample_post = $posts[0];
|
|
}
|
|
}
|
|
|
|
// Resolve placeholders if sample post exists
|
|
$resolved_sections = $sections;
|
|
if ($sample_post) {
|
|
$post_data = PlaceholderRenderer::build_post_data($sample_post);
|
|
$resolved_sections = [];
|
|
foreach ($sections as $section) {
|
|
$resolved_section = $section;
|
|
$resolved_section['props'] = PageSSR::resolve_props($section['props'] ?? [], $post_data);
|
|
$resolved_sections[] = $resolved_section;
|
|
}
|
|
}
|
|
|
|
$cpt_obj = get_post_type_object($cpt);
|
|
$title = $cpt_obj ? $cpt_obj->labels->singular_name . ' Preview' : 'Template Preview';
|
|
|
|
// Render HTML
|
|
$html = self::render_preview_html($title, $resolved_sections, 'template', $sample_post);
|
|
|
|
return new WP_REST_Response([
|
|
'html' => $html,
|
|
'sample_post' => $sample_post ? [
|
|
'id' => $sample_post->ID,
|
|
'title' => $sample_post->post_title,
|
|
] : null,
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Helper: Render preview HTML document
|
|
*/
|
|
private static function render_preview_html($title, $sections, $type, $sample_post = null)
|
|
{
|
|
// Get site URL for assets
|
|
$plugin_url = plugins_url('', dirname(dirname(__FILE__)));
|
|
|
|
// Start output buffering
|
|
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($title); ?> - Preview</title>
|
|
<style>
|
|
/* Reset and base styles */
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
line-height: 1.6;
|
|
color: #1f2937;
|
|
background: #fff;
|
|
}
|
|
img { max-width: 100%; height: auto; }
|
|
|
|
/* Section base */
|
|
.wn-section { padding: 4rem 1rem; }
|
|
.wn-container { max-width: 1200px; margin: 0 auto; }
|
|
|
|
/* Color schemes */
|
|
.wn-scheme-default { background: #fff; color: #1f2937; }
|
|
.wn-scheme-primary { background: #3b82f6; color: #fff; }
|
|
.wn-scheme-secondary { background: #1f2937; color: #fff; }
|
|
.wn-scheme-muted { background: #f3f4f6; color: #1f2937; }
|
|
.wn-scheme-gradient {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: #fff;
|
|
}
|
|
|
|
/* Hero section */
|
|
.wn-hero { text-align: center; padding: 6rem 1rem; }
|
|
.wn-hero h1 { font-size: 2.5rem; font-weight: 800; margin-bottom: 1rem; }
|
|
.wn-hero p { font-size: 1.25rem; opacity: 0.9; margin-bottom: 2rem; }
|
|
.wn-hero .wn-btn {
|
|
display: inline-block; padding: 0.75rem 1.5rem;
|
|
background: currentColor; color: inherit;
|
|
border-radius: 0.5rem; text-decoration: none;
|
|
filter: invert(1); font-weight: 600;
|
|
}
|
|
|
|
/* Content section */
|
|
.wn-content { padding: 3rem 1rem; }
|
|
.wn-content.wn-narrow .wn-container { max-width: 720px; }
|
|
.wn-content.wn-medium .wn-container { max-width: 960px; }
|
|
|
|
/* Image + Text */
|
|
.wn-image-text { display: flex; gap: 3rem; align-items: center; flex-wrap: wrap; }
|
|
.wn-image-text .wn-image { flex: 1; min-width: 300px; }
|
|
.wn-image-text .wn-text { flex: 1; min-width: 300px; }
|
|
.wn-image-text.wn-image-right { flex-direction: row-reverse; }
|
|
|
|
/* Feature grid */
|
|
.wn-features { display: grid; gap: 2rem; }
|
|
.wn-features.wn-grid-2 { grid-template-columns: repeat(2, 1fr); }
|
|
.wn-features.wn-grid-3 { grid-template-columns: repeat(3, 1fr); }
|
|
.wn-features.wn-grid-4 { grid-template-columns: repeat(4, 1fr); }
|
|
@media (max-width: 768px) {
|
|
.wn-features { grid-template-columns: 1fr; }
|
|
}
|
|
.wn-feature { text-align: center; padding: 1.5rem; }
|
|
.wn-feature-icon { font-size: 2rem; margin-bottom: 1rem; }
|
|
|
|
/* CTA Banner */
|
|
.wn-cta { text-align: center; padding: 4rem 1rem; }
|
|
.wn-cta h2 { font-size: 2rem; margin-bottom: 1rem; }
|
|
|
|
/* Contact form */
|
|
.wn-contact form { max-width: 500px; margin: 0 auto; }
|
|
.wn-contact input, .wn-contact textarea {
|
|
width: 100%; padding: 0.75rem; margin-bottom: 1rem;
|
|
border: 1px solid #d1d5db; border-radius: 0.375rem;
|
|
}
|
|
.wn-contact button {
|
|
width: 100%; padding: 0.75rem; background: #3b82f6;
|
|
color: #fff; border: none; border-radius: 0.375rem;
|
|
cursor: pointer; font-weight: 600;
|
|
}
|
|
|
|
/* Preview indicator */
|
|
.wn-preview-indicator {
|
|
position: fixed; top: 0; left: 0; right: 0;
|
|
background: #f59e0b; color: #000; text-align: center;
|
|
padding: 0.5rem; font-size: 0.875rem; font-weight: 500;
|
|
z-index: 9999;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wn-preview-indicator">
|
|
🔍 Preview Mode <?php if ($sample_post): ?>(Using: <?php echo esc_html($sample_post->post_title); ?>)<?php endif; ?>
|
|
</div>
|
|
|
|
<main style="padding-top: 2.5rem;">
|
|
<?php
|
|
foreach ($sections as $section) {
|
|
echo PageSSR::render_section($section, $sample_post ? PlaceholderRenderer::build_post_data($sample_post) : []);
|
|
}
|
|
|
|
if (empty($sections)) {
|
|
echo '<div style="text-align:center; padding:4rem; color:#9ca3af;">';
|
|
echo '<p>No sections added yet.</p>';
|
|
echo '<p>Add sections in the editor to see preview.</p>';
|
|
echo '</div>';
|
|
}
|
|
?>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
}
|