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
This commit is contained in:
Dwindi Ramadhana
2026-01-11 22:29:30 +07:00
parent 1ff9a36af3
commit 9331989102
6 changed files with 1857 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
<?php
namespace WooNooW\Frontend;
/**
* Placeholder Renderer
* Resolves dynamic placeholders to actual post/CPT data
*/
class PlaceholderRenderer
{
/**
* Get value for a dynamic placeholder source
*
* @param string $source Placeholder source (e.g., 'post_title', 'post_content')
* @param array $post_data Post data array
* @return mixed Resolved value
*/
public static function get_value($source, $post_data)
{
if (empty($source) || empty($post_data)) {
return '';
}
// Standard post fields
switch ($source) {
case 'post_title':
case 'title':
return $post_data['title'] ?? $post_data['post_title'] ?? '';
case 'post_content':
case 'content':
return $post_data['content'] ?? $post_data['post_content'] ?? '';
case 'post_excerpt':
case 'excerpt':
return $post_data['excerpt'] ?? $post_data['post_excerpt'] ?? '';
case 'post_featured_image':
case 'featured_image':
return $post_data['featured_image'] ??
$post_data['thumbnail'] ??
$post_data['_thumbnail_url'] ?? '';
case 'post_author':
case 'author':
return $post_data['author'] ?? $post_data['post_author'] ?? '';
case 'post_date':
case 'date':
return $post_data['date'] ?? $post_data['post_date'] ?? '';
case 'post_categories':
case 'categories':
return $post_data['categories'] ?? [];
case 'post_tags':
case 'tags':
return $post_data['tags'] ?? [];
case 'post_url':
case 'url':
case 'permalink':
return $post_data['url'] ?? $post_data['permalink'] ?? '';
}
// Check for custom meta fields (format: {cpt}_field_{name})
if (strpos($source, '_field_') !== false) {
$parts = explode('_field_', $source);
$field_name = end($parts);
// Try to get from meta array
if (isset($post_data['meta'][$field_name])) {
return $post_data['meta'][$field_name];
}
// Try direct field access
if (isset($post_data[$field_name])) {
return $post_data[$field_name];
}
}
// Check for direct key match
if (isset($post_data[$source])) {
return $post_data[$source];
}
return '';
}
/**
* Build post data array from WP_Post object
*
* @param \WP_Post|int $post Post object or ID
* @return array Post data array
*/
public static function build_post_data($post)
{
if (is_numeric($post)) {
$post = get_post($post);
}
if (!$post || !($post instanceof \WP_Post)) {
return [];
}
$data = [
'id' => $post->ID,
'title' => $post->post_title,
'content' => apply_filters('the_content', $post->post_content),
'excerpt' => $post->post_excerpt ?: wp_trim_words($post->post_content, 30),
'date' => get_the_date('', $post),
'date_iso' => get_the_date('c', $post),
'url' => get_permalink($post),
'slug' => $post->post_name,
'type' => $post->post_type,
];
// Author
$author_id = $post->post_author;
$data['author'] = get_the_author_meta('display_name', $author_id);
$data['author_url'] = get_author_posts_url($author_id);
// Featured image
$thumbnail_id = get_post_thumbnail_id($post);
if ($thumbnail_id) {
$data['featured_image'] = get_the_post_thumbnail_url($post, 'large');
$data['featured_image_id'] = $thumbnail_id;
}
// Taxonomies
$taxonomies = get_object_taxonomies($post->post_type);
foreach ($taxonomies as $taxonomy) {
$terms = get_the_terms($post, $taxonomy);
if ($terms && !is_wp_error($terms)) {
$data[$taxonomy] = array_map(function($term) {
return [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'url' => get_term_link($term),
];
}, $terms);
}
}
// Shortcuts for common taxonomies
if (isset($data['category'])) {
$data['categories'] = $data['category'];
}
if (isset($data['post_tag'])) {
$data['tags'] = $data['post_tag'];
}
// Custom meta fields
$meta = get_post_meta($post->ID);
if ($meta) {
$data['meta'] = [];
foreach ($meta as $key => $values) {
// Skip internal meta keys
if (strpos($key, '_') === 0) {
continue;
}
$data['meta'][$key] = count($values) === 1 ? $values[0] : $values;
}
}
return $data;
}
/**
* Get related posts
*
* @param int $post_id Current post ID
* @param int $count Number of related posts
* @param string $post_type Post type
* @return array Related posts data
*/
public static function get_related_posts($post_id, $count = 3, $post_type = 'post')
{
// Get categories of current post
$categories = get_the_category($post_id);
$category_ids = wp_list_pluck($categories, 'term_id');
$args = [
'post_type' => $post_type,
'posts_per_page' => $count,
'post__not_in' => [$post_id],
'orderby' => 'date',
'order' => 'DESC',
];
if (!empty($category_ids)) {
$args['category__in'] = $category_ids;
}
$query = new \WP_Query($args);
$related = [];
foreach ($query->posts as $post) {
$related[] = [
'id' => $post->ID,
'title' => $post->post_title,
'excerpt' => $post->post_excerpt ?: wp_trim_words($post->post_content, 20),
'url' => get_permalink($post),
'featured_image' => get_the_post_thumbnail_url($post, 'medium'),
'date' => get_the_date('', $post),
];
}
wp_reset_postdata();
return $related;
}
}