Files
WooNooW/woonoow-page-editor-brief.md
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

24 KiB

WooNooW Page Editor Strategy - Final Decision Brief (v3)

Overview

Extend WooNooW SPA to support structural pages and CPT templates with:

  • Unified Page Editor for pages (page CPT) and CPT templates (post, portfolio, custom)
  • Native WordPress page CPT for structural pages (SEO compatible)
  • Dynamic placeholders for CPT templates (blog posts, portfolio items, etc.)
  • Dual rendering strategy for bots vs humans (SSR + SPA redirect)
  • Zero page builder plugin dependencies
  • Integrated admin UI with collapsible sidebar

Core Architecture

Data Storage

Structural Pages (page CPT)

  • Location: WordPress wp_posts table (post_type='page')
  • Structure Storage: wp_postmeta, key: _wn_page_structure
  • Type: Static content only (no placeholders)
  • Example: Homepage, About, Contact, Terms, Privacy

CPT Templates

  • Location: WordPress wp_options table
  • Storage Keys: wn_template_{cpt_name}
    • wn_template_post (for blog posts)
    • wn_template_portfolio (for portfolio items)
    • wn_template_custom_cpt (for custom CPTs)
  • Type: Dynamic content with placeholders
  • Applies To: All items of that CPT (one template per CPT)

Page Structure Storage Format

{
  "type": "page|template",
  "sections": [
    {
      "id": "section-1",
      "type": "hero",
      "layoutVariant": "hero-left-image",
      "colorScheme": "primary",
      "props": {
        "title": {
          "type": "static",
          "value": "Welcome"
        },
        "subtitle": {
          "type": "static",
          "value": "Join us"
        },
        "image": {
          "type": "static",
          "value": "/uploads/hero.jpg"
        }
      }
    },
    {
      "id": "section-2",
      "type": "content",
      "props": {
        "content": {
          "type": "dynamic",
          "source": "post_content"
        }
      }
    },
    {
      "id": "section-3",
      "type": "feature-grid",
      "layoutVariant": "grid-3-columns",
      "props": {
        "heading": {
          "type": "static",
          "value": "Why Choose Us"
        }
      }
    }
  ]
}

Field Types: Static vs Dynamic

Static Fields (Structural Pages):

{
  "type": "static",
  "value": "Hardcoded text or value"
}

Dynamic Fields (CPT Templates):

{
  "type": "dynamic",
  "source": "post_title|post_content|post_excerpt|post_featured_image|related_posts|..."
}

Available Dynamic Sources (per CPT)

For post CPT:

  • post_title - Post title
  • post_excerpt - Post excerpt
  • post_content - Post body content
  • post_featured_image - Featured image URL
  • post_author - Author name
  • post_date - Publication date
  • post_categories - Category list
  • post_tags - Tag list
  • related_posts - Query for related posts (with count parameter)

For Custom CPTs:

  • {cpt_name}_title - CPT item title
  • {cpt_name}_featured_image - Featured image
  • {cpt_name}_field_{field_name} - Custom meta fields
  • Custom sources defined per CPT

Unified Page Editor Concept

Single Editor, Two Modes

Mode 1: Structural Pages (Static)

  • Edit individual pages (page CPT)
  • All content is static/hardcoded
  • Examples: Home, About, Contact, Terms, Privacy
  • SEO managed by Yoast/Rank Math

Mode 2: CPT Templates (Dynamic)

  • Design template for entire CPT
  • Content has dynamic placeholders
  • One template applies to all items of that CPT
  • Examples: Blog Post template, Portfolio template, News template

Create New Page Flow

When user clicks [+ Create Page]:

Modal Dialog:
┌──────────────────────────────────┐
│ Create New Page                  │
├──────────────────────────────────┤
│                                  │
│ What are you creating?           │
│                                  │
│ ○ Structural Page                │
│   Static content (page CPT)       │
│   Examples: About, Contact       │
│   Permalink: /page-slug          │
│                                  │
│ ○ Blog Post Template             │
│   Dynamic template for posts     │
│   Applies to: /blog/*            │
│   Includes placeholders          │
│                                  │
│ ○ Portfolio Template             │
│   Dynamic template for portfolio │
│   Applies to: /portfolio/*       │
│   Includes placeholders          │
│                                  │
│ ○ Other CPT Template             │
│   [Select CPT ▼]                 │
│   Applies to: /cpt-base/*        │
│                                  │
│ [Cancel]  [Next]                 │
└──────────────────────────────────┘

Editor Left Panel: Pages List (Unified)

Pages Sidebar

📄 STRUCTURAL PAGES
├─ ✓ Homepage (page)
├─ ○ About (page)
├─ ○ Contact (page)
└─ ○ Terms (page)

📋 TEMPLATES (Dynamic)
├─ ○ Blog Post (post)
│  └─ Applies to: /blog/*
├─ ○ Portfolio (portfolio)
│  └─ Applies to: /portfolio/*
├─ ○ News (custom_cpt)
│  └─ Applies to: /news/*
└─ ○ Custom CPT (custom_name)
   └─ Applies to: /custom-base/*

Visual indicators:
- 📄 = Structural page (static)
- 📋 = Template (dynamic)

Rendering Strategy: Dual Path

Path 1: Bot Detection → Server-Side Rendering (SSR)

When: Bot user agent detected
Action: Serve pre-rendered HTML (no redirect)
Content: All sections rendered as static HTML strings
Placeholders: Replaced with actual post data at render time
Result: Full content indexed by search engines

Bot visits /blog/my-article
  ↓
is_bot() === true
  ↓
Get post data + load post template
  ↓
Replace placeholders (post_title, post_content, etc.)
  ↓
Render sections to static HTML
  ↓
Output: <html><h1>Article Title</h1><p>Post content...</p></html>
  ↓
✓ Bot crawls full content
✓ All text indexed
✓ All links followable
✓ SEO perfect

Path 2: Human Detection → SPA Redirect

When: Human user agent detected
Action: HTTP 302 redirect to SPA single-page app
Destination: Matches WordPress permalink structure

  • /about/store/about
  • /blog/slug/store/blog/slug
  • /portfolio/slug/store/portfolio/slug Result: Beautiful interactive React UI
Human visits /blog/my-article
  ↓
is_bot() === false
  ↓
HTTP 302 → /store/blog/my-article
  ↓
SPA loads at /store/blog/my-article
  ↓
React detects path base (/blog) → loads post template
  ↓
React fetches post data for "my-article"
  ↓
React replaces placeholders with post data
  ↓
Renders sections with React components
  ↓
✓ Interactive UI
✓ No page reload
✓ Great UX

Bot Detection Logic

Detect bots by User-Agent string:

$bot_patterns = [
  'googlebot', 'bingbot', 'slurp', 'duckduckbot',
  'baiduspider', 'yandexbot', 'crawler', 'robot',
  'spider', 'facebookexternalhit', 'twitterbot',
  'linkedinbot', 'whatsapp', 'slackbot', 'applebot'
];

SPA Architecture (Enhanced)

SPA Remains Centralized

  • Single page location: WordPress page (configurable, e.g., /store/)
  • Single React router: Handles all SPA routes
  • Routes:
    • / - Homepage (products)
    • /shop - Shop listing
    • /product/:slug - Product detail
    • /cart - Shopping cart
    • /checkout - Checkout
    • /my-account/* - User account
    • NEW Dynamic Routes:
      • /:slug - Structural page (About, Contact, etc.)
      • /:pathBase/:slug - CPT item (blog post, portfolio, etc.)

Dynamic Route Matching

// React Router detects what to render
const router = createBrowserRouter([
  // Existing routes (higher priority)
  { path: '/', element: <Shop /> },
  { path: '/shop', element: <Shop /> },
  { path: '/product/:slug', element: <Product /> },
  
  // NEW: Dynamic routes (lower priority)
  { path: '/:pathBase/:slug', element: <DynamicPageRenderer /> },
  { path: '/:slug', element: <DynamicPageRenderer /> },
]);

// DynamicPageRenderer logic
function DynamicPageRenderer() {
  const { pathBase, slug } = useParams();
  const [pageData, setPageData] = useState(null);
  
  useEffect(() => {
    // Determine what template to use
    if (!pathBase) {
      // /:slug → Structural page (About, Contact, etc.)
      fetchPage(slug);
    } else {
      // /:pathBase/:slug → CPT item
      detectCPTFromPath(pathBase); // /blog → post, /portfolio → portfolio
      fetchCPTTemplate(cptType);
      fetchPostData(slug);
    }
  }, [pathBase, slug]);
  
  return <PageRenderer pageData={pageData} />;
}

No Multiple "SPA Islands"

  • Pages route through same SPA instance
  • Same styling, same theme variables, same JavaScript context

Page Editor UI Layout (3-Column Design)

Visual Layout

┌─────────────────────────────────────────────────────────────┐
│ [☰] WooNooW    Appearance > Pages > [Blog Post Template]   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│ ┌────────────┬──────────────────┬──────────────────────┐   │
│ │   Pages    │                  │                      │   │
│ │   Sidebar  │   Editor Panel   │  Settings + Preview  │   │
│ │            │                  │                      │   │
│ │ 📄 Home    │ SECTIONS         │ Page Settings        │   │
│ │ ○ About    │ ────────────────│ ├─ Type: Template    │   │
│ │ ○ Contact  │ 1. Hero Section │ ├─ CPT: post        │   │
│ │            │    [Image ◆]    │ └─ Permalink: /blog │   │
│ │ 📋 Posts   │ 2. Content      │                      │   │
│ │ ○ Portfolio│    [Post Body ◆]│ Section Settings     │   │
│ │ ○ News     │ 3. Related Posts│ ├─ Layout: Grid 3   │   │
│ │            │    [Query ◆]    │ ├─ Count: [3___]    │   │
│ │ [+ Create] │ 4. CTA Banner   │ └─ [Delete]         │   │
│ │            │                  │                      │   │
│ │            │ [+ Add Section] │ Preview             │   │
│ │            │                  │ [Desktop] [Mobile]  │   │
│ │            │ Legend:         │ ┌──────────────────┐│   │
│ │            │ [◆] = Dynamic   │ │ Live SPA Preview ││   │
│ │            │     placeholder │ │ (Real-time)      ││   │
│ │            │                  │ └──────────────────┘│   │
│ │            │                  │                      │   │
│ └────────────┴──────────────────┴──────────────────────┘   │
│                                                             │
│ [Discard Changes]  [Save Changes]  [Preview]               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Settings Panel Behavior

For Structural Pages (No Section Selected):

Page Settings
├─ Type: Page (Structural)
├─ Title: About Us
├─ Slug: about
└─ SEO: Manage in Yoast [→]

For Templates (No Section Selected):

Template Settings
├─ Type: Template (Dynamic)
├─ CPT: Post
├─ Applies to: /blog/*
└─ Edit CPT permalink: [→]

For Sections with Dynamic Fields:

Section Settings: Hero

Layout Variant
[Hero Left Image ▼]

Title Field
○ Static: [____________]
◉ Dynamic: [Post Title ◆]

Image Field
○ Static: [Choose Image]
◉ Dynamic: [Featured Image ◆]

[Delete Section]

Admin SPA Integration

Navigation Location

  • Menu: Appearance (existing)
  • Submenu: Pages (NEW)
  • Path: /admin/appearance/pages

Sidebar Behavior

Default (Expanded):

  • Admin sidebar shows full menu with labels
  • Page editor takes remaining width (3-column layout)

Collapsed:

  • Admin sidebar shows icons only (~60px width)
  • Page editor expands to use more width
  • Tooltips show on hover

Layout dimensions:

State Sidebar Content Area Pages List Editor Settings+Preview
Expanded 240px ~1160px 120px 650px 390px
Collapsed 60px ~1340px 120px 750px 470px

No Full-Screen Takeover

  • Part of admin SPA normal content flow
  • User maintains context (can quickly jump to other admin sections)
  • Sidebar collapse is user-controlled, not forced
  • Can navigate back via breadcrumb or admin menu

Escape Routes

  1. Click WooNooW logo → Dashboard
  2. Click any menu item in sidebar → Navigate to that section
  3. Press ESC key → Back (with unsaved changes warning)
  4. Click browser back button → Back (with unsaved changes warning)
  5. Click breadcrumb "Appearance" → Back to Appearance settings

SEO Preservation

Yoast/Rank Math Integration

  • Meta title, meta description managed by Yoast/Rank Math
  • Canonical URLs managed by WordPress
  • Open Graph tags managed by SEO plugins
  • Structured data can be added to page template
  • No changes needed to existing SEO workflow

For CPT Items (Posts, Portfolio, etc.)

  • Yoast/Rank Math still manages SEO for individual CPT items
  • Templates don't interfere with SEO workflow
  • Bot sees pre-rendered content with all placeholders filled

What Bots See

Example: Blog Post with Template

<h1>Article Title</h1>
<img src="/uploads/featured.jpg" alt="Article featured image" />
<article>
  <p>Article body content goes here...</p>
</article>
<section class="related-posts">
  <h2>Related Articles</h2>
  <div class="grid">
    <a href="/blog/related-1">Related Post 1</a>
    <a href="/blog/related-2">Related Post 2</a>
  </div>
</section>
  • All text crawlable
  • All links followable
  • Structure preserved
  • Meta tags present
  • Images indexed

API Endpoints

Get All Pages & Templates

GET /wp-json/woonoow/v1/pages

Response:

[
  {
    "id": 42,
    "type": "page",
    "slug": "home",
    "title": "Homepage",
    "icon": "page"
  },
  {
    "type": "template",
    "cpt": "post",
    "title": "Blog Post Template",
    "icon": "template",
    "permalink_base": "/blog/"
  }
]

Get Page or Template Structure

GET /wp-json/woonoow/v1/pages/about
GET /wp-json/woonoow/v1/templates/post

Response:

{
  "type": "page|template",
  "id": 42,
  "slug": "about",
  "title": "About Us",
  "url": "https://mystore.com/about",
  "seo": {
    "meta_title": "About Us | My Store",
    "meta_description": "Learn about our company...",
    "canonical": "https://mystore.com/about",
    "og_title": "About Us",
    "og_image": "https://mystore.com/og-image.jpg"
  },
  "structure": {
    "sections": [...]
  }
}

Get Single Post with Template Applied

GET /wp-json/woonoow/v1/posts/{post_id}/with-template

Response:

{
  "post": {
    "id": 123,
    "title": "Article Title",
    "content": "Article body...",
    "featured_image": "/uploads/image.jpg",
    "author": "John Doe",
    "date": "2026-01-11"
  },
  "template": {
    "sections": [...]
  },
  "rendered": {
    "sections": [
      {
        "type": "hero",
        "props": {
          "title": "Article Title",  // Placeholder filled
          "image": "/uploads/image.jpg"  // Placeholder filled
        }
      }
    ]
  }
}

Save Page or Template Structure

POST /wp-json/woonoow/v1/pages/about
POST /wp-json/woonoow/v1/templates/post

Request body:

{
  "sections": [...]
}

Response:

{
  "success": true,
  "page": { ... }
}

Implementation Phases

Phase 1: Core Infrastructure (Priority)

  • Add bot detection logic to TemplateOverride.php
  • Build PageSSR class to render sections as HTML
  • Build PlaceholderRenderer class to replace dynamic placeholders
  • Create REST API endpoints:
    • GET /wp-json/woonoow/v1/pages (list all)
    • GET /wp-json/woonoow/v1/pages/{slug} (get page)
    • GET /wp-json/woonoow/v1/templates/{cpt} (get template)
    • GET /wp-json/woonoow/v1/posts/{id}/with-template (get post with template)
    • POST /wp-json/woonoow/v1/pages/{slug} (save page)
    • POST /wp-json/woonoow/v1/templates/{cpt} (save template)
  • Update template_redirect hook:
    • Detect if structural page or CPT item
    • Load appropriate template
    • Replace placeholders (for CPT items)
    • Serve SSR for bots (exit without redirect)
    • Redirect humans to SPA with matching permalink structure
  • Create React DynamicPageRenderer component
  • Test bot crawling vs human browsing

Phase 2: Admin SPA Integration

  • Add "Pages" submenu to Appearance menu
  • Create /admin/appearance/pages route
  • Build PageEditor component with 3-column layout
  • Implement sidebar collapse/expand functionality
  • Add breadcrumb navigation
  • Implement create page modal (structural vs template selection)
  • Test navigation flow

Phase 3: WooNooW Page Editor (UI)

  • Build custom admin page editor UI (React)
  • Section list with drag-to-reorder
  • Section selector popup when clicking "+ Add Section"
  • Layout variant dropdown per section
  • Color scheme selector per section
  • Form fields for section content:
    • Static field: text input
    • Dynamic field: dropdown with available sources
  • Live SPA preview iframe (real-time updates)
  • Desktop/Mobile preview toggle
  • Save/Publish button → stores to postmeta or wp_options
  • Visual indicator for dynamic placeholders ([◆])

Phase 4: Section Library

  • Define section types and their props schema
  • Hero section renderer (SSR + React)
  • Content section renderer (for post body)
  • Feature grid renderer (SSR + React)
  • Related items section (for related posts/CPT items)
  • CTA banner renderer (SSR + React)
  • Add more sections as needed

Phase 5: Polish & Launch

  • Caching for SSR (cache rendered HTML for 1 hour)
  • Unsaved changes warning (ESC key, browser back, menu click)
  • Test SEO with Google Search Console
  • Test with bot simulators (curl with bot user agents)
  • Test dynamic placeholder rendering
  • Documentation for merchants
  • Documentation for developers (extending sections, custom placeholders)

Database Schema

Structural Pages (page CPT)

wp_posts:
├─ id: 42
├─ post_type: 'page'
├─ post_name: 'about'
├─ post_title: 'About Us'
├─ post_status: 'publish'
└─ post_modified: '2026-01-11 20:58:00'

wp_postmeta:
├─ post_id: 42, meta_key: '_wn_page_structure'
  value: {"type": "page", "sections": [...]}
├─ post_id: 42, meta_key: '_yoast_wpseo_title'
  value: "About Us | My Store"
├─ post_id: 42, meta_key: '_yoast_wpseo_metadesc'
  value: "Learn about our company..."
├─ post_id: 42, meta_key: '_yoast_wpseo_canonical'
  value: "https://mystore.com/about"
└─ post_id: 42, meta_key: '_yoast_wpseo_opengraph-image'
   value: "https://mystore.com/og-image.jpg"

CPT Templates

wp_options:
├─ option_name: 'wn_template_post'
  option_value: {"type": "template", "cpt": "post", "sections": [...]}
├─ option_name: 'wn_template_portfolio'
  option_value: {"type": "template", "cpt": "portfolio", "sections": [...]}
└─ option_name: 'wn_template_custom_cpt'
   option_value: {"type": "template", "cpt": "custom_cpt", "sections": [...]}

Target Users & Experience

Tech-Savvy Users (Developers/Agencies)

Use Case: Building custom stores with WooNooW
Tools: WooNooW CSS variables, class documentation, section schema
Value: Framework for rapid SPA development, design system enforcement

Non-Tech Merchants

Use Case: Quick store setup with predefined pages and templates
Tools: WooNooW Page Editor (constrained choices, no freestyle design)
Value: Professional pages in minutes, consistent styling across all CPTs

Both

Result: Consistent visual identity across structural pages + dynamic CPT items


Decision Summary

Aspect Decision
Editor Type Single unified editor for pages + CPT templates
Structural Pages Native page CPT, stored in postmeta
CPT Templates Stored in wp_options (one per CPT)
Field Types Static (hardcoded) or Dynamic (with placeholders)
Available Placeholders post_title, post_content, featured_image, author, date, related_posts, etc.
Page Builder Integration None (custom WooNooW editor only)
Admin Location Appearance > Pages (single submenu)
Editor Context Part of admin SPA (not full-screen)
Sidebar Behavior Collapsible (expanded or icon-only)
Create Flow Modal asks: Structural page or CPT template?
SPA Routing Matches WordPress permalink structure
Permalink Matching /blog/slug/store/blog/slug
SEO Yoast/Rank Math for pages, templates don't interfere
Bot Handling No redirect, serve pre-rendered HTML with placeholders filled
Human Handling HTTP 302 redirect to SPA with matching permalink structure
SPA Location Single centralized page (e.g., /store/)
Performance Cache SSR HTML (1 hour TTL)
UI Pattern 3-column layout (pages list, editor, settings+preview)
Template Types Hero, Content, Feature Grid, Related Items, CTA Banner

What's NOT Changing

Existing shop/product/cart/checkout functionality
Existing SPA architecture (single page, centralized)
Existing template override logic (extended, not replaced)
Existing API patterns (new endpoints follow conventions)
Existing CSS variable system (reused for all sections)
SEO plugin compatibility (Yoast, Rank Math continue to work)
Admin menu structure (Pages added to existing Appearance menu)
Admin sidebar (enhanced with collapse functionality, not replaced)


Key Architectural Benefits

One editor, two modes - Merchants don't learn separate systems
Clean separation - Static pages vs dynamic templates clear
URL consistency - SPA routes match WordPress permalinks
SEO perfect - Bots see full content, humans get SPA experience
Scalable - Works for current CPTs and future custom CPTs
Flexible placeholders - Can add any post field or custom meta
No page builder lock-in - Custom sections, not Gutenberg/Elementor
Maintains design consistency - Predefined sections, no freestyle design


Next Discussion Topics

  • Section library detailed specs (Hero, Content, Features, Related, CTA)
  • Placeholder source definitions and extensibility
  • Section schema validation
  • Caching strategy details (Redis vs file-based vs transient)
  • Migration path for existing pages/posts
  • Developer documentation (extending sections, custom placeholders)
  • Component structure and file organization (React + PHP)