DEV Community

Cover image for Build a Visual Page Builder in Laravel Without Leaving Blade
Dipak Sarkar
Dipak Sarkar

Posted on

Build a Visual Page Builder in Laravel Without Leaving Blade

If you've ever wanted a Shopify-style page editor inside your Laravel app — drag-and-drop sections, live preview, multi-theme support, all powered by plain Blade views — that's exactly what we built.

coderstm/laravel-page-builder is a JSON-driven page composition system for Laravel 11 and 12. It gives your users a visual editor while keeping your developer workflow exactly as it should be: Blade files, PHP classes, and zero magic.


The Problem

Most Laravel page builders fall into one of two traps:

  1. Too much magic — custom DSLs, opaque rendering pipelines, or tight coupling to a specific frontend stack.
  2. Too rigid — pre-baked HTML structures that fight your design system the moment you need to deviate.

We wanted something that felt native to Laravel: Blade views for rendering, JSON for storage, and a clean PHP architecture underneath.


How It Works

Sections and Blocks Are Just Blade Views

A section is a Blade file that starts with a @schema() directive. That's it.

@schema([
    'name'     => 'Hero',
    'settings' => [
        ['id' => 'title',    'type' => 'text',  'label' => 'Title',    'default' => 'Welcome'],
        ['id' => 'subtitle', 'type' => 'text',  'label' => 'Subtitle', 'default' => 'Build something great'],
        ['id' => 'bg_color', 'type' => 'color', 'label' => 'Background'],
    ],
])

<section {!! $section->editorAttributes() !!} style="background: {{ $section->settings->bg_color }}">
    <h1>{{ $section->settings->title }}</h1>
    <p>{{ $section->settings->subtitle }}</p>
</section>
Enter fullscreen mode Exit fullscreen mode

Drop this file in resources/views/sections/hero.blade.php and it's immediately available in the editor. No registration, no YAML config, no artisan generate commands — the registry scans it automatically.

Blocks follow the exact same pattern:

@schema([
    'name'     => 'Card',
    'settings' => [
        ['id' => 'title', 'type' => 'text', 'label' => 'Title', 'default' => 'Card Title'],
        ['id' => 'image', 'type' => 'image', 'label' => 'Image'],
    ],
])

<div {!! $block->editorAttributes() !!}>
    <img src="{{ $block->settings->image }}" alt="">
    <h3>{{ $block->settings->title }}</h3>
</div>
Enter fullscreen mode Exit fullscreen mode

Container sections accept blocks via @blocks($section):

<section {!! $section->editorAttributes() !!}>
    @blocks($section)
</section>
Enter fullscreen mode Exit fullscreen mode

Pages Are Stored as JSON

When an editor saves a page, it writes a JSON file to disk:

{
    "sections": {
        "hero": {
            "type": "hero",
            "settings": {
                "title": "Welcome to Our Store",
                "bg_color": "#1a1a2e"
            }
        },
        "features": {
            "type": "feature-grid",
            "settings": {},
            "blocks": {
                "card-1": { "type": "card", "settings": { "title": "Fast" } },
                "card-2": { "type": "card", "settings": { "title": "Reliable" } }
            },
            "order": ["card-1", "card-2"]
        }
    },
    "order": ["hero", "features"]
}
Enter fullscreen mode Exit fullscreen mode

No database tables for content. JSON on disk means:

  • Version-controllable page content
  • Fast reads with no query overhead
  • Easy to seed, import, or diff

The Rendering Pipeline

The architecture follows a strict five-layer dependency flow:

Schema → Registry → Components → Renderer → Services
Enter fullscreen mode Exit fullscreen mode
  • Schema — Immutable readonly value objects describing section/block structure
  • Registry — Singleton scanners that discover and cache schemas from Blade files
  • Components — Hydrated Section and Block runtime objects built from JSON + schema
  • Renderer — The single entry point for all HTML output; never bypassed
  • Services — High-level orchestrators (PageService, PageStorage, PageRenderer)

Dependencies flow downward only. A Renderer never calls a Service. A Schema never calls a Registry. This makes the codebase easy to trace and test.


The Visual Editor

The editor is a React SPA with an iframe live preview. Changes sync to the preview in real time — no page refresh, no manual save to see output.

What the editor gives you out of the box:

  • Drag-and-drop section and block reordering
  • Settings panel with 21+ field types: text, color, image, icon, rich text, select, toggle, range slider, and more
  • Inline text editing directly on the preview
  • Media library with pluggable storage backends (local, S3, Cloudflare R2, DigitalOcean Spaces)
  • SEO panel — title, description, keywords per page
  • Theme settings — global CSS variables (colors, fonts, spacing) editable in the sidebar

The editor is injected via a single route and activates only when ?pb-editor=1 is present. In production, $section->editorAttributes() returns an empty string — zero overhead.


JSON Templates — Shopify-Style Fallbacks

Not every page needs a custom layout. Templates let you define default structures for pages that don't have their own JSON or Blade view:

{
    "layout": "page",
    "wrapper": "main#page-main.container",
    "sections": {
        "hero": {
            "type": "hero",
            "settings": {
                "title": "{{ $page->title }}"
            }
        },
        "content": { "type": "page-content" }
    },
    "order": ["hero", "content"]
}
Enter fullscreen mode Exit fullscreen mode

Drop this in resources/views/templates/default.json, assign $page->template = 'default', and every page using that template renders with the same structure — settings pre-filled from the page model.

The wrapper key accepts a CSS selector: main#id.class[attr=value]. The renderer builds the wrapping element from it automatically.


Multi-Theme Support

The package integrates with qirolab/laravel-themer. Each theme can:

  • Register its own sections and blocks from themes/{name}/views/sections/ and blocks/
  • Override app templates by shadowing templates/{name}.json
  • Define global settings schema (colors, fonts, spacing) that feed into CSS variables

Sections can accept any theme block using the @theme wildcard:

@schema([
    'name'   => 'Content Row',
    'blocks' => [
        ['type' => '@theme'],  // accepts any registered block from the active theme
    ],
])
Enter fullscreen mode Exit fullscreen mode

Themes can shadow without modifying. If a theme ships hero.blade.php, it takes precedence over the app's hero.blade.php — no changes to the app required.


Recursive Block Nesting

Blocks nest to any depth. A row block can contain column blocks, which can contain card blocks, which can contain icon blocks. The renderer traverses the tree recursively, and the editor renders nested drag-and-drop handles at every level.

Block references in a section schema work in three ways:

Entry Resolved as
['type' => 'row'] Theme block — looked up via BlockRegistry
['type' => '@theme'] Wildcard — any registered block
['type' => 'item', 'name' => 'Custom Item'] Local definition — used as-is

The distinction matters: adding a name key to a bare type reference changes it from a registry lookup to a local definition.


Per-Page Layout Zones

Pages can define header and footer sections that render outside the main sortable content:

{
    "header": {
        "sections": {
            "nav": { "type": "navigation" }
        },
        "order": ["nav"]
    },
    "sections": {
        "hero": { "type": "hero" }
    },
    "order": ["hero"]
}
Enter fullscreen mode Exit fullscreen mode

Rendered via @sections('header') in the Blade layout. Editors configure them in a dedicated panel — they don't appear in the main drag-and-drop list.


Adding a New Field Type

The 21 built-in types cover most needs, but adding a custom type doesn't touch the core engine. Create a React component under resources/js/components/settings/, register it in FieldRegistry.ts, and it's available in any section's @schema() definition.


Installation

composer require coderstm/laravel-page-builder
php artisan vendor:publish --tag=pagebuilder-config
php artisan vendor:publish --tag=pagebuilder-views
php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Then create your first section in resources/views/sections/hero.blade.php and visit /page-builder in your app.


What's Next

We're actively working on:

  • Revision history with diff view
  • More built-in section types (pricing, testimonials, FAQ)
  • Headless mode — JSON output for decoupled frontends

Links


If you've built something on top of this or have questions about the architecture, drop them in the comments. Happy to dig into any layer of the system.

Top comments (0)