digitalcloud/nova-page-builder-field
A Laravel Nova field that adds a page builder to your resources, letting users compose and manage page content through a flexible builder UI. Integrates page building directly into Nova so content structures can be created and edited alongside your models.
Before you install, check the calendar. The last tagged release is January 2019, which puts this package in the Nova 1.x / 2.x era (Laravel 5.7–5.8). If you're on Nova 4 or 5, the field's compiled JS relies on the old FormField / HandlesValidationErrors mixins and Nova::script() asset registration, so it will not mount without a fork and a rebuild. Verify against your composer.json Nova constraint first — that single check saves the most time.
Minimal path on a compatible Nova install:
composer require digitalcloud/nova-page-builder-field
php artisan vendor:publish --provider="DigitalCloud\NovaPageBuilderField\FieldServiceProvider"
php artisan nova:publish
Then add the field to a resource — the class lives under src/ in the package (commonly DigitalCloud\NovaPageBuilderField\PageBuilder; confirm the exact FQCN in the README or src/):
use DigitalCloud\NovaPageBuilderField\PageBuilder;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title'),
PageBuilder::make('Content')->hideFromIndex(),
];
}
Where to look first, in order:
README.md — for the exact field class name and any config options.src/ — the Field subclass; this is where component, resolveUsing, and fillUsing behavior is defined.resources/js/field.js (or similar) — the Vue component that boots GrapesJS. This is the file you'll almost certainly end up forking.config/ — if published, editor/plugin settings live here; otherwise options are hard-coded in the Vue component.First use case: a Page model with a longText or json content column, a single Nova resource, and a Blade view that renders {!! $page->content !!} inside your site layout. Get one page rendering end-to-end before you introduce blocks, plugins, or a media manager. This validates the two things that usually break: the editor not mounting, and the stored HTML not surviving sanitization.
Decide the storage shape on day one. GrapesJS emits HTML and CSS, and its project state is JSON. Storing only HTML means you can never re-open the page in the editor without losing structure; storing only JSON means your front end has to render the project. The durable pattern is both:
content_html (LONGTEXT) — what the front end serves.content_project (JSON/LONGTEXT) — what the editor rehydrates.content_css (LONGTEXT, nullable) — GrapesJS's component CSS, if you enable it.Model events are the natural place to derive one from the other:
protected static function booted()
{
static::saving(function ($page) {
$page->content_html = app(HtmlSanitizer::class)->clean(
$page->content_html ?? ''
);
});
}
Render with a wrapper contract. Pair the builder field with a plain Select for layout/template, then dispatch in Blade:
@extends('layouts.' . ($page->template ?? 'default'))
@section('content')
<div class="pb-scope">
@if($page->content_css)<style>{!! $page->content_css !!}</style>@endif
{!! $page->content_html !!}
</div>
@endsection
The .pb-scope wrapper matters — see gotchas.
Custom blocks without forking. GrapesJS's block manager is the intended extension point. If the package exposes a JS hook (check the Vue component for a block registration pass), register your app's blocks there. If not, publish the field's JS, keep the package's editor init, and append your own editor.BlockManager.add(...) calls. Keep these in a small, versioned JS file so upgrades are a diff, not a rewrite.
Draft vs. published. Don't serve unpublished edits straight from the same column. Keep the editor bound to draft columns, and on an explicit "Publish" action, copy draft → published and bump a published_at. This is cheap and prevents the common failure where an editor saves a half-finished layout and the live page breaks.
Preview inside Nova. A Text field with asHtml() or a small custom detail field that renders the stored HTML in an iframe is worth 20 lines — it lets editors check output without leaving the admin.
Multiple builders on one resource. If a page has hero + body + footer builders, give each a distinct attribute and confirm the package's Vue component keys its GrapesJS container by field attribute. If it hard-codes a container ID, only the first field will mount, and you'll need to patch the component.
Caching. Generated HTML is expensive to re-render but trivial to cache. Put a Cache::remember("page:{$id}:html", ...) around the render, and Cache::forget in the saved observer. This is the single highest-leverage addition for a builder-driven site.
Storage::disk('public')) and a custom AssetManager before you let anyone near the editor.<script>, on* handlers, and javascript: URLs..pb-scope { ... }) or, better, scope each component's rules at save time. Without this, a page builder style will silently restyle your admin or your marketing site.hideFromIndex() on the builder field — the resource index will try to resolve and render megabytes of JSON otherwise, and Nova's search/sort columns get slow fast.nova:publish after every install or update. The classic "field renders blank" symptom is stale compiled assets in public/vendor/nova. Clear it, re-publish, hard-refresh.storage/logs/laravel.log for PHP-side resolution errors. A blank grey box almost always means the Vue component threw during mounted().package.json and upgrade deliberately.LONGTEXT/JSON (not TEXT), and consider excluding the project column from default SELECT * queries via $hidden/$appends discipline.How can I help you explore Laravel packages today?