- 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
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
pageCPT 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_poststable (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_optionstable - 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 titlepost_excerpt- Post excerptpost_content- Post body contentpost_featured_image- Featured image URLpost_author- Author namepost_date- Publication datepost_categories- Category listpost_tags- Tag listrelated_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/slugResult: ✅ 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
- Click WooNooW logo → Dashboard
- Click any menu item in sidebar → Navigate to that section
- Press ESC key → Back (with unsaved changes warning)
- Click browser back button → Back (with unsaved changes warning)
- 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
PageSSRclass to render sections as HTML - Build
PlaceholderRendererclass 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
DynamicPageRenderercomponent - Test bot crawling vs human browsing
Phase 2: Admin SPA Integration
- Add "Pages" submenu to Appearance menu
- Create
/admin/appearance/pagesroute - Build
PageEditorcomponent 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)