- 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
474 lines
15 KiB
PHP
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;
|
|
}
|
|
}
|