hansschouten/laravel-pagebuilder
Laravel Pagebuilder provides a drag-and-drop, block-based page editor for Laravel apps. Create reusable content blocks, manage page templates, and render dynamic pages with Blade, no separate CMS needed. Ideal for admin-managed marketing pages and content-driven sites.
The v0.31.0 release note is only a pointer to the upstream tag, so treat the v0.30.0...v0.31.0 compare view as the actual changelog before you touch composer.json. This is a 0.x package: minor bumps have historically carried signature and config changes, so pin it exactly ("hansschouten/phpagebuilder": "0.31.0") rather than using a caret constraint, and upgrade in a branch with the diff open.
Minimal path:
composer require hansschouten/phpagebuilder:0.31.0. On Laravel versions without package auto-discovery, register the package service provider and facade alias in config/app.php.config/phpagebuilder.php), migrations, and the block/editor views under resources/views/vendor/.... Commit the published migrations — see the gotcha below.php artisan migrate, then confirm the pages-style tables exist before wiring any routes.layouts.app) that yields the builder content where you want it rendered.{!! PageBuilder::loadPage($page) !!} (facade) or the equivalent phpagebuilder() helper, inside a view that extends your layout. Verify in this order: admin panel loads → page saves → block content appears on the front end → uploaded image resolves from public/.Where to look first, in this order: config/phpagebuilder.php (layout view, uploads directory, middleware, route prefix, registered blocks), the published block views, then the published migrations to learn the schema. Most "it doesn't work" issues in this package are a config value or an unregistered block, not a bug.
Layout-first. Decide the wrapper view before anyone starts authoring pages. The builder stores block content only; chrome, nav, and meta belong in your layout. Give the layout a consistent contract (a $page variable for title/SEO fields) so every page inherits it.
Custom blocks as the unit of work. Day-to-day authoring value comes from blocks, not from the generic rich-text block. The pattern is a block class (render logic, thumbnail, view data) plus a Blade partial. Keep the class thin: resolve data, return a view, let Blade do the markup. Register each new block in config and restart/clear view cache — an unregistered block silently fails to appear in the editor.
Routing. Pages carry a route/slug field, so rendering is a lookup: either an explicit route per page, or a catch-all Route::get('{slug}', ...)->where('slug', '.*') registered after all static routes. Handle misses with a real 404 rather than rendering an empty layout, and keep the builder prefix out of your sitemap.
Lock down the admin panel immediately. Set the package's middleware config to ['web', 'auth'] (plus a gate or role check in a service provider) on day one, and put the route prefix behind an IP allowlist or separate guard if the panel is internet-facing. These routes are auto-registered and easy to forget in a launch checklist — check php artisan route:list after install.
Cache the rendered output, not the block queries. Rendering walks block classes per request. Cache::rememberForever("page:{$page->id}:{$page->updated_at}", fn () => view(...)) gets you most of the win with trivial invalidation. If you cache full responses instead, only cache pages that don't depend on session state — never cache an authenticated variant.
Swap the page model instead of editing vendor code. Point the config's model entry at App\Models\Page extends the package model and add your own scopes, relations, and casts there. Same for the repository/manager accessors: decorate, don't fork. This is what keeps the next 0.x upgrade cheap.
Content parity across environments. Write an artisan command that exports pages and blocks to JSON and imports them into staging/prod, and a seeder that plants a few fixture pages locally. Without this, content and code drift and nobody can reproduce a rendering bug.
Testing. There are no model factories shipped, so write Page::factory() yourself and feature-test the rendering route with assertSee. Keep one HTML snapshot fixture per custom block; it's the fastest way to catch a breaking change from a package bump.
database/migrations against the package's on every upgrade and add your own follow-up migration rather than re-publishing over live tables.{!! !!} on anything a non-trusted editor typed is the single largest XSS surface here — restrict the HTML block to trusted roles, or sanitize on save..php-named file in a public folder on a misconfigured server is an RCE. If that's a concern, move uploads behind a controller that sets Content-Type explicitly.php artisan view:clear, confirm the published view path, and temporarily wrap the render call in a try/catch to surface the swallowed error.route:clear and config:clear before debugging anything else.public/vendor/... and out of your pipeline.vendor/ — which is exactly what makes the next 0.x bump a fifteen-minute job instead of a rewrite.How can I help you explore Laravel packages today?