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

Nova Page Manager Laravel Package

optimistdigital/nova-page-manager

Built for Laravel Nova, this package lets you create, organize, and edit site pages with customizable templates, multilingual content, SEO fields, and slugs—all from the Nova admin panel. Includes block-based content editing, page structure control, and easy translation management.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the install identity before you write a line of code. This package has been published under two names: optimistdigital/nova-page-manager (namespace OptimistDigital\PageManager) and its current home outl1ne/nova-page-manager (namespace Outl1ne\PageManager). Most blog posts and Stack Overflow answers still reference the old one, so a copy-pasted import will produce Class "OptimistDigital\PageManager\..." not found in a project where the install is perfectly healthy. Run composer show | grep page-manager, note the namespace, and stick to it everywhere.

composer require outl1ne/nova-page-manager
php artisan vendor:publish --provider="Outl1ne\PageManager\PageManagerServiceProvider"
php artisan migrate

Prerequisites, checked in this order: a working Laravel Nova install (this is a Nova tool, not a standalone CMS), the Nova major version the package's composer.json supports, and the package's own PHP/Laravel constraints. If Nova isn't rendering other resources correctly, fix that first — every error you see afterwards will be misattributed.

Then read config/nova-page-manager.php immediately. This file is the real API. Templates, blocks, regions, locales, default locale, and the region resolver all live there. Everything you will do day-to-day is either editing this file or consuming what it produces. The published file also gives you the migrations for pages, page translations, and regions.

Register the two Nova resources so editors get a UI:

// app/Providers/NovaServiceProvider.php — boot()
Nova::resources([
    \Outl1ne\PageManager\Nova\Page::class,
    \Outl1ne\PageManager\Nova\Region::class,
]);

First use case: one page, end to end, in an afternoon. Declare one template with two blocks and one region:

// config/nova-page-manager.php (shape abbreviated — match your published version)
'locales' => ['en'],
'default_locale' => 'en',

'templates' => [
    'default' => [
        'label' => 'Default Page',
        'blocks' => [
            'hero' => [
                'title' => 'Hero',
                'fields' => [
                    \Laravel\Nova\Fields\Text::make('Heading'),
                    \Laravel\Nova\Fields\Image::make('Background'),
                ],
            ],
            'text' => [
                'title' => 'Text',
                'fields' => [
                    \Laravel\Nova\Fields\Textarea::make('Body'),
                ],
            ],
        ],
    ],
],

'regions' => [
    'header' => 'Header',
],

Create a page in Nova with slug about, template default, one hero and one text block, then publish it. The package deliberately does not route or render anything for you — that is the single most important thing to internalise on day one. You own the frontend:

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

// app/Http/Controllers/PageController.php
public function show(?string $slug = null)
{
    $page = Page::findBySlug($slug ?: '/');   // adjust to the resolver your version exposes
    abort_unless($page?->isPublished(), 404);

    return view('pages.show', compact('page'));
}
{{-- resources/views/pages/show.blade.php --}}
@extends('layouts.app')

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

Add resources/views/blocks/hero.blade.php and text.blade.php. When that loop renders real content, you have validated the whole chain: config → Nova editing → JSON storage → your Blade components. Do that spike before anyone commits a marketing site to this.

Implementation Patterns

Treat templates and blocks as a code-reviewed component library, not a page builder. Every block key in config should map 1:1 to a Blade partial under resources/views/blocks/. Adding a block type should be a single PR that touches three things — config, partial, test — and nothing else. Keep that directory as the canonical inventory, and add a test that walks every configured template's blocks and asserts the matching partial exists. Without it, @includeIf fails silently and you ship a page with invisible sections instead of an exception.

Never call the model from a view or route directly. Wrap it in one thin application-owned class. The package's real selling point is the exit ramp; a repository keeps the exit a one-file change:

// app/Support/Pages/PageRepository.php
class PageRepository
{
    public function findByPath(string $path, ?string $locale = null): ?Page
    {
        return Cache::remember(
            "page:{$locale}:{$path}",
            now()->addHour(),
            fn () => Page::findBySlug($path, $locale),
        );
    }
}

Resolve locale once in middleware, pass it in explicitly, and let the repository own published-state filtering and the 404. The controller stays four lines and every future content-delivery decision has one home.

Invalidate on save, never on deploy. Content lives in MySQL as structured JSON, so your deploy pipeline never touches it — which means a deploy cannot fix a stale cache. Register a model observer on saved and deleted that forgets page:* and region:*, plus the translation models. Otherwise "publishing didn't work" tickets are cache tickets.

Regions belong to the layout, resolved once per request. Resolve header/footer/promo slots in a View::composer('layouts.app', ...) scoped by locale, cached separately from pages. If your version's region resolver is page-aware, decide deliberately whether a region edit is global or per-page — mixing both in one site is a reliable support-ticket generator.

Ship preview before you ship anything else. Editors do not trust a draft/publish toggle they cannot see through. Add an authenticated preview route that skips the published check, gate it on Auth::guard('nova')->check() or a signed URL, and link it from the Nova resource. This is the highest-value twenty lines you will write on top of the package.

Emit SEO metadata once, in the layout. Put title/description/OG fields on the template (or a shared SEO block group), then output them from layouts.app with fallbacks to the page title and a site-wide default. Derive the canonical URL from the slug, not request()->url(), so it stays stable behind proxies and query strings.

Localization: fall back, don't fabricate. Look up the requested locale, then default_locale, then 404. Never fall back to a locale you never seeded — you will render empty pages that look published. And keep user-visible strings out of block partials; route them through translation files.

Migrate and back up with artisan commands. Content is plain JSON in your own tables, so php artisan pages:import export.json is straightforward. More importantly, write php artisan pages:export before you need it — that dump is both your backup and your migration path off the package.

Extend the Nova resources, never edit them. Subclass:

class Page extends \Outl1ne\PageManager\Nova\Page
{
    public function fields(NovaRequest $request): array
    {
        return [...parent::fields($request), /* your columns */];
    }
}

Register your subclass in NovaServiceProvider instead of the vendor class. Use the config's resolver/class hooks for region and page resolution. Anything under vendor/ is lost on the next composer update.

Gotchas and Tips

  • The namespace rename is the #1 day-one failure. Verify with composer show, pin the major version in composer.json, and grep the app for stale OptimistDigital\PageManager imports before debugging an install that was never broken.
  • Nova upgrade coupling is the structural risk. A Nova major can break resources, fields, and this package simultaneously. Pin both. Read both changelogs before either upgrade. Give Nova and this package one shared upgrade ticket, not two.
  • Test php artisan config:cache in staging before you depend on it. Templates place Nova field objects inside config arrays, and config caching serialises values into a PHP file. If your pinned versions don't survive that, you find out in a deploy. If it breaks, move template definitions into a service provider or a small registry class resolved at runtime.
  • Renaming a block or template key silently orphans stored content. The JSON keeps the old key, the page renders blank, and no error is raised. Version your keys (hero_v2) and write a one-off migration command when you rename — or accept the data loss knowingly.
  • Blocks are ordered arrays, not keyed maps. Reordering in Nova changes render order; don't write layout logic that assumes "the first block is always the hero." If layout depends on it, add an explicit variant field.
  • Draft pages return 404 and editors report it as a bug. Check the published flag — and any global scope your version applies — before digging into routing. php artisan tinker plus a raw dump of the JSON column settles it in thirty seconds.
  • Decide flat vs. nested slugs now. Moving a live site from about to /company/about is a redirect project, not a config change. Whatever you pick, add a redirects table or middleware while the site is small.
  • No media library is included. Pair it with outl1ne/nova-file-manager or spatie/laravel-medialibrary, and store relative paths in block fields rather than full CDN URLs — domains change, stored content shouldn't have to.
  • Clear the view cache when block partials change. Content survives deploys; Blade templates don't. A new partial rendering stale markup is one php artisan view:clear away, and it will look like a data bug.
  • Watch query counts on page lists. Eager-load translations when building menus or sitemaps; the per-locale translation relationship N+1s quietly at fifty pages.
  • Own the risk explicitly. Single maintainer, 179 stars, effectively no dependents. Pin the version, keep pages:export current, and keep block partials free of package-specific calls so a fork or migration stays a contained project rather than a re-platforming.
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.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings