'POST', 'callback' => [__CLASS__, 'unset_spa_landing'], 'permission_callback' => [__CLASS__, 'check_admin_permission'], ]); // 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[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 template presets (Must be before generic template cpt route) register_rest_route($namespace, '/templates/presets', [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_template_presets'], 'permission_callback' => '__return_true', ]); // Get/Save CPT templates register_rest_route($namespace, '/templates/(?P[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[a-zA-Z0-9_-]+)/(?P[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[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[a-zA-Z0-9_-]+)', [ 'methods' => 'POST', 'callback' => [__CLASS__, 'render_template_preview'], 'permission_callback' => [__CLASS__, 'check_admin_permission'], ]); // Set page as SPA Landing (shown at SPA root route) register_rest_route($namespace, '/pages/(?P\d+)/set-as-spa-landing', [ 'methods' => 'POST', 'callback' => [__CLASS__, 'set_as_spa_landing'], 'permission_callback' => [__CLASS__, 'check_admin_permission'], ]); // Delete page register_rest_route($namespace, '/pages/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [__CLASS__, 'delete_page'], 'permission_callback' => [__CLASS__, 'check_admin_permission'], ]); } /** * Check admin permission */ public static function check_admin_permission() { return current_user_can('manage_woocommerce'); } /** * Get available template presets */ public static function get_template_presets() { return new WP_REST_Response(TemplateRegistry::get_templates(), 200); } /** * Get all editable pages (and templates) */ public static function get_pages() { $result = []; // Get SPA settings $settings = get_option('woonoow_appearance_settings', []); $spa_frontpage_id = $settings['general']['spa_frontpage'] ?? 0; // 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', 'is_spa_frontpage' => (int)$page->ID === (int)$spa_frontpage_id, ]; } // 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 SPA settings $settings = get_option('woonoow_appearance_settings', []); $spa_frontpage_id = $settings['general']['spa_frontpage'] ?? 0; // 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, 'seo' => $seo, 'is_spa_frontpage' => (int)$page->ID === (int)$spa_frontpage_id, '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); // Invalidate SSR cache delete_transient("wn_ssr_page_{$page->ID}"); 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); } /** * Set page as SPA Landing (the page shown at SPA root route) * This does NOT affect WordPress page_on_front setting. */ public static function set_as_spa_landing(WP_REST_Request $request) { $id = (int)$request->get_param('id'); // Verify the page exists $page = get_post($id); if (!$page || $page->post_type !== 'page') { return new WP_Error('invalid_page', 'Page not found', ['status' => 404]); } // Update WooNooW SPA settings - set this page as the SPA frontpage $settings = get_option('woonoow_appearance_settings', []); if (!isset($settings['general'])) { $settings['general'] = []; } $settings['general']['spa_frontpage'] = $id; update_option('woonoow_appearance_settings', $settings); return new WP_REST_Response([ 'success' => true, 'id' => $id, 'message' => 'SPA Landing page set successfully' ], 200); } /** * Unset SPA Landing (the page shown at SPA root route) * After unsetting, SPA will redirect to /shop or /checkout based on mode */ public static function unset_spa_landing(WP_REST_Request $request) { // Update WooNooW SPA settings - clear the SPA frontpage $settings = get_option('woonoow_appearance_settings', []); if (isset($settings['general'])) { $settings['general']['spa_frontpage'] = 0; } update_option('woonoow_appearance_settings', $settings); return new WP_REST_Response([ 'success' => true, 'message' => 'SPA Landing page unset. Root will now redirect to shop/checkout.' ], 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'), ]; // Apply template if provided $template_id = $body['templateId'] ?? null; if ($template_id) { $template = TemplateRegistry::get_template($template_id); if ($template) { $structure['sections'] = $template['sections']; } } 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); } /** * Delete page */ public static function delete_page(WP_REST_Request $request) { $id = (int)$request->get_param('id'); $page = get_post($id); if (!$page || $page->post_type !== 'page') { return new WP_Error('not_found', 'Page not found', ['status' => 404]); } // Check if it's the SPA front page $settings = get_option('woonoow_appearance_settings', []); $spa_frontpage_id = $settings['general']['spa_frontpage'] ?? 0; if ((int)$id === (int)$spa_frontpage_id) { // Unset SPA frontpage if deleting it if (isset($settings['general'])) { $settings['general']['spa_frontpage'] = 0; update_option('woonoow_appearance_settings', $settings); } } $deleted = wp_delete_post($id, true); // Force delete if (!$deleted) { return new WP_Error('delete_failed', 'Failed to delete page', ['status' => 500]); } return new WP_REST_Response([ 'success' => true, 'id' => $id, 'message' => 'Page deleted successfully' ], 200); } // ======================================== // 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(); ?> > <?php echo esc_html($title); ?> - Preview
🔍 Preview Mode (Using: post_title); ?>)
'; echo '

No sections added yet.

'; echo '

Add sections in the editor to see preview.

'; echo ''; } ?>