Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Page Blocks Laravel Package

dejodev/page-blocks

A Laravel package for creating and managing reusable page blocks, letting you compose dynamic page content from configurable block types and render them easily in Blade views without hardcoding layouts.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a small, single-purpose Filament add-on for editable page content: rather than hard-coding marketing pages, you give editors a stack of "blocks" (hero, rich text, image + text, CTA, etc.) they can reorder and fill in. It is a low-traffic package (0 stars, 0 dependents), so treat the source as the documentation and budget time to read it before promising it to a client.

Install and publish

composer require dejodev/page-blocks
php artisan vendor:publish --provider="DejoDev\PageBlocks\PageBlocksServiceProvider"
php artisan migrate

Publish both config and migrations in one go if the provider tags them; if vendor:publish lists tags, prefer the explicit --tag= form so you know exactly what landed in your app.

Register the plugin with your panel

// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(\DejoDev\PageBlocks\PageBlocksPlugin::make());
}

Where to look first, in this order

  1. config/page-blocks.php — this is almost always where block types are declared, along with the model/table names and any disk or route prefix. This file is your real API surface.
  2. The published migration — it defines the JSON/longText column that holds the block payload. Knowing the exact shape ([{type, data}] vs {type: data}) saves you hours later when you need to query or migrate it.
  3. The Filament Resource / Page class in src/ — check whether it ships its own PageResource or expects you to attach blocks to an existing model. This decides whether you adopt the package's model or wrap your own.
  4. The Blade renderer — the resources/views folder shows you which view name is used per block type. That naming convention is your extension point.

First use case to validate with

Build one throwaway page: a hero block (heading, subheading, background image), a rich_text block, and a cta block with a link. Create it in the panel, render it on a public route, and confirm that reordering the blocks in the admin changes the public output. If that loop works end to end, the rest of the package is variations on a theme. If it doesn't, you've found the package's ceiling before you've written any client code.

Implementation Patterns

Treat the block registry as the contract

Day to day, most work is adding block types. Keep them declarative and colocated — one class or one config array per block, containing: a key, a label, an icon, a Filament form schema, and a Blade view. Resist the temptation to add per-block PHP logic; if a block needs behaviour, that behaviour usually belongs in the renderer or a view composer, not in the block definition.

// config/page-blocks.php — illustrative shape; confirm key names in the source
'blocks' => [
    'hero' => [
        'label' => 'Hero',
        'icon'  => 'heroicon-o-photo',
        'view'  => 'blocks.hero',
        'schema' => [
            TextInput::make('heading')->required(),
            Textarea::make('subheading'),
            FileUpload::make('image')->image()->directory('pages/hero'),
            TextInput::make('cta_label'),
            TextInput::make('cta_url')->url(),
        ],
    ],
    // ...
],

Render through a single entry point

Do not scatter @foreach ($page->blocks as $block) across a dozen views. Wrap it once and reuse:

// app/Support/BlockRenderer.php
public function render(Page $page): string
{
    return Cache::remember(
        "page:{$page->id}:blocks:{$page->updated_at?->timestamp}",
        now()->addHours(6),
        fn () => $page->blocks
            ->map(fn ($block) => view("blocks.{$block['type']}", $block['data'] ?? [])->render())
            ->implode(''),
    );
}

Keying the cache on updated_at (rather than flushing manually on save) means a model observer is optional, and stale content self-heals. If the package ships its own renderer, call it from inside the closure instead of duplicating the loop.

Keep public routes thin

// routes/web.php
Route::get('/{slug}', PageController::class)->where('slug', '.*');

// PageController
public function __invoke(string $slug, BlockRenderer $renderer)
{
    $page = Page::where('slug', $slug)->firstOrFail();

    return view('page', ['page' => $page, 'html' => $renderer->render($page)]);
}

Echoing pre-rendered HTML with {!! $html !!} keeps the Blade layout simple and makes the cache boundary obvious.

Seed content so environments match

Block JSON in a local database is worthless to a teammate. Ship a seeder that creates the three or four canonical pages (home, about, contact) with a fixed block order, and use that same seeder in CI for rendering tests. This also gives you a canary: if a block key is renamed in config, the seeder fails loudly rather than silently rendering empty sections.

Wrap the package in one application-level seam

Given the dependency risk of a 0-star package, put a thin application wrapper around the two things you'll actually touch — the page model and the renderer — rather than calling package classes directly from controllers, seeders and tests. If the package is abandoned or you outgrow it (you will want block-level versioning or a media library integration eventually), you swap the internals of two classes and leave the rest of the app untouched.

Editor workflow

Grant the page resource to editors, not admins, and pair it with a preview. If the package doesn't ship one, add a "Preview" action that hits the public route in a new tab; content editors judge blocks visually, and the cost of not having preview is a stream of "it looks wrong live" tickets. Also give the reorder handle a clear affordance — drag-and-drop reordering is the single most-used interaction in these builders.

Gotchas and Tips

Zero stars, zero dependents. There is no community to lean on: no Stack Overflow answers, no third-party tutorials, no issue history to search. Read every file in src/ on day one — it's a small package and this is genuinely a 30-minute job. Pin the version in composer.json and check what changed before upgrading, because there's no deprecation policy to rely on.

Filament major-version compatibility. Small Filament plugins break hard across v3 → v4 (form component namespaces, Filament\Forms\Components vs schema API, panel plugin contract). Confirm which major the package targets before installing, and if your panel is on the other version, walk away rather than patching the vendor tree.

JSON column drift. Block payloads live as JSON. That means no schema enforcement: adding ->required() to a field in config does nothing for rows already in the database. When you change a block's shape, write a one-off migration that backfills the JSON, and keep your renderer defensive:

{{-- blocks/hero.blade.php --}}
@if(!empty($image))
    <img src="{{ Storage::disk('public')->url($image) }}" alt="{{ $heading ?? '' }}">
@endif

Every $variable in a block view should be treated as possibly-missing, because old rows will be missing it.

Unknown block types render as errors, not nothing. If an editor saved a block and you then delete or rename it in config, the view resolution throws View [blocks.old-type] not found. Guard the loop with View::exists("blocks.{$type}") and log the miss — silent skipping hides real bugs; a hard 500 on your homepage doesn't.

File uploads leak. FileUpload blocks store paths; deleting a page or removing a block does not delete the underlying file unless the package explicitly implements it. Check for a deleting model hook, and if absent, add one that collects image paths from the JSON and calls Storage::disk(...)->delete(...). Do this before launch, not after — orphaned media is tedious to reconcile later.

Rich text is user input. If a block type uses RichEditor or a TinyMCE field, sanitise on output (HTMLPurifier or Filament's built-in sanitisation, depending on version) or restrict the field to trusted roles. {!! !!} on un-sanitised editor content is a stored-XSS vector, and it's the easiest mistake to make with any block builder.

Reordering can renumber your data unexpectedly. Some implementations write an explicit sort column, others rely on array order inside the JSON. Know which one yours does before you write any query that filters blocks, because "get all pages with a CTA block" is trivial in one design and requires JSON path queries in the other. On MySQL 5.7+/Postgres, whereJsonContains handles the array case; on SQLite (common in test suites) support is uneven — assert that path in a test.

Cache invalidation. Any page-level caching keyed on updated_at also needs invalidation when config changes (new block view, changed view name). Add a deploy step that clears the page cache tag, or version the cache key with a constant you bump on release.

Extension points to prefer. Publish and override the Blade views (safest — no vendor edits), override config for the block registry (safe), and only subclass package classes if the source is clearly designed for it (look for interfaces, protected methods and a service container binding). If block registration happens in a service provider with no published config, subclassing the provider is cleaner than editing config at runtime.

Verify before committing. Concrete things to confirm in the repo, since they determine your integration cost: (1) does it ship its own Page model and migration, or attach blocks to yours; (2) is the block list config-driven or hard-coded in a Filament Builder; (3) is there a renderer at all, or only the admin side; (4) does it support locale-scoped content; (5) what does composer.json constrain for filament/filament and php. If (3) is missing, budget an afternoon to write the renderer — the admin half is the harder part, so that's still a reasonable trade.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style