Skip to content

Nova ​

Introduction ​

kitloom supports Laravel Nova 5 through -nova packages built on the same domain packages as the Filament ones. A Nova site and a Filament site share content, settings and modules; only the admin differs.

Requirements: Laravel Nova 5 (licensed), Laravel 12 or 13. The Nova skeleton runs on Laravel 13 with Nova 5.11. Every kitloom package is commercial, the -nova packages as much as the -filament ones.

Inertia

Nova runs on Inertia, and so do Vue themes (kitloom/site-inertia). Both share the server package inertiajs/inertia-laravel: the themes require ^3.0, which Nova 5 accepts, so one install serves both.

Setting up Nova ​

Install Nova as usual. In NovaServiceProvider, let in whoever has something to do in it — a permission beyond reading the site (Kitloom\Permissions\AdminAccess). Customers and subscribers stay out, as WooCommerce keeps them out of wp-admin, and use their account on the site; a user without a role stays out too:

php
// app/Providers/NovaServiceProvider.php
protected function gate(): void
{
    Gate::define('viewNova', fn (User $user): bool => app(AdminAccess::class)->allows($user));
}

The Nova skeleton serves Nova at /nova (NOVA_PATH). Its NovaServiceProvider lists only the application's own dashboard and its Users tool (App\Nova\Tools\Users: All Users, Add New User, Profile and Roles); everything else comes from the packages.

While Nova serves a request, kitloom/wp-schema-nova switches the request to Visibility::everything(): the admin sees drafts, private posts and the trash, and the resources decide by capability what each row may have done to it.

How modules attach ​

A -nova package registers its resources and tools itself, inside Nova::serving(), and does nothing when Nova is not installed:

php
use Kitloom\WpSchema\Nova\Support\ResourceOverride;
use Laravel\Nova\Nova;

public function boot(): void
{
    if (! class_exists(Nova::class) || ! (bool) $this->app->make('config')->get('kitloom-redirects.nova.register', true)) {
        return;
    }

    Nova::serving(function (): void {
        $redirection = ResourceOverride::find(RedirectionResource::class, Nova::$resources);
        $notFoundLog = ResourceOverride::find(NotFoundLogResource::class, Nova::$resources);

        Nova::resources([$redirection, $notFoundLog]);
        Nova::tools([new Redirects($redirection, $notFoundLog, (bool) $this->app->make('config')->get('kitloom-redirects.nova.menu', true))]);
    });
}

kitloom-<name>.nova.register removes a package's admin entirely. A few packages — Redirections, Internal Links, Search & Replace, Import & Export — also have kitloom-<name>.nova.menu, which removes only their sidebar section, so the site can list the resources in its own menu. See Configuration.

Overriding a resource ​

To customise a kitloom resource, extend it and register your class in NovaServiceProvider. kitloom packages look for a subclass among the registered resources (ResourceOverride::find()) and register yours instead of their own — no two resources on one URL:

php
namespace App\Nova;

use Kitloom\Redirects\Nova\Resources\RedirectionResource as BaseResource;

class RedirectionResource extends BaseResource
{
    public static $perPageOptions = [50, 100];
}

The resource's model comes from the model registry, so swapping the model does not require overriding the resource.

The ready-made Posts, Pages, Categories and Tags work the same way: a subclass of BlogPostResource, PageResource, CategoryResource or TagResource replaces the package's. An application whose resources extend the base PostResource / TermTaxonomyResource instead turns the ready-made ones off with kitloom-wp-schema.nova.content.register.

Form tabs ​

Modules add tabs to other resources' forms — SEO, Custom Fields — through Kitloom\WpSchema\Nova\Forms\FormSection implementations tagged FormSections::TAG. A Nova form section says which resources it appliesTo(), returns its fields() and the metaKeys() it owns, and may prepare or save a value itself (prepareMetaValue(), saveMetaValue()); the resource writes those meta keys in the same pass as its own.

Resources that use the HasFormSections trait show them; every kitloom content resource does. Your own resource builds its form with withFormSections() — its fields become the first tab, Content, and the modules' tabs follow:

php
use Kitloom\WpSchema\Nova\Contracts\HtmlEditor;
use Kitloom\WpSchema\Nova\Forms\HasFormSections;
use Kitloom\WpSchema\Nova\Resources\PostResource;

class BookResource extends PostResource
{
    use HasFormSections;

    /**
     * Nova needs the model as a static property; a post type's model extends `Post`.
     */
    public static $model = Book::class;

    protected static function type(): string
    {
        return 'book';
    }

    public function fields(NovaRequest $request)
    {
        return [
            $this->withFormSections($request, [
                ...parent::fields($request),
                app(HtmlEditor::class)->field(__('Content'), 'post_content')->fullWidth()->hideFromIndex(),
            ]),
        ];
    }
}

Settings pages ​

kitloom/settings-nova provides Kitloom\Settings\Nova\SettingsTool: a sidebar section with settings pages. A module extends it, declares its pages in boot() with static::page() and links to them from menu() with static::pageUrl():

php
use Kitloom\Settings\Nova\SettingsTool;
use Kitloom\WpSchema\Nova\Menu\ModuleHelpItem;
use Laravel\Nova\Menu\MenuItem;
use Laravel\Nova\Menu\MenuSection;

class Reviews extends SettingsTool
{
    public function boot(): void
    {
        static::page('reviews', __('Reviews Settings'), fn () => [
            Boolean::make(__('Hold new reviews for moderation'), 'reviews_moderation'),
            Number::make(__('Reviews per page'), 'reviews_per_page'),
        ], ReviewsSettings::class);
    }

    public function menu(Request $request)
    {
        return MenuSection::make(__('Reviews'), array_values(array_filter([
            MenuItem::make(__('All Reviews'), '/resources/reviews'),
            MenuItem::link(__('Settings'), static::pageUrl('reviews'))
                ->canSee(fn (): bool => static::canSeePage('reviews')),
            ModuleHelpItem::make('acme/reviews'),
        ])))->collapsable();
    }
}

A page opens at /nova/settings/{page} (kitloom-settings-nova.path). Its fields are ordinary Nova fields — panels, tab groups and Rows included. A field without its own fillUsing saves into the option named by its attribute: a string as is, a yes/no as 1/0, an array as JSON; the values go through SettingWriter. The fourth argument of page() is what the Gate is asked about before the page opens or saves — declare a permission that covers it; without it the page needs manage_options.

Pages of the shared Settings section (Media, Reading, Discussion, Privacy, Cache) are Kitloom\WpSchema\Nova\Contracts\SiteSettingsPage implementations tagged SiteSettings::PAGE_TAG. A module's own settings go into its own section, as above.

The module's section ​

Each module has one sidebar section, named after the module: its screens, then Settings, then How it works last. Kitloom\WpSchema\Nova\Menu\ModuleHelpItem::make('vendor/name') is that last item — the module's README at /nova/modules/{vendor}/{name}, served by kitloom/modules-nova. It returns null when that package is not installed, the module has no README or KITLOOM_MODULES_MENU_HELP is false (see Enabling & Disabling), hence the array_filter().

Rows ​

Kitloom\WpSchema\Nova\Fields\Rows is a list of rows, each taking one of the field's layouts with that layout's fields — added, removed, moved and collapsed on the form. Modules use it for slides, repeaters and flexible-content custom fields, on resources and on settings pages:

php
use Kitloom\WpSchema\Nova\Fields\Rows;

Rows::make(__('Chapters'), 'chapters')
    ->layout('chapter', __('Chapter'), [
        Text::make(__('Title'), 'title'),
        Number::make(__('First page'), 'page'),
    ])
    ->button(__('Add Chapter'))
    ->max(50);

A row is ['layout' => name, 'key' => string, 'values' => [attribute => value]]. Without rows(get:, set:) the rows are read from the attribute — a list, as JSON or serialized — and written back as a list. layout() takes a $limit for that layout and readonly; collapsed() folds the rows that are there.

Revisions ​

A record's detail page has a Revisions tab while there is a saved version to go back to: each version (when, by whom, "AI agent" for an agent's) opens to show what restoring it would change. Restore a revision in the actions menu picks one and brings it back; the restore is saved as the newest version, so it can be undone the same way.

Visual editor ​

Post and page text and WYSIWYG custom fields take their editor from the Kitloom\WpSchema\Nova\Contracts\HtmlEditor contract — app(HtmlEditor::class)->field($label, $attribute). By default it is a code field with html highlighting, which saves the markup byte for byte. kitloom/visual-editor-nova binds TinyMCE 6 instead:

bash
composer require kitloom/visual-editor-nova

It keeps WordPress markup — block comments, shortcodes, attributes — and sends a text only after it was changed. With kitloom/media-nova the toolbar has Add Media, the library window. config/kitloom-visual-editor.php (shared with the Filament editor) sets the height, the site's styles and TinyMCE options; VISUAL_EDITOR=false brings the code field back. The script is built into the package, so installing it needs no build. To use another editor, bind HtmlEditor in your application's provider — see Extension Points › The HTML editor.

kitloom/media-nova adds the Media section — Library and Add New Media File — and Settings → Media (/nova/settings/media-settings). A library entry is the WordPress attachment itself, so files uploaded by WordPress or by the Filament admin are there too.

Every post form that takes PostResource's fields opens with Featured Image, stored as WordPress keeps it: the attachment id in _thumbnail_id, deleted when the field is emptied. The field — like image, file and gallery custom fields and category thumbnails — is the Kitloom\WpSchema\Nova\Contracts\AttachmentPicker. Without the media admin it is a searchable list of attachment titles; kitloom/media-nova binds the library window with search and uploads, and post lists then open each row with the featured image. kitloom-media.nova.picker set to false keeps the plain list.

For a field of your own, Kitloom\Media\Nova\Fields\MediaField picks files from the library: MediaField::make(__('Cover'), 'cover'), ->multiple() for several, ->allFiles() for more than images.

Two-factor sign-in ​

The Two-Factor Authentication module (kitloom/two-factor with kitloom/two-factor-nova) uses Nova's own two-factor screens, which are Laravel Fortify's; the application does not list Features::twoFactorAuthentication() — the package adds it. After the password, Nova asks for a code from an authenticator app or a recovery code. Users set it up on Nova's User Security screen. Two-Factor Authentication → Settings (/nova/settings/two-factor) picks the roles that must use it and resets another user's factors. The factors are user meta, not Fortify's users.two_factor_* columns.

The sidebar ​

Nova keeps one Nova::mainMenu callback. The Admin Menu module (kitloom/admin-menu with kitloom/admin-menu-nova) registers it and lets administrators arrange the sidebar from the admin itself, at /nova/admin-menu, without touching code:

  • reorder and nest entries by dragging, and move them between sections;
  • rename any section or entry, and set its icon (a Heroicon name);
  • hide what the site does not use — the page still opens by its address;
  • restrict a section or an entry to permissions, so each role sees a menu of its own;
  • add sections, groups and links of your own — to a page of the admin or to any URL — and delete them again;
  • reset to what the packages build.

The edits are applied on top of whatever the application and the packages build; see Admin Menu.

bash
composer require kitloom/admin-menu kitloom/admin-menu-nova
php artisan migrate

The edits live in the menu_overrides table, shared with the Filament screen; the screen is for those who may manage_options. Entries nobody placed land at kitloom-admin-menu.default_sort. An application with a Nova::mainMenu callback of its own sets kitloom-admin-menu.nova.register to false, calls AdminMenu::sort($menu, $request) from inside it and lists AdminMenuTool among its tools.

The Nova skeleton uses it, with migrations that arrange the sidebar in WordPress order: Dashboard, Posts, Media, Pages, Comments, Appearance (Themes, Menus, Admin Menu, Widgets), Modules, Users, Tools, Settings, then the plugin sections. Its config/kitloom-admin-menu.php sets default_sort to 0, so a newly installed module's section sinks to the bottom until someone places it.

What's where ​

SectionPackage
Posts (posts, categories, tags), Pages, Commentskitloom/wp-schema-nova
Settings → General, Writing, Permalinks, Widgets; the raw options editorkitloom/wp-schema-nova
Users → All Users, Add New User, Profilethe application (App\Nova\User, App\Nova\Tools\Users)
Users → Roles; the role field and filterkitloom/permissions-nova
Media → Library, Add New Media File; Settings → Media; the library as attachment pickerkitloom/media-nova
Menus, Menu Locationskitloom/navigation-nova
Appearance → Themes (/nova/themes); Settings → Reading, Privacy (/nova/settings/privacy), Cache (/nova/settings/cache)kitloom/site-nova
Tools → Shortcodes (a section of its own)kitloom/shortcodes-nova
Modules → Installed Modules (/nova/modules); the How it works pageskitloom/modules-nova
Admin Menu (/nova/admin-menu)kitloom/admin-menu-nova
Custom Fields → Post Types, Taxonomies; a section per typekitloom/post-types-nova
The visual editorkitloom/visual-editor-nova
Module sectionssee the catalog

Differences from Filament ​

Every module in the catalog has both a Filament and a Nova package, built on the same domain package. Where they differ:

FilamentNova
LicenceCommercial, as every kitloom packageCommercial, as every kitloom package
Menu orderMenuOrder, from the packages themselveskitloom/admin-menu-nova and the site's migrations
Module settingsSettingsPage subclass, at /admin/{slug}SettingsTool::page(), at /nova/settings/{page}
Blocks from modulesCollapsible sections under the formTabs after Content
Html editor by defaultA text areaA code field with highlighting
Two-factor methodsApp, email and recovery codesApp and recovery codes (email codes are Filament's only)
Custom post typesResource configurations, no classesA generated resource class per type

kitloom — modules and themes for Laravel.
Pricing · Services · License · Refunds · Privacy · Community · [email protected]