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

Voyager Page Blocks Laravel Package

pvtl/voyager-page-blocks

A Laravel package that adds reusable page blocks to Voyager, letting you compose pages from modular content sections managed in the admin panel. Includes block types, page associations, and flexible rendering for faster, more maintainable site building.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites

  • A Laravel app with Voyager 1.x already installed and working (php artisan voyager:install has been run, admin login works).
  • PHP 7.2–7.4 is the safe bet. The package was last released 2020-07-27 against Laravel 6/7-era Voyager; expect friction on PHP 8+ and Laravel 9+.
  • Composer access to the repo. It's archived, so composer require pvtl/voyager-page-blocks may still resolve from Packagist, but pin it and be ready to switch to a VCS path repo or a fork if you need patches.

Install in four steps

composer require pvtl/voyager-page-blocks

# Publish migrations (and any config/views the provider offers)
php artisan vendor:publish --provider="Pvtl\VoyagerPageBlocks\PageBlocksServiceProvider"

php artisan migrate

# Voyager admin: check the sidebar for the new "Page Blocks" BREAD

Confirm the exact provider namespace, publish tags, and config keys against the repo README before running — with an archived package you only get one shot at reading the docs, and they may not match your Voyager version.

Where to look first

  1. database/migrations/ after publishing — this tells you the real schema (columns, ordering field, whether it's polymorphic or page-id based). Everything else follows from this.
  2. The registered BREAD in Voyager admin (/admin/page-blocks) — this is where editors create blocks and attach them.
  3. The package's model (PageBlock or similar) — read its relationships and any render()/accessor methods before writing your own Blade.
  4. config/voyager.php — to understand how the module hooks into Voyager's menu and routes.

First use case: a homepage with a hero and a text block

  1. In Voyager admin, create two blocks: one of type hero, one of type text.
  2. Attach them to your page record (via the BREAD's relationship field, or by setting the page/parent id — depends on the schema you saw in step 1).
  3. Render in Blade:
@foreach ($page->blocks()->orderBy('order')->get() as $block)
    @includeIf("blocks.{$block->type}", ['block' => $block])
@endforeach
  1. Create resources/views/blocks/hero.blade.php and blocks/text.blade.php. The @includeIf means an unknown block type silently renders nothing rather than throwing.

That's the whole mental model: blocks are rows, types are Blade partials, order is a column.


Implementation Patterns

1. Type-to-partial convention (the core pattern)

Keep a one-to-one mapping between the type column and a Blade partial under resources/views/blocks/. Add a fallback partial for unknowns so editors can't 500 a page:

@includeFirst(["blocks.{$block->type}", 'blocks.fallback'], ['block' => $block])

This lets non-developers create blocks in admin without touching code, as long as you've shipped the partial.

2. Centralise rendering in a component or helper

Don't scatter @foreach loops through views. Wrap it once:

{{-- resources/views/components/page-blocks.blade.php --}}
@props(['page'])
@foreach ($page->blocks as $block)
    @includeFirst(["blocks.{$block->type}", 'blocks.fallback'], ['block' => $block])
@endforeach
<x-page-blocks :page="$page" />

This is also the single place to add caching, sanitisation, and error handling later.

3. Polymorphic/reusable blocks

If the schema uses a blockable morph (check the migration), you can attach the same block concept to products, articles, landing pages — not just pages. That's the highest-leverage use of the package: one editorial UI, many content types.

// In each model that owns blocks
public function blocks(): MorphMany
{
    return $this->morphMany(PageBlock::class, 'blockable')->orderBy('order');
}

If the schema is hard-wired to a page_id, prefer adding a morph column yourself over duplicating the table.

4. Ordering UI

Voyager's relationship BREAD gives you a drag-and-drop or multi-select, but it doesn't always persist an order column. Two reliable options:

  • Add order to the relationship pivot/table and sort by it in the accessor; provide a small custom Voyager form field or reorder action.
  • Use Voyager's built-in ordering if the BREAD supports it (order_column in the model's $sortable).

Pick one and document it for editors — otherwise "why did my blocks reorder?" becomes a support ticket.

5. Caching

Block rendering is a prime cache target. Key on the parent model and its updated_at:

public function renderedBlocks(): string
{
    return Cache::remember(
        "page-blocks:{$this->getMorphClass()}:{$this->id}:{$this->updated_at}",
        now()->addHours(6),
        fn () => view('partials.blocks', ['blocks' => $this->blocks])->render()
    );
}

Bust it in the block model's saved and deleted events so editors see changes immediately:

static::saved(fn ($block) => Cache::forget(/* keys for the parent */));

6. Seeding for parity across environments

Blocks are content, and content that only exists in the production admin UI will bite you in staging. Seed the standard block set (hero, CTA, footer text) per environment so tests and local dev match production shape.

7. Block-level validation

If block content is stored as JSON or HTML, validate on save in a Voyager model observer, not at render time. A malformed block should be rejected by the editor, not caught by a 500 on the front end.


Gotchas and Tips

Archived status is the headline

Last release mid-2020, repo archived. Treat it as vendored code you own, not a dependency you upgrade. Before committing:

  • Check composer.json constraints against your Laravel/PHP versions.
  • Search the source for deprecated helpers (str_*, array_*, Arr:: usage) and Laravel facade changes if you're on 9+.
  • The cheapest long-term fix is often to copy the migration, model, and 2–3 Blade partials into app/ and drop the dependency entirely. The package's value is the editorial UX pattern, not the code.

Voyager BREAD permissions

The single most common "it's installed but nothing appears" cause: the BREAD exists but roles lack browse/read/edit permissions. Go to Tools → BREAD → Page Blocks → edit → permissions and grant access explicitly. Also confirm the menu item landed in admin (check config/voyager.php menu).

Migration collisions

Publishing may produce a create_page_blocks_table migration that clashes with an existing table of the same name (especially if you've tried a similar package before). Read the published migration and rename or guard with Schema::hasTable() before running.

Relationship vs. column attachment

The gotcha that wastes the most time: the package may attach blocks via a Voyager relationship (pivot/morph) and also have a legacy page_id column. Decide which your queries use and stick to it — mixing both produces duplicate blocks in one view and missing blocks in another.

N+1 on blocks and media

Blocks that reference Voyager media ($block->image, file attachments) will hammer the DB without eager loading:

$page->load('blocks.media'); // adjust relation name to the actual schema

And if you're caching, cache the rendered HTML, not the Eloquent collection — serialised models with media relations get stale and large.

Unknown block types should not 500

Always use @includeIf / @includeFirst with a fallback. When an editor picks a type you haven't built a partial for, log it and render nothing. Bonus: this lets you delete a block type from the codebase without breaking existing pages.

XSS on rich-text blocks

{!! $block->content !!} for a text/html block is standard, but only safe if the editor role is trusted. If lower-privilege users can edit blocks, run the content through a sanitiser (HTMLPurifier, mews/purifier) on save.

Voyager storage driver

Images uploaded via the block form go through Voyager's storage driver (local or S3). Switching drivers later breaks stored paths. Centralise a blockImageUrl($block) helper rather than calling Voyager::image() inline everywhere, so a future driver change is one edit.

Order column quirks

If blocks appear unsorted, check:

  • The order column exists and is non-nullable with a default.
  • Your query actually orderBy('order') — a relationship defined without it will return insertion order.
  • The Voyager BREAD's relationship form field isn't reordering on save (some versions do).

Debugging checklist

Symptom Likely cause
BREAD missing from admin Permissions on the BREAD, or menu cache (php artisan view:clear)
Blocks render but out of order Missing orderBy, or null order values
White page / 500 on front end Unknown type, missing partial — add fallback
Blocks duplicate Both page_id and polymorphic relation queried
Editor changes not visible Cached render, key not busted on save
File/image paths broken Voyager storage driver changed

Extension points worth using

  • A render() method or accessor on the block model — override in an app-level subclass to centralise formatting.
  • Model events (saved, deleted) on the block model — for cache busting and search index updates.
  • A custom Voyager form field for block type (select from registered partials) — prevents editors choosing types that don't exist.
  • View composers — if you'd rather not wrap in a Blade component.

If you're greenfield, reconsider

The pattern (typed content blocks + Voyager BREAD + Blade partials) is sound and worth copying. The package itself is a 2020 artefact. On a new project, implement the same three-table/three-file pattern yourself in an afternoon and keep your dependency graph clean.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin