# WooNooW Page Editor (Page Editor feature) — Comprehensive Trace & Consistency Audit ## Summary WooNooW implements “Page Editor” as a **section-based layout system** with **two render paths**: (1) an interactive React canvas in **admin-spa** and (2) a front-end storefront render via **customer-spa DynamicPage section components** (and a bot-safe SSR fallback for SEO). The admin editor stores page/template section JSON in WordPress (**page**: post meta `_wn_page_structure`; **CPT templates**: `wn_template_{cpt}` option). The key engineering goal—**WYSIWYG consistency** between what the editor shows and what the frontend renders—is currently only partially achieved due to mismatched prop flattening/dynamic placeholder handling and incomplete SSR coverage. --- ## Architecture ### Primary pattern **JSON schema + adapter rendering**: - Admin editor maintains a `sections[]` JSON tree (Zustand store) - The canvas maps `section.type` → customer-spa React section components - Frontend render uses those same React components at runtime - Bot render uses PHP `PageSSR` + `PlaceholderRenderer` to turn the same JSON into HTML ### Major subsystems 1. **Admin editor UI (admin-spa)** - `Appearance > Pages` editor (sidebar + canvas + inspector) - CanvasRenderer adapts section JSON into props for React section components 2. **WordPress REST API (includes/Api/PagesController.php)** - Fetch/save page structures + CPT templates - Preview endpoints for editor iframe-like previews (even though the v4 brief is canvas-only) 3. **Front-end storefront renderer (customer-spa)** - DynamicPage sections like `HeroSection`, `FeatureGridSection`, etc. 4. **Bot/server rendering (includes/Frontend/PageSSR.php + PlaceholderRenderer.php + TemplateOverride.php)** - Detect bots → serve minimal SSR HTML - Resolve dynamic placeholders to post data ### Technology stack - Backend: **PHP 8.2+**, WordPress REST API, WooCommerce hooks, Yoast/RankMath compatibility, transient caching - Frontend: **React 18 + TypeScript**, Zustand, React Query, dnd-kit, UI primitives - Data: JSON structures persisted into WordPress post meta / options ### Execution start (runtime entry points) - **Admin editor**: `admin-spa/src/routes/Appearance/Pages/index.tsx` - **Front-end page display**: mediated by `TemplateOverride.php` (SPA wrapper + redirect + bot SSR) - **Bot SSR path**: - `TemplateOverride::maybe_serve_ssr_for_bots()` → `TemplateOverride::serve_ssr_content()` → `PageSSR::render()` → `PageSSR::render_section()` → section-specific render methods --- ## Directory Structure (meaningful subset) ```text project-root/ ├── admin-spa/ │ └── src/routes/Appearance/Pages/ │ ├── index.tsx — page editor orchestrator (data fetching + save) │ ├── components/ │ │ ├── CanvasRenderer.tsx — canvas composition + dnd + mapping to section renderers │ │ ├── CanvasSection.tsx — selection/hover wrapper (partially inspected) │ │ ├── InspectorPanel.tsx — inspector UI (fields/styles/background) │ │ ├── InspectorField.tsx / InspectorRepeater.tsx — field editors + repeaters │ │ └── section-renderers/ — older editor-specific renderers (hero etc.) │ └── store/usePageEditorStore.ts — Zustand store + persistence payload ├── customer-spa/ │ └── src/pages/DynamicPage/sections/ │ ├── HeroSection.tsx │ ├── FeatureGridSection.tsx │ └── (other section components) ├── includes/ │ ├── Api/PagesController.php — REST API for pages/templates/preview │ ├── Frontend/PageSSR.php — bot SSR HTML generator from section JSON │ ├── Frontend/PlaceholderRenderer.php — dynamic source resolution from post data │ └── Frontend/TemplateOverride.php — SPA routing + bot SSR integration └── templates/ └── spa-full-page.php / spa-wrapper.php — SPA mount templates ``` --- ## Key Abstractions ### 1) Page Editor JSON model (Section + SectionProp) - **Where**: - `admin-spa/src/routes/Appearance/Pages/store/usePageEditorStore.ts` - **Responsibility**: Defines the shape of editable layout: - `Section` includes `type`, `layoutVariant`, `colorScheme`, `styles`, `elementStyles`, and `props` - `props` is a map of `{ [fieldName]: { type: 'static'|'dynamic', value?, source? } }` - **Lifecycle**: - Created client-side when user adds a section - Updated live as inspector changes - Persisted on save via REST - **Used by**: - CanvasRenderer adapter to customer-spa components - PHP SSR and placeholder renderer ### 2) Zustand store: `usePageEditorStore` - **File**: `admin-spa/src/routes/Appearance/Pages/store/usePageEditorStore.ts` - **Responsibility**: - Holds `currentPage`, `sections`, selection/hover/device mode, unsaved-change flag - Implements add/delete/duplicate/reorder/update actions - Implements SPA landing page API calls - Implements `savePage()` that POSTs `sections` to REST endpoints - **Interface (selected)**: - `addSection(type, index?)` - `updateSectionProp(sectionId, propName, value: SectionProp)` - `updateSectionStyles(sectionId, styles)` - `updateElementStyles(sectionId, fieldName, styles)` - `savePage()` - **Important behavior**: - `setSections()` marks `hasUnsavedChanges=true` (and save clears it) - **Used by**: - `admin-spa/src/routes/Appearance/Pages/index.tsx` ### 3) Canvas renderer: `CanvasRenderer` - **File**: `admin-spa/src/routes/Appearance/Pages/components/CanvasRenderer.tsx` - **Responsibility**: - Drag-and-drop reorder of sections (dnd-kit) - Maps `section.type` → customer-spa section components - Wraps customer component with `withSectionWrapper()` to adapt JSON props - **Key behavior**: - `flattenSectionProps()` transforms: - static `{type,value}` → value - dynamic `{type,source}` → string marker like `"[source]"` (NOT actual placeholder resolution) - **Used by**: - `admin-spa/src/routes/Appearance/Pages/index.tsx` - **Consistency risk**: - Editor canvas does **not** resolve dynamic placeholders to actual post data. It passes markers, while frontend runtime likely resolves using fetched post data. ### 4) Inspector UI: `InspectorPanel` (and related field editors) - **File**: `admin-spa/src/routes/Appearance/Pages/components/InspectorPanel.tsx` - **Responsibility**: - When a section is selected: - shows “Content” tab (fields) - shows “Design” tab (section background + spacing + element-level style overrides) - special repeaters for `feature-grid`, `bento-category-grid`, `shoppable-image` - When no section selected: - page/template info, SPA landing toggle, container width radio, delete actions - **Key behavior**: - Uses `SECTION_FIELDS` definitions per `section.type` - For dynamic field support, `InspectorPanel` passes `supportsDynamic={field.dynamic && isTemplate}` to `InspectorField` - **Consistency risk**: - Styles and elementStyles keys must match what customer-spa section components read; mismatches cause divergence between editor and SSR/render output. ### 5) Frontend render components: customer-spa DynamicPage sections - **Files**: - `customer-spa/src/pages/DynamicPage/sections/HeroSection.tsx` - `customer-spa/src/pages/DynamicPage/sections/FeatureGridSection.tsx` - **Responsibility**: - Render real UI using props: `layout`, `colorScheme`, `styles`, `elementStyles` - Use helper `getSectionBackground(styles)` (in `HeroSection`) - **Key behavior**: - The components assume props are already in “resolved” form (e.g., `title` is plain string, `image` is a URL, `items` are concrete objects). - **Consistency implication**: - Admin editor passes `flattenSectionProps()` values; dynamic placeholders are not resolved → UI may show placeholder markers rather than real values. ### 6) REST API: `includes/Api/PagesController.php` - **File**: `includes/Api/PagesController.php` - **Responsibility**: - Provide structures to admin editor and accept saves - Store page structures in post meta (`_wn_page_structure`) and templates in wp_options (`wn_template_{cpt}`) - **Key endpoints**: - `GET /woonoow/v1/pages` (list pages + CPT templates) - `GET /woonoow/v1/pages/{slug}` (page structure with SEO + container width) - `POST /woonoow/v1/pages/{slug}` (save page sections) - `GET /woonoow/v1/templates/{cpt}` - `POST /woonoow/v1/templates/{cpt}` - `POST /preview/page/{slug}` and `/preview/template/{cpt}` - `GET /woonoow/v1/content/{type}/{slug}` (return post data + template + rendered sections) - **Important behavior**: - `get_content_with_template()` calls `PageSSR::resolve_props()` to resolve section props for the frontend render payload. - Contains a special pre-resolution for `related_posts` dynamic arrays. ### 7) Bot SSR: `PageSSR` + `PlaceholderRenderer` - **Files**: - `includes/Frontend/PageSSR.php` - `includes/Frontend/PlaceholderRenderer.php` - wired by `includes/Frontend/TemplateOverride.php` - **Responsibility**: - Convert stored JSON + post data into static HTML for bots - Replace dynamic placeholders at SSR time - **Key limitations (observed)**: - `PageSSR` includes explicit renderers for hero/content/image_text/feature_grid/cta/contact_form; other section types may fall back to `render_generic()` which only prints string props. - `render_content()` uses `apply_filters('the_content')`, meaning SSR content is likely different from React’s rendering if React expects different sanitization or structured HTML. ### 8) Routing + SSR integration: `TemplateOverride` - **File**: `includes/Frontend/TemplateOverride.php` - **Responsibility**: - Redirect WooCommerce pages to SPA routes - Serve SPA template (`spa-full-page.php`) for full SPA mode - Detect bots and serve SSR via `maybe_serve_ssr_for_bots()` - **Key invariant**: - SSR runs only when `is_bot()` matches known patterns and the target has `_wn_page_structure` or `wn_template_{post_type}` sections. --- ## Data Flow (concrete tracing) ### A) Editor → persisted structure 1. User opens editor: `admin-spa/src/routes/Appearance/Pages/index.tsx` 2. Editor fetches list: `api.get('/pages')` (REST) → `PagesController::get_pages()` 3. User selects a page/template: - `api.get('/pages/{slug}')` or `api.get('/templates/{cpt}')` 4. Editor loads `pageData.structure.sections` and sets Zustand `sections` 5. User edits: - inspector modifies `sections[n].props[fieldName]` and `styles/elementStyles` 6. On Save: - `saveMutation` POSTs `{ sections }` to `/pages/{slug}` or `/templates/{cpt}` - `PagesController::save_page()` saves into `_wn_page_structure` - `PagesController::save_template()` saves into `wn_template_{cpt}` ### B) Editor canvas: JSON → React component props 1. `CanvasRenderer` maps `section.type` to a customer-spa renderer 2. `flattenSectionProps()` converts each prop: - static: passes `value` directly - dynamic: passes placeholder marker string like `[source]` 3. The customer-spa component renders based on expected prop types (string URLs, arrays, etc.) **This is the main “WYSIWYG gap”**: dynamic placeholders in the editor are not resolved with a selected post context. ### C) Frontend runtime render (human + bot) — key split 1. `TemplateOverride` decides whether to serve SPA or SSR. 2. **Humans** (non-bots): - SPA template renders React app; dynamic page fetch uses `PagesController::get_content_with_template()` (not fully traced here, but confirmed by the endpoint implementation). 3. **Bots**: - `maybe_serve_ssr_for_bots()` calls `serve_ssr_content()` 4. `PagesController::get_content_with_template()`: - fetches post data via `PlaceholderRenderer::build_post_data()` - resolves each section’s props via `PageSSR::resolve_props()` (with special pre-resolution for `related_posts`) - returns `rendered.sections` payload ### D) Bot SSR: JSON → HTML 1. `TemplateOverride::serve_ssr_content()`: - gets `_wn_page_structure` or `wn_template_{cpt}` - calls `PageSSR::render($sections, $post_data)` 2. `PageSSR::render_section()`: - resolves props again with `resolve_props()` - calls `render_hero`, `render_feature_grid`, etc. 3. `PlaceholderRenderer::get_value()` converts sources to actual values. --- ## Non-Obvious Behaviors & Design Decisions (and what that means) ### 1) Editor currently does not truly “resolve dynamic placeholders” - Admin canvas uses `flattenSectionProps()` and converts dynamic props to markers like `"[source]"`. - Frontend runtime resolves placeholders using PHP `PageSSR::resolve_props()` inside `PagesController::get_content_with_template()`. **Why it matters**: a template section that uses dynamic sources (post title, featured image, related posts) will show “`[post_title]`”-style strings in the editor canvas, but will show real values on the storefront. ### 2) Two parallel “renderers” exist: customer-spa React vs PageSSR PHP - React sections are full fidelity for humans. - PHP PageSSR covers only some section types explicitly. **Why it matters**: - Even for bots, different section types may fallback to generic HTML and lose styling/content structure. - That produces another “editor vs rendered” mismatch, because the editor is React-rendered (not SSR). ### 3) Section schema keys are implicitly coupled to both sides - Example: customer `HeroSection` expects `styles.paddingTop/paddingBottom` and `styles.contentWidth`, elementStyles per element name. - PHP `PageSSR::render_hero()` expects `section_styles` keys like `backgroundType`, `paddingTop`, etc. **Risk**: any rename in editor/styling keys breaks one render path silently. ### 4) Related-posts handling is special-cased - `get_content_with_template()` does a manual resolution when it detects dynamic prop `related_posts`. - This suggests the generic `PageSSR::resolve_props()` doesn’t produce the required array shape without precomputation. **Implication**: other complex dynamic sources might also need special handling but are not generalized. ### 5) Duplicate section deep clone via JSON stringify - `duplicateSection` uses `JSON.parse(JSON.stringify(section))`. **Implication**: any non-serializable structures in the section (e.g. functions) would be lost; current SectionProp model is JSON-safe, but this is a design constraint. --- ## Section-by-Section Trace (what’s implemented + the gap) Below are the sections we directly observed in code paths: ### Hero section - **Editor**: - `InspectorPanel` defines `hero` fields: `title`, `subtitle`, `image`, `cta_text`, `cta_url` (title/subtitle/image can be `dynamic: true`). - `DEFAULT_SECTION_PROPS.hero` in Zustand initializes static values. - **Canvas**: - `CanvasRenderer` maps `type='hero'` → `customer-spa` `HeroSection` - `withSectionWrapper` flattens section.props: - dynamic title becomes marker string - **Frontend runtime**: - `customer-spa HeroSection` expects resolved `title/subtitle/image` strings - **Bot SSR**: - `PageSSR::render_hero()` renders title/subtitle/image/cta with inline style support and background/overlay/spacing. **Gap**: dynamic placeholders aren’t resolved in the editor canvas, so WYSIWYG breaks for Hero dynamic fields. ### Feature Grid section - **Editor**: - `InspectorPanel` includes: - `SECTION_FIELDS` for `feature-grid`: only `heading` (static) - repeaters for `features` inside “Feature Grid Repeater” - Zustand defaults include `features` as `{type:'static', value:''}` (note: this is inconsistent with repeater expecting arrays—possible defect). - **Canvas**: - `CanvasRenderer` maps `feature-grid` → `customer-spa FeatureGridSection` - flattenSectionProps() will transform: - static `features` → `''` or array depending on stored shape - dynamic `features` → `"[source]"` marker (but the editor repeater may prevent dynamic) - **Frontend runtime**: - `customer-spa FeatureGridSection` uses `items = items.length>0 ? items : features` - It supports “post-card” mode by detecting `item.url`. - **Bot SSR**: - `PageSSR::render_feature_grid()` renders `items = $props['items'] ?? []`; it does not include a `features` fallback. - Also it uses `get_icon_svg()` only for known Lucide icon names; unknown icons degrade. **Gaps / defects**: 1. **Schema inconsistency** between editor and SSR: - React component uses `items` and `features` props - PHP SSR renders only `items` 2. Zustand default for `feature-grid.features` is `''` not `[]`, which likely makes the repeater misbehave and/or yields empty rendering on save. 3. Editor likely supports dynamic features only via special logic, but the inspector repeater has logic for `featuresProp.type === 'dynamic'`—we did not verify the dynamic editing path end-to-end. ### Image + Text section - **Editor**: - `InspectorPanel` defines `image-text` fields: `title`, `text` (textarea), `image`, `cta_text`, `cta_url` - **Canvas**: - maps `image-text` → `customer-spa ImageTextSection` - **Bot SSR**: - `PageSSR` has `render_image_text()` but it relies on helper `render_universal_row()` and expects specific prop shapes (string or `props[...]` objects). **Gap**: - If editor stores nested `{type,value}` shapes, React HOC wrapper flattens them to plain strings; SSR expects resolved strings after `resolve_props()`. That’s fine for bots because SSR uses resolve_props. But editor WYSIWYG may still be placeholder-markers if any fields are dynamic. ### CTA Banner section - Editor: `cta-banner` fields - Canvas: mapped to customer-spa `CTABannerSection` - Bot SSR: `render_cta_banner()` exists **Gap**: - Same dynamic placeholder mismatch in editor (markers vs resolved values). ### Contact Form section - Editor: `contact-form` includes `webhook_url` and `redirect_url` - Canvas: mapped to customer-spa `ContactFormSection` - Bot SSR: renders a `