Skip to main content

Documentation

No results found.
Features

Page Editor Tour

WebProCMS ships with a visual page editor that lets editors and marketers build full pages without writing code. Pages are assembled from pre-designed rows; every text string, image, button, color, spacing value, and HTML class is editable...

WebProCMS ships with a visual page editor that lets editors and marketers build full pages without writing code. Pages are assembled from pre-designed rows; every text string, image, button, color, spacing value, and HTML class is editable through a sidebar that opens beside a live preview iframe. Edits feel instant — typing in a field updates the preview synchronously, and only saves, history, and AI calls round-trip to the server.


The problem

Most CMS editors force a tradeoff: drag-and-drop builders give marketers fast iteration but hide HTML and CSS behind opaque settings panels, leaving designers no way to express precise layout intent. Code-first systems give designers full control but block marketers from making routine copy and image edits without a developer.

The page editor in WebProCMS is built so the same surface serves both audiences: every visible attribute is exposed as an editable field, and every field is grouped into one of three modes — Content, Design, or Advanced — so editors who only want to swap a headline never see a class input, and designers who want to tweak padding never have to scroll past it.

The fix

The editor opens with a list of rows on the left and a live preview iframe on the right. Clicking a row drills into it; the row's elements appear as cards in the sidebar. Each card has up to three icons depending on what's editable on that element:

  • Content (document icon) — text, rich text, images, toggles, repeaters
  • Design (paintbrush icon) — classes, colors, padding, margin, alignment
  • Advanced (code-bracket icon) — custom element IDs and HTML attributes

Switching tabs only shows the fields relevant to that mode. The other tabs are loaded but their inputs aren't mounted, so heavy components like the rich-text editor stay out of the DOM until needed.

The preview iframe is the live page rendered with the current draft, refreshed on every edit. There is no separate "publish" view to toggle to — what you see while editing is the rendered output.

Editing modes

Mode What it edits Examples
Content Words, pictures, structured data Heading text, body paragraph, image upload, "show button" toggle, pricing-card list items
Design Visual presentation Tailwind classes, background color, text color, padding/margin tokens, alignment
Advanced Markup hooks id="get-started" for jump links, data-* and aria-* attributes for analytics or accessibility tooling

The icon strip on each card hides modes that don't apply. A pure-classes wrapper element shows only the paintbrush; a heading shows all three (text, classes, ID).

Field types

Every input is type-aware. The editor picks the appropriate UI based on the field's declared type:

Type UI Notes
text Single-line input Default for short strings
headingtext Single-line input + h1–h4 dropdown Used by <x-dl.heading> so editors can change semantic level
richtext Tiptap editor Bold, italic, lists, links, headings — saves as HTML
toggle Switch Show/hide sections, show secondary CTA, mark a card as featured
image Pick-from-library button + thumbnail Routes through the media library; click to change, hover-X to remove
classes Monospace textarea + Tailwind autocomplete Suggests theme tokens (primary, tone-700, py-section-hero, etc.)
repeater Add/edit/remove items list Pricing cards, FAQ entries, gallery images, testimonials
bg_color / text_color_classes Strategy browser + swatch popover Applies brand-aware foreground/surface combinations
padding / margin Token chips Snap to spacing tokens (py-section, py-section-hero, etc.)

Rich-text fields use Tiptap — content is HTML, not Markdown. Image fields don't upload directly: every image flows through the media library so alt text, dimensions, and reusability are managed in one place.

Drill-in: rows, items, and fields

The editor models a page as three nested levels:

  1. Rows — full-width sections (hero, features grid, footer, etc.). The row list is the editor's home screen.
  2. Items — the individual elements inside a row. A pricing-cards row has a heading item, a subheadline item, a buttons item, and a repeater item for the cards.
  3. Fields — the editable attributes of an item. A heading item has the heading text, the heading tag (h1/h2/h3), and the heading classes.

Clicking a row opens its items in the sidebar. Clicking an item card expands it to show the active mode's fields. Clicking a different mode-icon swaps the visible fields without leaving the item.

Manipulating items

Beyond editing field values, every item card supports structural operations:

  • Add an item above or below or inside (the + buttons on every gap)
  • Duplicate — clones an item with a fresh prefix so its values don't collide with the original
  • Move — click the Move icon to pick up; click any item to drop (see "Reordering rows and items" below)
  • Cut / paste — Cmd/Ctrl+X cuts, Cmd/Ctrl+V pastes; cross-row paste also migrates the item's saved overrides into the target row's slug. See "Multi-select and keyboard shortcuts" below for selecting more than one item at a time.
  • Delete
  • Wrap with loop — convert a static element into a repeater
  • Unwrap from loop — collapse a repeater back into a single static instance

Items can be nested inside layout containers (Grid, Block, Panel, Container, Div) — the layout primitives are themselves items, with their own classes editable through the same paintbrush UI.

Repeaters: per-item styling and template code

Repeater rows (feature grids, pricing tiers, testimonial sets…) render every card through one shared template with a list of data items, so cards stay perfectly uniform. Two tools loosen that when you need it:

  • Style one card only (paintbrush on the item row). Each item in a repeater's list has a paintbrush button that opens a per-item style panel: an Item Classes input (e.g. ring-2 ring-primary to highlight the "Most Popular" pricing tier) and an Item Attributes name/value list. These apply to that card only, on top of the shared card classes, and they travel with the item when you reorder or duplicate it. Works for static items; cards produced by a dynamic data source aren't individually styleable (they're query results).
  • Edit the item template (admins). The item's Advanced tab has an Item Template section with the raw Blade markup every card renders through — add a second button to all cards, move the icon below the title, and so on. Edits are syntax-checked before they apply, structural markers are rejected, and the change is undoable like any other edit. Also available on gallery, slider, and accordion bodies. If you'd rather have full per-card structural freedom, convert the repeater to a grid instead (each card becomes independent structure).

Reordering rows and items

Every row card and every item card has a Move icon (↕) and a Clipboard icon (📋) in its action bar.

  • Move is a one-click pickup. Click the icon to "pick up" the card; click any other card in the list to drop it there. A primary-colored line previews the drop position as the cursor moves. Esc or the Cancel link in the bottom banner aborts. Clicking the same card again cancels.
  • Clipboard opens a small Copy / Cut / Paste menu (⌘C / ⌘X / ⌘V — the same actions are wired to keyboard globally). Paste lands at the end of the row list (for rows) or inside the current container (for items).

Pickup mode handles every reorder distance — one step, ten steps, or jumping to the top — with the same two clicks. There is no drag-to-reorder.

Multi-select and keyboard shortcuts

Both rows in the row list and items inside a row use the same selection model:

  • Click — select one
  • Cmd/Ctrl+click — toggle this one in or out of the current selection
  • Shift+click — select the range from the last-clicked entry through this one

With one or more rows or items selected, standard keyboard shortcuts apply:

  • Cmd/Ctrl+C — copy the selection to the clipboard
  • Cmd/Ctrl+V — paste (rows append to the end of the page; items paste into the currently open row, after the active drill-in target)
  • Cmd/Ctrl+X — cut (copy then remove)

The clipboard persists in localStorage, so a row or set of rows copied on one page can be pasted into any other page in the same browser. Item paste migrates the source items' saved overrides into the target row, so cross-row and cross-page paste preserves the actual content rather than just the structure.

Selection is scoped: rows can only be selected from the row list, and items can only be selected after drilling into a row, so the two never overlap.

The library: rows, blank starters, and shared rows

The "Add row" button opens a drawer with three tabs:

  • Library — pre-designed rows from the design library, filterable by category (hero, pricing, footer, etc.). Each has a live preview thumbnail. See design-library.md for the full catalogue.
  • Blank — start-from-scratch primitives (section, block, div, panel, container) with optional starter items (heading, subheadline, button, image)
  • Shared — rows saved for cross-page reuse. See shared-rows.md.

Inserting a row injects the template into the page blade with a fresh slug, eager-wraps it in editor markers so item-mode is active immediately, and refreshes the preview.

Replace template (browse mode)

Any row already on the page can be swapped for a different template from the same category — or a different category — without losing the section-level styling. The "Browse alternates" button on a row card opens an inline carousel: arrow keys cycle through every row in the current category, the preview updates live, and a confirm action commits the swap.

The swap preserves cross-cutting overrides (section_* keys, toggle_sticky) so background color, padding, container width, and custom IDs carry over to the new template. Component-specific overrides (the old row's headline text, the old row's button label) are dropped — the new template's defaults take their place. This is intentional: the user is asking for a different layout, not a different copy of the same content. Implementation lives in EditorLibraryActions::applyBrowseRow.

Undo / redo

Every mutation pushes a snapshot onto a per-page history stack capped at 50 entries. The undo and redo buttons in the header toolbar walk the stack in either direction. Snapshots include the full row list and all unsaved drafts, so undoing a paste rolls back both the blade structure and the migrated overrides in one step.

History is per-file and lives entirely client-side, so undoing on one page never affects another. The stack is an in-memory ring buffer that resets on every page load (or a full state replacement like a discard/merged save) — it does not persist across a reload. Implementation lives in the Alpine store's _history ring buffer in editor-state.js.

For longer-term recovery (older than the 50-entry stack, or after a save), see page revisions in backups-and-revisions.md — every save snapshots a versioned copy of the page's overrides that can be restored from the editor's discard menu.

Reset, discard, and refresh

The discard menu in the header bundles five actions, each scoped to a single row or the whole page:

Action Scope What it does
Discard unsaved changes Page or row Drops session drafts, reverts to last saved state. Saved overrides keep applying.
Discard content overrides Page or row Deletes saved overrides for content fields (text, images, toggles, repeaters). Design styling stays.
Discard design customizations Page or row Deletes saved overrides for classes, colors, padding, custom IDs, and section settings. Content stays.
Refresh from design library Page or row Re-pulls the row's blade from its source template, re-bakes saved overrides on top. Use this after a design library template has been updated upstream.
Restore from earlier version Page Rolls the page's overrides back to a previously-saved revision (auto-snapshotted on every save)
Restore page from backup Page Per-page restore from a sitewide snapshot — only this page's overrides + blade file are touched

Discard actions are destructive (no undo) and confirm via a modal before running. Refresh is non-destructive — the original template is replaced but every saved override is re-baked on top, so the customisations survive the structural change. Implementation lives in EditorDiscardActions.

Save

Saving writes two things: the row blade source files to disk (so Tailwind's class scanner sees user-typed classes at the next build), and the field values as ContentOverride rows in the database. The blade files contain the structural skeleton plus baked default classes; the override rows hold the user's customisations.

A save flushes any pending drafts in one batched request, persists the overrides, writes the blade, snapshots a page revision, and — locally or in production — fires the asset rebuild job so new Tailwind classes show up on the live site within a second.

Two people editing the same page

Two editors can work on the same page at once without losing each other's work. When you save a page someone else has changed since you opened it, the editor three-way merges the two sessions automatically: edits to different fields or different sections both land, and a section one of you added or removed is preserved. Only where you both changed the same field, or one edited a section the other deleted, do you get a small "Someone else saved this page" chooser — pick which version to keep for each conflict and save again. Nothing is written until you resolve those, so a conflict never silently overwrites either side. In the rare case a change can't be merged at all, an amber banner above the row list offers to reload the latest version or keep yours. Implementation notes live in editor-architecture.md under "Concurrent saves".

What lives where

Path Purpose
resources/views/pages/dashboard/pages/⚡editor.blade.php Page editor shell, row list, modal hosts, save / undo / discard buttons.
app/Concerns/EditorContentActions.php Drill-in (openContentEditor), cross-row paste, clipboard, field reset.
app/Concerns/EditorMediaActions.php Media picker callbacks, image replace / remove (each takes $rowIndex).
app/Concerns/EditorAiActions.php AI image generation and single-field "Translate this" (each takes $rowIndex).
app/Concerns/EditorRowActions.php Row reorder, hide, rename, remove, paste rows from another page.
app/Concerns/EditorLibraryActions.php Library drawer, insert row, replace template (browse mode), blank-row starter, push classes back to library.
app/Concerns/EditorDiscardActions.php Discard / refresh / version restore actions.
resources/js/editor/editor-state.js Alpine store holding live field values; mutations stay client-side, server flushes are debounced. Also owns client-side undo/redo (the _history ring buffer, 50-entry cap) — the server-side EditorHistoryActions trait was deleted when undo/redo moved fully client-side.

For deeper architectural notes (the Alpine store + Livewire shell split, debounce timing, event flow), see editor-architecture.md.