05 — Page builder#
The editor is the product. Everything else supports it. One editor edits pages, posts, headers, footers, mega-menu panels, global blocks and templates — the chrome around it changes, the editing model never does.
5.1 Screen layout#
┌───────────────────────────────────────────────────────────────────────────────┐
│ ← Pages About us [Draft saved 12:04] [Desktop|Tablet|Mobile] │
│ [Preview] [Save draft] [Publish ▾] │
├──────────────┬──────────────────────────────────────────────┬────────────────┤
│ OUTLINE │ LIVE PREVIEW (iframe) │ SETTINGS │
│ │ │ │
│ ▾ Hero │ ┌────────────────────────────────────────┐ │ Heading │
│ ▾ Col 1 │ │ Welcome │ │ ────────── │
│ Heading │ │ Short intro │ │ ▾ Content │
│ Text │ │ [Contact us] │ │ Text [___] │
│ Button │ │ │ │ Level [H1 ▾] │
│ ▾ Col 2 │ └────────────────────────────────────────┘ │ ▸ Style │
│ Image │ │ ▸ Advanced │
│ ▸ Features │ [ + Add section ] │ │
│ ▸ CTA │ │ [Duplicate] │
│ │ │ [Delete] │
│ [+ Section] │ │ │
└──────────────┴──────────────────────────────────────────────┴────────────────┘
| Region | Width | Owner | Behaviour |
|---|---|---|---|
| Top bar | full | PageEditor |
title, autosave status, device switch, Preview, Save draft, Publish (with a dropdown: Publish, Schedule, Submit for review) |
| Outline | 260 px, collapsible | OutlineTree |
nested list of sections, columns, blocks; drag to reorder; click to select; keyboard navigable |
| Preview | fills | iframe | renders the draft tree through the real public renderer; click an element to select it |
| Settings | 320 px | SettingsPanel |
generated from schema.php; Content open, Style and Advanced collapsed |
On screens narrower than 1280 px the outline collapses to an icon rail. Below 1024 px the editor switches to a stacked mode: preview on top, settings in a bottom sheet. The editor is usable on a tablet; it does not claim to be usable on a phone.
5.2 The non-negotiable defaults#
- Content tab only.
StyleandAdvancedare collapsed on every selection, every time. They do not remember being open across selections — a beginner must never find themselves in a panel of CSS they did not ask for. - No blank page.
New pageopens a template picker.Add sectionopens a gallery. The only way to get an empty section isAdd section -> Blank, which is the last item. - No colour pickers in Content. Colour fields show theme token swatches. A free hex input
exists only in
Advanced. - No browser dialogs. Delete opens an in-app modal naming the thing; autosave reports through a quiet status text; errors are toasts.
5.3 Add Section gallery#
Eight ready-made section designs ship in Phase 1, each a templates row with kind = section:
| Category | What it contains |
|---|---|
| Hero | heading + text + button + image, two columns |
| Features | 3 columns of icon_list + heading + text |
| Pricing | 3 columns of heading + text + icon_list + button |
| FAQ | accordion, single column, container width |
| Testimonial | quote text + author, centred |
| CTA | heading + button on a primary background |
| Gallery | gallery block, 3 columns, lightbox on |
| Contact | form + address text + map image, two columns |
The gallery shows preview.png-style thumbnails, a search box and a category filter. Inserting a
section deep-copies the template tree and assigns new ids. User-saved sections appear in a
My sections tab, and global blocks in a Reusable tab (inserting from there creates a reference,
not a copy — the UI says so).
5.4 Selection, drag and drop, and the toolbar#
- Clicking in the preview selects the nearest block; clicking its padding selects the column;
clicking the section background selects the section. A breadcrumb in the settings panel
(
Hero > Column 1 > Heading) lets the user walk up. - The selected element shows a floating toolbar in the preview: move handle, duplicate, delete,
save as template(sections only), and+to insert a block after it. - SortableJS handles dragging in both the outline and the preview. Drop targets are columns; a block cannot be dropped outside a column. Dragging a section reorders at the top level only (sections never nest).
- Keyboard: arrow keys move the selection through the outline,
Enterfocuses the first field,Ctrl+Dduplicates,Deletedeletes (with the confirm modal),Ctrl+Z/Ctrl+Shift+Zundo and redo,Ctrl+Ssaves a draft,Ctrl+Shift+Ppublishes.
5.5 State, autosave, undo/redo#
The Livewire component holds the tree as one PHP array, mirrored in Alpine for instant local UI.
| Concern | Rule |
|---|---|
| Dirty tracking | any mutation marks the tree dirty and bumps an in-memory revision counter |
| Autosave | debounced 3 s after the last change, and at most once every 10 s; writes pages.draft_content only |
| Autosave failure | the status text turns to a retry state, the tree stays in memory, and a retry happens on the next change or every 15 s; the browser warns on unload while dirty |
| Undo stack | last 50 tree snapshots, client-side, in memory; cleared on publish |
| Redo | invalidated by any new mutation |
| Conflict | the editor holds draft_updated_at; if the stored value is newer (another tab or user), saving shows a modal: Keep mine / Load theirs / Open the other version in a new tab. Last write never silently wins |
| Leaving | navigating away while dirty asks in an in-app modal, not onbeforeunload alone |
Undo snapshots are whole trees. A page large enough for that to hurt (hundreds of blocks) is already a page the user should split; a soft warning appears past 150 blocks.
5.6 Preview#
- The preview iframe loads
/_kp/preview/{page}?token=..., which renders the draft tree with the realPageRenderer, the real header and footer, and the real theme CSS. There is no separate preview renderer, so "it looked different after publishing" cannot happen. - The token is a signed, short-lived URL tied to the user and page, so a draft is never readable by a visitor who guesses the id.
- Device switching changes the iframe width to 390 / 834 / fill and reloads nothing.
Previewin the top bar opens the same URL in a new tab, full width, with an editing bar.
5.7 Publishing#
Publish ▾ offers:
| Action | Requires | Result |
|---|---|---|
| Publish | pages.publish permission |
validate, snapshot a version, status published, published_at = now(), clear that page in the cache |
| Schedule | same | status scheduled, published_at in the future; the cron publishes it and clears the cache |
| Submit for review | pages.create only (Writer) |
status pending, notifies Editors; no public change |
| Unpublish | pages.publish |
status draft, page removed from the cache and from the sitemap, a 410 or 404 is served |
| Save as template | templates.manage |
copies the tree into templates with a generated thumbnail |
Publish is blocked, with the offending block named, when: a required field is empty, a referenced
media or form row is missing, the tree fails structure validation, or a non-Admin left content in an
html block. "Blocked" means a toast plus the outline marking the block in red — never a silent
failure and never a 500.
5.8 Version history#
Page -> History lists versions newest first: version number, time, author, label, and a short
change summary (3 blocks added, 1 removed, Hero settings changed), computed by diffing the two
trees structurally.
Previewopens that version read-only in the preview route.Restorecopies the old tree intodraft_contentand selects it in the editor; it does not publish. The user reviews, then publishes — which creates a new version. History is append-only.Compareshows a side-by-side of the two outlines with added/removed/changed markers. It is not a text diff; a text diff of JSON is useless to a non-technical owner.
5.9 Pages screen#
| Feature | Detail |
|---|---|
| List | title, path, status pill, author, last updated, template, actions |
| Search | title and path, case-insensitive, Bengali-safe |
| Filters | status, type, author, category (posts), date range |
| Sort | last updated (default), title, published date |
| Bulk | publish, unpublish, trash, restore, set category/tags (posts) |
| Create | New page -> template picker -> title -> opens the editor |
| Duplicate | deep copy with (copy) in the title and a free slug; always a draft |
| Trash | soft delete; a Trash tab lists them with Restore and Delete permanently |
| Permanent delete | confirm modal naming the page; removes versions, pivots and cache entries; keeps the audit log |
| Homepage | a Set as homepage action; the current homepage is marked in the list |
| Hierarchy | pages display nested by parent_id; moving a parent rewrites descendant paths and writes 301s |
The Blog screen is the same component with type = post, plus category/tag columns and the
scheduled tab.
5.10 Editing other things with the same editor#
| Target | Entry point | Context | Differences |
|---|---|---|---|
| Post | Blog -> post | page |
adds excerpt, categories, tags, featured image, author in a right-panel Post tab |
| Header | Design -> Header | part |
kind header; three zones (top bar, main, sticky); part settings tab; preview over any chosen page |
| Footer | Design -> Footer | part |
kind footer; column presets 1–6 |
| Mega panel | Menus -> item -> Mega menu | mega |
panel width/background settings; preview inside a simulated header |
| Global block | Design -> Saved sections | page |
single section; shows a usage count and warns before publishing |
| Template | Design -> Templates | page or section |
no publish; Save template instead |
In every case the centre is the same preview, the right panel is the same generated form, and the outline is the same tree. A user who learned to edit a page can edit the header.
5.11 Inline editing from the public site (Phase 4)#
When a user with edit rights views the public site, each section shows a small Edit affordance on
hover, and text blocks become directly editable in place.
- The toolbar is injected only for authenticated users with permission, and only when the page is rendered dynamically — the cached HTML never contains it. A logged-in editor bypasses the HTML cache, so visitors keep getting cached files.
- Clicking
Edit sectionopens the editor with that section selected. - Editing text in place writes to
draft_contentthrough the same autosave endpoint and shows the sameunpublished changesbanner, withPublishandDiscard.
5.12 Performance budget for the editor#
| Interaction | Budget | How |
|---|---|---|
| Select a block | < 100 ms | Alpine handles selection locally; the panel is pre-rendered per block type |
| Edit a text field | < 50 ms visible | local state; Livewire sync is debounced |
| Preview refresh | < 600 ms | targeted re-render of the changed section where possible, full reload otherwise |
| Autosave | < 400 ms | one UPDATE of a JSON column, no re-render |
| Insert a section | < 500 ms | template tree is fetched once and cached client-side |
| Open the editor | < 1.5 s | one page query, one version query, the cached block manifest |
A tree of 100 blocks is the benchmark size; the gallery and media picker paginate so neither grows with the library.
KodePress documentation · generated from the Markdown sources by
tools/build-docs-site.py · internal preview, not indexed.