makraz/ux-vvvebjs
A Laravel package that integrates the VvvebJs drag-and-drop page builder, letting you create and edit HTML pages visually. It provides UX components and assets for quick setup and customization in Laravel applications.
Read this first: it is a Symfony UX bundle, not a Laravel package. There is no service provider, no config/vvvebjs.php to publish, no Blade views, and no Laravel-facing form type. composer require makraz/ux-vvvebjs inside a Laravel app will pull Symfony Form/HttpKernel/Stimulus wiring you cannot boot. Treat v0.1.0 as a reference implementation and JS asset kit for VvvebJs, and lift the pieces you need.
Minimal path for a Laravel developer:
resources/js and resources/css, or vendor the built assets into public/vendor/vvvebjs.assets/controller/…) into resources/js/controllers/. Stimulus works fine in Laravel via @hotwired/stimulus; register it in your app.js. This controller is the real value: iframe lifecycle, dynamic plugin/component loading, and hidden-field sync.<div data-controller="vvvebjs" data-vvvebjs-…> plus <input type="hidden" name="content">.POST /vvvebjs/upload) using Storage::disk(...) — Laravel's filesystem is Flysystem, so the "Flysystem backend" maps naturally to Storage::put.longText/json column and render it through a sanitizer.First useful use case: a pages.content HTML column edited by an admin through a full-width page builder, with a read-only preview for reviewers.
If you are on Symfony (or maintaining a hybrid), the bundle's own quick path is: enable the bundle, ensure symfony/stimulus-bundle ≥ 2.9.1, use VvvebJsType in a form, and optionally drop VvvebJsAdminField into EasyAdmin.
Field + hidden input pattern. The bundle's core contract is simple and worth copying verbatim: render the editor into an iframe, keep the canonical HTML in a hidden input, sync on form submit and page unload. In Blade:
<div data-controller="vvvebjs"
data-vvvebjs-height-value="700"
data-vvvebjs-upload-url-value="{{ route('vvvebjs.upload') }}"
data-vvvebjs-read-only-value="{{ $readOnly ? 'true' : 'false' }}">
<input type="hidden" name="content" value="{{ old('content', $page->content) }}">
</div>
Multi-instance. The bundle supports several editors per page. In Laravel that means every instance needs distinct element IDs, its own hidden input name (blocks[0][html], blocks[1][html]), and a per-instance uploadUrl if uploads land in different folders. The Stimulus controller is scoped to its element, so this works — but plugin scripts loaded dynamically by VvvebJs can collide globally. Enable only the plugins each instance actually needs.
Uploads. Prefer a custom handler over the bundled backends. In Laravel terms: a VvvebJsUploadController with $request->validate(['file' => 'image|max:10240']), Storage::disk('public')->putFile(...), returning the JSON shape the editor expects. If you are literally porting the Symfony controller, keep its UploadHandlerInterface seam — it is the cleanest extension point in the package.
Self-hosting over CDN. Default assets come from jsDelivr. For any real app, set cdn_url equivalent to a local path and pin the VvvebJs version. Put the files under public/vendor/vvvebjs (or import through Vite) so CSP, offline dev, and asset versioning behave. The bundle supports both AssetMapper and Webpack Encore; the Laravel analogue is Vite — the same "just give me URLs" approach works.
Sanitize on write or read, always. Page-builder output is untrusted HTML by definition. Run it through mews/purifier (HTMLPurifier) before persisting or before rendering, and restrict designerMode to trusted roles. readOnly is a UI convenience, not a security boundary.
Admin panels. VvvebJsAdminField is the EasyAdmin one-liner. The Filament/Nova equivalents are a custom field wrapping the same Stimulus controller — expect ~50 lines, not a config flag.
Version pinning. It is 0.1.0 and explicitly pre-BC. If you vendor any of its code, fork it into your own repo with a comment pointing at the upstream commit, and don't track ^0.1.
wire:submit, and Inertia visits may not trigger the unload sync reliably. In Livewire, bind the hidden input with wire:model and dispatch an input event from the controller after every editor change — don't rely on submit alone.wire:ignore and re-initialize the Stimulus controller on livewire:navigated.BOOTSTRAP5, COMMON, etc.) assume their own CSS is present.frame-src/script-src policies will silently break syncing. Whitelist the CDN or self-hosted asset origin explicitly; check the iframe's console, not just the parent's.upload_max_filesize/post_max_size, and Nginx's client_max_body_size. A "file too large" that never reaches your controller is usually the web server.@csrf / X-CSRF-TOKEN wired into whatever the editor's uploader sends — the editor's default multipart request likely does not include it, so configure headers or exempt the route behind auth + throttle.UploadHandlerInterface-shaped class backed by Storage — you get disks, visibility, and URL generation for free.cdn_url resolve under your CSP? (4) is the upload response shape what the editor expects (url, name, etc.)?UploadHandlerInterface (custom storage), cdn_url (self-hosting), height/border (layout fit), designerMode/readOnly (role gating), and the plugin/component-group config (payload trimming). Everything else you will end up writing yourself.How can I help you explore Laravel packages today?