kreetancraft / laravel-site-settings
Application settings, navigation menus & multi-platform review aggregator for Laravel — Livewire 4 + Flux UI, Google Tag Manager, Spatie Settings, Google/Facebook/TripAdvisor reviews & public API.
Package info
github.com/Kreetancraft/laravel-site-settings
pkg:composer/kreetancraft/laravel-site-settings
Requires
- php: ^8.2
- laravel/framework: ^12.0|^13.0
- livewire/flux: ^2.0
- livewire/livewire: ^4.0
- lorisleiva/laravel-actions: ^2.7
- spatie/eloquent-sortable: ^4.0|^5.0
- spatie/laravel-settings: ^3.4|^4.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^2.0|^3.0|^4.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0|^4.0
- spatie/laravel-permission: ^6.0
README
Application settings, a header mega-menu and footer link builder, and Google Tag Manager. Livewire 4 + Flux UI.
Ships no layout, no CSS, no image handling and no user model — it renders into your application and knows nothing about your content types until you tell it.
Design decisions worth knowing before you install
It knows nothing about what you can link to. Menu items point at a custom URL, or at a model
you register through MenuLinkRegistry / site-settings.linkables. The shipped config names the
kreetancraft/laravel-blog models; on a host without that package those types simply offer no
rows rather than failing.
It names no permission of its own. The screens ask the ordinary authorization question and
SettingsPolicy answers it. Until permissions exist anywhere in the app it is open, so it works
on a bare install rather than failing closed.
It ships no image handling. A mega panel and its categories can carry a promo image, resolved
through site-settings.image_resolver when one is configured. With none, the picker does not
render at all — a control that silently saves nothing is worse than no control.
It appears in your sidebar on its own if you have kreetancraft/laravel-user-management,
through the admin.navigation container tag — both screens under one Site heading, renamable
via site-settings.navigation.group. Without that package nothing reads the tag and nothing is
built.
Flux is a hard dependency. The views use <flux:*> throughout.
Installation
composer require kreetancraft/laravel-site-settings
php artisan migrate
php artisan vendor:publish --tag=site-settings-config
Tailwind v4 does not scan vendor/. In resources/css/app.css:
@source '../../vendor/kreetancraft/laravel-site-settings/resources/views';
Two screens are registered: admin.settings and admin.navigation, both behind
can:view-settings.
The navigation builder
One builder, three locations, each with its own depth cap:
| Location | Depth | What the levels mean |
|---|---|---|
header |
3 | nav item → panel category → link |
footer_primary |
2 | heading → link |
footer_secondary |
2 | heading → link |
A top-level header item is a plain link, a dropdown, or a mega panel. A dropdown or panel trigger stores no URL — it opens something rather than navigating — and the form hides the link fields accordingly. A panel category may have no link at all, in which case it renders as a heading.
Depth is enforced on write by MenuItemGuard, including on a move: dragging a subtree under a
new parent is refused when the deepest child would land past the cap. Violations surface as
validation errors on parent_id, not exceptions.
Reordering uses Livewire 4's wire:sort, which reports only the item that moved and the index it
landed on. The component rebuilds the order from that item's own siblings, scoped by
(menu_id, parent_id).
Linkable types
// config/site-settings.php 'linkables' => [ 'page' => [ 'label' => 'Page', 'model' => \App\Models\Page::class, 'display_column' => 'title', 'path_template' => '/{slug}', ], ],
Or at runtime, from a package's own service provider:
MenuLinkRegistry::register('event', 'Event', Event::class, 'title', '/events/{slug}');
MenuItem::resolvedUrl() fills {slug} from the linked model, and falls back to the stored url
when the model is gone.
Permissions
SettingsPolicy declares PERMISSION_SUBJECT = 'setting' and manage as an extra ability, so
kreetancraft/laravel-user-management discovers it and creates view-settings,
create-settings, update-settings, delete-settings and manage-settings on
php artisan user-management:sync-permissions.
Reads are gated on view-settings, every write on manage-settings. Both are also registered as
plain gates, because the screens and the route middleware ask for abilities by name rather than
for a policy on a model — there is no row here that an administrator thinks of as the subject.
Define either ability yourself before this package boots and yours wins.
Images
// config/site-settings.php 'image_resolver' => \Kreetancraft\Media\Support\MediaImageResolver::class, 'media_picker_view' => 'media::picker-field', 'media_modal_view' => 'media::picker-modal',
The three move together. The resolver is duck-typed — it needs urlFor, listFor and syncFor,
and nothing imports an interface across packages — so you can point it at your own class. An
optional preload($models, $collection) is used to warm a whole menu in one query.
The saved image comes back as featured_image_url on every node of the API tree, and is null
with no resolver configured.
Maintenance mode
EnsureSiteIsLive answers 503 with the configured message: JSON for API requests, the
site-settings::maintenance view otherwise. Anyone holding view-settings still gets through,
so whoever turned it on can reach the screen that turns it off.
The API
site-settings.routes.api (default prefix api/v1):
| Route | Returns |
|---|---|
GET site |
settings, menus keyed by location, and JSON-LD |
GET site/stats |
counters from stats_resolver |
GET menus/{location} |
one menu tree |
The aggregate is cached and flushed on every write; set site-settings.cache.enabled to false
to bypass it entirely. stats_resolver and schema_resolver are
host callbacks — this package has no idea what you count or what schema you want.
Nothing here calls out to the network, and a test asserts it: rendering the settings screen
with Http::preventStrayRequests() must stay green. Reviews — ingestion and trust badges alike —
live in their own package.
Google Tag Manager
<head>@gtmHead</head> <body>@gtmBody</body>
Both render nothing until a container ID is saved.
Requirements
PHP 8.2+, Laravel 12/13, Livewire 4, Flux 2, spatie/laravel-settings,
spatie/eloquent-sortable.
Testing
composer test
License
MIT.