hansschouten/phpagebuilder
PHPageBuilder is a drag-and-drop page builder for PHP applications. It lets you create and edit pages visually with customizable blocks, manage layouts, and store content in a database. Framework-agnostic, works with Laravel or plain PHP, with multilingual support and an extensible block system.
The package is a framework-agnostic drag-and-drop page builder. You define a schema (which containers/rows exist and which blocks can go in them), the editor UI lets an editor compose a page, and the result is stored as JSON that you render with plain PHP views.
composer require hansschouten/phpagebuilder (if Packagist resolution fails, add the repo as a VCS entry in composer.json), then commit the lockfile — the last release is Jan 2025 and activity is thin, so treat the version as frozen until you deliberately upgrade.demo/ directory and the shipped pagebuilder.json (the schema file). That file is the real documentation: it shows containers (name + allowed block types) and blocks (name, view template path, and a fields definition used to auto-generate the editor form).main, sidebar) and two blocks (text, image). Point each block's view at a partial in your app, e.g. resources/views/blocks/text.php, and give it one field (type: textarea / type: text).PhpPageBuilder::create($schemaPathOrArray). The editor is a single embedded page: the builder serves the Vue UI and handles the save POST for you. Put it behind auth before you show it to anyone.view partials. Your first use case is almost always a marketing/landing page: a hero text block, an image block, and a rich-text block in one container.Minimal Laravel version: one route for GET /admin/pages/{page}/edit (builder), one for POST saves (if you take saving over yourself), one public GET /{slug} that renders the stored JSON.
Schema as committed code, pages as data. Keep pagebuilder.json (or a config/pagebuilder.php array) in version control, one schema per site/locale/tenant. Pages — the editor output — live in storage. That separation is what makes schema changes reviewable and page content editable by non-developers.
Custom storage is the main integration seam. The default storage is file-based JSON, which is fine for a prototype but wrong for multi-server Laravel. Implement the package's storage interface against Eloquent: a pages table with slug, title, json, published_at. One row per page, JSON cast to array. This also gives you drafts vs. published, revisions (keep a page_revisions table on every save), and per-user permissions almost for free.
Block views are plain PHP includes — keep them dumb. Each partial should receive the block's saved field values and do nothing but escape and print. Typical block set for a real project: text (rich text), image (URL + alt + optional link), cta (label + href), embed (raw HTML, sanitized). Add a _wrapper block convention if you need per-block CSS classes/anchor IDs.
Render pipeline and caching. Don't call the renderer on every request. Compile once into HTML and cache it keyed on page_id + updated_at (Cache::remember). Render-time is cheap (includes), but it's still N file lookups and you lose the ability to cache the whole response otherwise. Invalidate on save and on deploy.
Reuse the editor for structured content, not just pages. Because the schema defines blocks independently, teams commonly point a second schema at a "sections" field on other models (product descriptions, landing sections on a CMS entry). Store the JSON in a json column and render it through the same partials.
Restrict rather than validate. The cleanest way to keep editors out of trouble is narrower containers: a hero container that only accepts text and image, a body container that accepts anything. Fewer schema variants beat runtime validation.
Laravel wiring. Bind a configured builder instance in a service provider built from config('pagebuilder.schema'); publish nothing, resolve the schema path from base_path(). Put editor routes in routes/admin.php behind auth + a policy, and the render route in routes/web.php. If you render inside Blade, expose a @pagebuilder($page) directive that calls your cached renderer and outputs {!! !!} — that's the one intentional unescaped output in the app, so keep it in one place.
Media. Field types cover text/textarea/select/image-URL; they do not manage uploads. Add your own upload endpoint (Laravel Storage::disk('public'), intervention/image for resizing) and have the image field store a path, resolved to a URL in the block partial so CDN/base-URL changes don't require re-editing every page.
storage/ or a DB before you go live, and check file permissions for the PHP-FPM user.isset.e().view paths from its own root in some configurations — use absolute paths from base_path()/__DIR__ to remove ambiguity). Also log the POST body on save failures; the editor's error surface is thin.public/ automatically; either symlink the package's dist directory at deploy or copy assets into your build. Pin the version — an unpinned upgrade can swap the build hash and leave you serving a stale UI that "works" but drops fields.How can I help you explore Laravel packages today?