Files
WooNooW/includes/Api/PagesController.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

474 lines
15 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'],
]);
}
/**
* 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;
}
}