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

outl1ne/nova-page-manager

A Laravel Nova package for creating and managing pages with flexible, block-based content. Build dynamic page layouts, edit SEO fields, and organize site content from a clean Nova interface, ideal for marketing sites, CMS-driven apps, and any project needing structured pages without a full CMS.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (well under an hour):

composer require outl1ne/nova-page-manager
php artisan vendor:publish --tag=nova-page-manager-config
php artisan vendor:publish --tag=nova-page-manager-migrations
php artisan migrate

Publish per tag rather than all-at-once, so a later vendor:publish doesn't overwrite views or config you've customized. Then register the package in app/Providers/NovaServiceProvider.php — follow the README of your pinned version, since this is the step that has historically changed between majors (a NovaPageManager::make() entry in tools() in some versions, explicit Page/Region resource registration in resources() in others).

Where to look first, in order:

  1. config/nova-page-manager.php — the most important file in the package. Page/Region model bindings, route registration, locale handling, and view resolution all live here. Almost every customization starts with a config edit.
  2. The published migrations — read the actual schema. Whether region content is a single opaque column or structured JSON determines whether you extend via config or fork.
  3. vendor/outl1ne/nova-page-manager/src/ — the package is thin. Twenty minutes of reading answers most "does it support X" questions faster than the docs do.

First use case — a marketing page end to end:

  1. Nova → Pages → create Home, slug home, choose a page template.
  2. Nova → Regions → create a hero region with the fields your template expects.
  3. Attach the region to the page from the Page resource; drag to order.
  4. Hit the frontend. With the package's catch-all route enabled, GET /home renders the default Blade view — confirm the wiring works before writing any application code.
  5. Only then swap in your own layout and take over routing.

Day-one decision: does the package own the public route, or does your app? Start with the package route for the tightest feedback loop while you validate the data model; take routing back the moment you need middleware, layouts, or caching. Most teams end up owning it.

Implementation Patterns

Pattern 1 — package routing as scaffolding. The catch-all /{slug} route gives instant "create a page, see it in the browser" feedback. Perfect for about, terms, privacy. Disable it once you need auth-gated content, per-section middleware, or a real cache layer.

Pattern 2 — app-owned routing (what you ship). Turn the package route off and resolve pages yourself:

Route::get('/{slug}', function (string $slug) {
    $page = Page::where('slug', $slug)
        ->where('published', true)       // add this yourself — see Gotchas
        ->with('pageRegions.region')     // always eager-load
        ->firstOrFail();

    return view('pages.show', compact('page'));
})->where('slug', '.*');

Eager-loading pageRegions.region is non-negotiable. N+1s here are invisible until a page has twenty regions, then they're everywhere.

Pattern 3 — one map from region type to view. Never scatter @includeIf strings across templates. Keep the mapping in config so a new block type requires no Blade edits:

// config/regions.php
return [
    'hero'       => 'regions.hero',
    'rich-text'  => 'regions.rich-text',
    'footer-cta' => 'regions.footer-cta',
];
@foreach ($page->pageRegions as $pageRegion)
    @php($view = config("regions.{$pageRegion->region->type}"))
    @if ($view && view()->exists($view))
        @include($view, ['region' => $pageRegion])
    @endif
@endforeach

The guard matters: an unknown type should degrade to nothing, not 500 the page.

Pattern 4 — regions as a shared component library. Regions are first-class and reusable, which is the real day-to-day win over hand-rolled CRUD. A footer-cta or sidebar edited once appears on many pages; you maintain one Blade partial per region type. Create the region in the Region resource first, then attach — not the other way around.

Pattern 5 — bind your own models via config. Need extra columns, casts, accessors, or relationships? Don't subclass inline — point the config at your models:

// config/nova-page-manager.php
'page_model'   => App\Models\Page::class,
'region_model' => App\Models\Region::class,

Keep your Page extending the package model so the Nova resource keeps functioning. This is the cheapest extension point; exhaust it before considering a fork.

Pattern 6 — cache the read path, bust on save. The DB is never the bottleneck at hundreds-to-low-thousands of pages, but you don't want the public site touching these tables per request:

// App\Observers\PageObserver
public function saved(Page $page): void
{
    Cache::forget("page:{$page->slug}");
    if ($page->wasChanged('slug')) {
        Cache::forget('page:'.$page->getOriginal('slug'));  // don't forget the old one
    }
}

Render with Cache::remember("page:{$slug}", 3600, fn () => view('pages.show', ...)->render()) and register the observer in a service provider — the package does none of this.

Pattern 7 — add a published flag before anyone else touches it. There is no publish workflow. Add the column via your own migration, filter on it in the resolver, expose a Nova toggle, and (if you want scheduling) a published_at. Add a signed preview route that bypasses the filter — editors will ask in week two.

Pattern 8 — seed content for local dev. Pages authored in staging don't exist in a fresh clone. Seed a minimal page set (one page per template) or take a DB dump, or every new developer sees 404s and assumes the install is broken.

Pattern 9 — brownfield migration. If you already have pages/content_blocks, write a one-off mapping migration (slug/title/body → page + region rows), keep the legacy table read-only for one release as a rollback path, and cut over when the renderer matches. Content is cheap to move early and expensive after editors have invested in it.

Pattern 10 — localization is a data concern, not a package feature. The package supports per-locale content, but routing locale into the resolver is yours. Keep the locale in the URL prefix or a session key, pass it into every query, and define a fallback locale so a half-translated page doesn't render empty regions.

Gotchas and Tips

Saves go live instantly. No drafts, no revisions. This is the biggest operational surprise for a non-technical team — add published before handover, or unreviewed copy ships.

Sanitize rich content on render, not just on save. Whatever field holds HTML will eventually carry a <script> — a paste, a compromised account, a code-view toggle. Purify at render time; save-time-only leaves historical content dirty.

Slug changes silently break URLs and caches. Enforce uniqueness in validation, write a 301 on change, and bust the cache for the old slug too:

public function updating(Page $page): void
{
    if ($page->isDirty('slug')) {
        Redirect::create([
            'from' => '/'.$page->getOriginal('slug'),
            'to'   => '/'.$page->slug,
            'code' => 301,
        ]);
    }
}

Region-type → view mismatches fail blank, not loudly. A missing view renders an empty gap and the editor reports "the page is broken." Guard with view()->exists() and, outside production, render a visible placeholder naming the missing view.

Missing region = blank section, always. Decide a fallback per type (empty-state copy, default image) inside the partial. Editors detach regions to "test something" and forget to reattach.

Watch the catch-all route. /{slug} will shadow or be shadowed by your routes depending on registration order. If GET /login starts returning a page-manager 404, that's why. Owning routing explicitly deletes this whole class of bug.

Config caching hides your changes. php artisan config:cache locally, or a forgotten config:clear after editing nova-page-manager.php, makes model bindings look inert. When a binding "isn't working," clear config first.

Extend additively; don't rewrite packaged migrations. Adding columns to pages via your own migration is fine, but a future package migration altering the same table can collide. Keep your schema deltas isolated.

Pin the exact version and record the constraint matrix. Laravel / PHP / Nova versions in your README, plus a CI job that installs against the pinned Nova version — a breaking Nova major should fail in CI, not in the editor's admin panel. A changelog line like 6.0.3's "Fixed saving with MergeValue inputs" is exactly the kind of bug that only surfaces when someone hits Save on a Flexible Content field; know whether your version has it.

Pre-agree the fork trigger. Nova ships a major roughly annually and third-party compatibility lags. The decision to make now is not "will we fork" but "at what point" — e.g. if compatibility trails by more than one release cycle, vendor into packages/ and own it. MIT licensing makes that cheap; deciding under pressure makes it expensive.

Nova form performance degrades with many regions per page. Test with a realistic page (20+ regions) before committing. The bottleneck, if any, is the editor form, not the database. If it's slow, split long pages across templates rather than piling more fields into one region.

Write the editorial process down. Ramp-up is asymmetric: a Laravel dev is productive in hours, editors need fifteen minutes of Nova training — but with no drafts or revisions, process documentation matters more than tooling. Record who publishes and how.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
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