feat: Page Editor live preview
- 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
This commit is contained in:
@@ -68,6 +68,20 @@ class PagesController
|
||||
'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'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,4 +484,204 @@ class PagesController
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user