coderstm/laravel-page-builder
Laravel Page Builder package for creating and managing dynamic pages with reusable content blocks and a visual editing workflow. Integrates into Laravel apps to build customizable layouts, templates, and page content.
Install and inspect before you integrate. With zero stars and zero dependents, treat the first hour as a code audit, not a setup step.
composer require coderstm/laravel-page-builder
Because the package is Laravel-native, expect the usual surface: an auto-discovered service provider, publishable config, migrations, and views. Publish everything into your app so you can read and modify it:
php artisan vendor:publish --provider="Coderstm\\PageBuilder\\PageBuilderServiceProvider"
Where to look first, in order:
composer.json in the vendor directory — check the Laravel and PHP version constraints against your stack. This is the single most likely reason the spike fails on day one.First use case to build: a single static marketing page assembled from three sections — hero, feature grid, CTA — each containing one or two blocks. Do not build the editor yet. Hand-write the JSON layout directly against the database, then render it through the package's Blade layer. This proves the rendering pipeline and forces you to read the schema before you commit to it.
// Roughly the shape you'll be working with — verify against the actual schema in source first.
$page = Page::create([
'slug' => 'landing',
'theme' => 'default',
'layout' => [
['type' => 'hero', 'blocks' => [['type' => 'heading', 'data' => ['text' => 'Hello']]]],
['type' => 'features','blocks' => [['type' => 'feature-list', 'data' => ['items' => [...]]]]],
['type' => 'cta', 'blocks' => [['type' => 'button', 'data' => ['label' => 'Sign up', 'url' => '/register']]]],
],
]);
Once that renders, you have a working mental model and a schema fixture you can harden against.
Page model holds JSON; renderer is stateless. Keep the layout JSON column on your own Page (or equivalent) model, and call into the package's renderer from a controller or Blade component. This keeps the package on the edge of your app rather than threaded through it — critical when, not if, you fork it.
// app/Http/Controllers/PageController.php
public function show(string $slug)
{
$page = Page::where('slug', $slug)->where('status', 'published')->firstOrFail();
return view('pages.show', [
'page' => $page,
'html' => PageBuilder::render($page->layout, $page->theme),
]);
}
Wrap the package behind your own interface. This is the single most important pattern given the stewardship risk. Define an internal PageRenderer contract, implement it against this package, and let everything else in your app depend on the interface.
interface PageRenderer
{
public function render(array $layout, ?string $theme = null): string;
}
When the fork drifts or you replace it wholesale, only the bound implementation changes.
Register custom sections/blocks in one place. Keep a single registry file (or provider method) that maps a block type string to its PHP definition and Blade partial. This becomes your content contract — document it, version it, and validate against it on save.
// app/PageBuilder/registry.php
return [
'heading' => [
'view' => 'page-builder.blocks.heading',
'fields' => ['text' => 'string', 'level' => 'int'],
],
'button' => [
'view' => 'page-builder.blocks.button',
'fields' => ['label' => 'string', 'url' => 'string', 'style' => 'string'],
],
];
Validate on save, fall back on render. Two different failure modes, two different responses. On save, reject unknown block types and unexpected fields — the editor should never persist a layout the renderer cannot handle. On render, log-and-skip unknown or broken blocks rather than throwing a 500 at a live page. A marketing page with one missing block is a bug; a 500 is an incident.
Materialize rendered HTML on publish. Layouts hydrate per section and per block per request. That is cheap at low traffic and increasingly expensive as page count and block count grow. Add a rendered_html column (or a full-page cache keyed by a page_version integer bumped on publish) so scaling becomes a cache question, not a renderer question.
// On publish
$page->update([
'rendered_html' => PageBuilder::render($page->layout, $page->theme),
'status' => 'published',
'version' => $page->version + 1,
]);
Theme as a first-class request concern. Resolve the theme once per request and pass it down explicitly. Multi-tenant or multi-brand setups should key theme selection off the tenant/brand context, not off per-block configuration — otherwise you get partial brand bleed that is painful to debug.
Serve JSON for headless consumers. Because layouts are structured JSON, you get a delivery endpoint nearly free. Reuse the same layout payload for a /api/pages/{slug} route, and let the same validation gate both the Blade renderer and the API response. This is the pattern's biggest practical payoff — content stops being trapped in HTML.
Draft, preview, publish. If the package does not ship these, add them at the Page model level: status, published_at, version, and a signed preview token route that renders the draft layout against the live theme. Preview is the difference between a usable editor and a broken workflow.
There is no schema version field — add one immediately. A JSON layout is a data contract, and without a schema_version key in every stored layout, the day you rename a block type is the day you write a content migration under pressure. Bake it in on day one, reject missing versions on save, and write a migration path per version bump.
Sanitize block HTML server-side, without exception. Any block field that accepts HTML (rich text, embed, raw) is a stored-XSS vector the moment an editor account is compromised or careless. Use an allowlist-based sanitizer (HTML Purifier, mews/purifier, or a Laravel-validated subset) at both save and render time. Do not trust the editor. Do not trust the theme.
Missing block partials are a deploy-time 500. Add a deploy test that asserts every registered block type has a resolvable view, and a render-time guard that logs and skips when the view is missing. A single renamed Blade partial should never take out every page that uses it.
Theme upload is a remote-code surface. If the package or your editor allows uploading or installing themes, that path executes Blade — which is PHP. Validate uploads aggressively, sandbox if possible, and if you cannot lock it down, disable theme upload entirely and treat themes as deploy artifacts.
Confirm the version constraints before you build anything. A package with no visible release cadence frequently pins to an older Laravel. Test the install against your actual current and next-major versions in the first hour, not the first week.
Expect CSS-framework assumptions in the default theme. The shipped theme almost certainly assumes Tailwind, Bootstrap, or similar. If it collides with your design system, budget the rewrite into the spike — do not discover it during the editor build.
Watch JSON column size and autosave frequency. MySQL json and PostgreSQL jsonb behave differently for indexing and querying; if you plan to query into layouts, jsonb is materially nicer. On the editor side, debounce autosave and cap section count per page; a page with 200 blocks autosaving every keystroke will find your write throughput ceiling.
Resolve theme once, not per block. Obvious in hindsight, easy to get wrong in a naive extension, and a measurable per-request cost once you have more than a handful of blocks.
Pin to a fork, not to Packagist. The plan is to vendor the package into packages/ or a private VCS fork, pin it in composer.json, and add CI plus characterization tests around the JSON contract before changing any code. This is not a contingency; it is the adoption path. With zero stars and zero dependents there is no ecosystem to lean on and no security-response process to inherit.
Add a nightly JSON export. If the editor lacks revisions — and it likely does — an export of every published layout to object storage is your recovery net. Cheap, effective, and it also gives you a diffable history for debugging "why did this section disappear."
Extension points worth knowing: custom block types via the registry; custom section definitions via the config; custom themes via the views/theme resolution layer; and the renderer itself, which you should call through your own PageRenderer interface so the package stays replaceable.
How can I help you explore Laravel packages today?