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

Laravel Nova Flexible Content Laravel Package

gwd/laravel-nova-flexible-content

A flexible content field for Laravel Nova that lets editors build pages from repeatable, configurable content blocks. Define layouts in PHP, then compose and reorder them in the admin panel for structured, ACF-style page building.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. This package has 1 star, 0 dependents, and a single maintainer. Treat it as a "read the source before you commit" dependency, not a drop-in. Before wiring it into a client project, open src/ and resources/js/ on GitHub and confirm:

  • Which Nova major versions are supported (check composer.json constraints for laravel/nova and laravel/framework).
  • Whether the Vue components are compiled for Nova 4 (Vue 2) or Nova 5 (Vue 3). Nova 5 moved to Vue 3 and broke most third-party fields; a low-traffic package is unlikely to have kept up.
  • Whether it ships prebuilt assets (dist/js/field.js) or expects you to npm install && npm run prod inside the package.

If any of those are unclear, compare against whitecube/nova-flexible-content or outl1ne/nova-flexible-content — both are actively maintained and cover the same "ACF-style flexible blocks in Nova" use case. Only pick this one if you have a specific reason (fork, existing data, licensing).

Install (typical shape — verify against the README):

composer require gwd/laravel-nova-flexible-content
php artisan vendor:publish --provider="Gwd\FlexibleContent\FieldServiceProvider"
php artisan migrate   # only if the package ships a migration; most flexible-content fields store JSON on your own model

Most flexible-content fields are not their own table — they serialize block data into a JSON/TEXT column on your existing model. Add that column yourself:

Schema::table('pages', function (Blueprint $table) {
    $table->json('content')->nullable();
});

And cast it:

protected $casts = ['content' => 'array'];

Where to look first, in order:

  1. README.md — the only doc you get.
  2. src/*.php — find the Field subclass; its component() string tells you which Vue file is loaded.
  3. resources/js/components/ — read the block/layout rendering component. This is where you'll learn the real shape of the JSON it produces, which is what your Blade templates must consume.
  4. composer.json — Nova version constraint.

First use case: a Page model with repeatable layout blocks.

use Gwd\FlexibleContent\FlexibleContent; // verify namespace

FlexibleContent::make('Content')
    ->addLayout('Text', 'text', [
        Text::make('Heading'),
        Textarea::make('Body'),
    ])
    ->addLayout('Image + Text', 'image_text', [
        Image::make('Image', 'image_disk_path'),
        Textarea::make('Caption'),
    ]);

Then render:

@foreach ($page->content as $block)
    @includeIf("partials.blocks.{$block['layout']}", ['block' => $block])
@endforeach

The layout key (or whatever the package names it — confirm by saving one block and dd()-ing the column) is your dispatch key.

Implementation Patterns

Keep layout definitions DRY across resources. Don't paste the same ->addLayout(...) chain into a Page resource, a Post resource, and a Landing resource. Extract a static method:

class ContentBlocks
{
    public static function make(string $attribute = 'Content'): FlexibleContent
    {
        return FlexibleContent::make($attribute)
            ->addLayout('Text', 'text', [...])
            ->addLayout('Hero', 'hero', [...])
            ->addLayout('Gallery', 'gallery', [...]);
    }
}

Now a schema change is one edit, and every resource follows along.

Mirror layouts on the frontend with a component map, not @includeIf alone. @includeIf silently swallows typos, so a missing partial renders nothing and you ship a blank page. Prefer an explicit map:

// config/block-views.php
return [
    'text'      => 'blocks.text',
    'hero'      => 'blocks.hero',
    'gallery'   => 'blocks.gallery',
];
@foreach ($page->content ?? [] as $block)
    @php $view = config("block-views.{$block['layout']}"); @endphp
    @if ($view && View::exists($view))
        @include($view, ['block' => $block])
    @endif
@endforeach

This gives you graceful degradation for orphaned layouts — critical when you remove a layout from the Nova field but old rows still hold that block type.

Model helper for block access. Add a blocks() accessor that normalizes the payload so templates don't need to know about the package's internal wrapper keys:

public function blocks(): Collection
{
    return collect($this->content ?? [])
        ->filter(fn ($b) => isset($b['layout']))
        ->values();
}

Seeding and fixtures. Flexible content is awkward to seed by hand. Build the array shape once by saving through the Nova form, then copy the resulting JSON into your factory:

'content' => [
    ['layout' => 'text', 'attributes' => ['heading' => 'Hi', 'body' => '...']],
],

Write a factory state per layout (->withHero(), ->withGallery()); you'll reuse them constantly in feature tests that assert a block renders.

Validation of nested fields. Nova validates sub-fields inside a flexible content field inconsistently depending on the package version. If nested rules aren't firing, add a custom rule at the model level that iterates blocks and validates each attributes array with Validator::make(). This is more reliable than trusting field-level ->rules() inside a nested layout.

Media inside blocks. Store image paths as plain strings in the block attributes and resolve them via Storage::url() at render time — do not rely on the package's Image field resolution unless you've tested both the create and update flows. Nested media fields are the single most common source of orphaned files and stale paths in flexible-content implementations.

Content migration. When you add a layout, old rows are unaffected — they simply don't contain it. When you rename a layout key, write a one-off artisan command:

Page::whereJsonContains('content', ['layout' => 'old_key'])
    ->each(fn ($p) => $p->update(['content' => /* remap */]));

Run it in a migration or a guarded command, never ad-hoc in tinker on production.

Gotchas and Tips

The maintenance risk is the headline risk. One star and zero dependents means: no issue triage, no PR review, and no fix arriving when Nova ships a breaking change. Mitigate by forking the repo into your VCS, pinning the commit hash in composer.json, and budgeting a day to maintain it yourself. Prefer a path or VCS repository entry over Packagist so a surprise deletion upstream doesn't break composer install in CI.

Nova 5 / Vue 3 incompatibility is the most likely failure mode. If the field appears blank in the admin UI with a Vue is not defined or a silent console error, that's the cause. There is no fix short of porting the components. Check nova:version against the package's composer.json before you spend time debugging anything else.

Rebuild assets if the UI doesn't update. If you edit the package's Vue files (or the fork), run npm run prod inside the package directory and php artisan nova:publish. Nova caches the compiled field JS aggressively; a stale dist/field.js will make you think your changes aren't taking effect.

JSON column vs TEXT. $table->json() gets you whereJsonContains / whereJsonLength and MySQL-side validation. Use it. On MariaDB or older MySQL, JSON is aliased to LONGTEXT and you lose the operators — then you're back to LIKE '%"layout":"hero"%', which is fragile and unindexable.

Don't query by string matching on block content. For "find all pages with a hero block," use whereJsonContains('content', ['layout' => 'hero']). If that's a common query, denormalize: a has_hero boolean or a block_types JSON column maintained in a model observer. Full-text search over serialized block JSON is a trap.

Nested field quirks. Fields inside layouts don't always respect ->hideFromIndex(), ->sortable(), or ->rules() the way top-level Nova fields do — the package is rendering them in its own Vue context, not Nova's standard field wrapper. Test index and detail views explicitly; don't assume create/update working implies the rest works.

Orphaned block types break rendering, not saving. Nova will happily save a page containing a layout you've since removed from the PHP definition. Your frontend must handle unknown layout values — hence the explicit view map plus View::exists() guard above. Add a test: seed a block with layout => 'nonexistent' and assert the page still returns 200.

Debugging checklist, in order:

  1. Save one block, then dd(Page::find($id)->getRawOriginal('content')) — confirm the actual JSON key names (layout vs type vs block).
  2. Browser console on the Nova edit screen — Vue errors surface here, not in laravel.log.
  3. php artisan nova:publish — fixes a surprising number of "field not showing" reports.
  4. Confirm the field's component() name matches a file registration in resources/js/field.js.

Extension points to know before forking. The Field class's component() method, the Vue block component in resources/js/components/, and any service provider bindings are the three seams. If you need custom layout previews, conditional layouts (e.g., show "Gallery" only when a page type is selected), or collapsible block reordering, you'll likely be editing the Vue component directly rather than using a documented API.

Escape hatch. If you hit a wall, migrating to whitecube/nova-flexible-content is usually feasible because both store arrays of {layout, attributes}-shaped blocks in a JSON column. Write the migration command first, keep the Blade templates identical, and only then swap the field definition — the frontend shouldn't need to change at all.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource