Give your plugin admin screens and public pages that look and behave like the rest of TruCMS: the right middleware, the right layout component, breadcrumbs, SEO and theme-overridable views.
A plugin never touches routes/*.php of the core. It has its own RouteServiceProvider (listed in the main provider's $providers) that wraps each route file in a middleware stack. Keep admin and public routes in separate files so each gets the right theme:
Modules/Booking/app/Providers/RouteServiceProvider.php (trimmed)
public function map(): void
{
Route::middleware(['web', 'auth', AdminTheme::class]) // admin
->as('admin.booking.')
->prefix(config('dotartisan.admin_prefix', 'admin') . '/bookings')
->group(module_path($this->name, '/routes/admin.php'));
Route::middleware(['web', FrontTheme::class]) // public
->as('booking.')
->prefix(config('booking.front_prefix', 'booking'))
->group(module_path($this->name, '/routes/front.php'));
}| Audience | Middleware | Name prefix / URL prefix |
|---|---|---|
| Admin (same as core) | web, auth, verified, 2fa, AdminTheme (+ permission:access {alias} for a whole area) | admin.{alias}. / {admin_prefix}/{alias} |
| Public pages | web, FrontTheme (+ doCache for cacheable pages) | {alias}. / {alias} (also declare it in public_paths) |
| Signed-in visitors | web, FrontTheme, TwoFactorMiddleware, auth, verified | {alias}.account. / user/… or {alias}/account |
| Staff outside the admin | as above + permission:access {alias} staff cabinet | Booking uses staff/bookings |
| Webhooks | web (no theme) with ->withoutMiddleware(PreventRequestForgery::class) | {alias}.webhook — the global CSRF exception only covers install/* and webhooks/* |
| JSON API | api, auth:sanctum | api.{alias}. / api/{alias} (TruCommerce) or v1/… |
admin.{alias}.{resource}.{action}; the demo write-guard and the breadcrumb/menu conventions rely on it."public_paths": ["myalias"] in module.json an editor could create a CMS page called /myalias that collides with your route; the declared segment is added to ReservedSlugRegistry.{slug} route is registered last, so your routes always win once their first segment is reserved.{model:slug} for slug lookups on HasSlug models.php artisan route:clear (a cached route file hides changes).Core public routes are wrapped in Route::localized(function () { ... }) (codezero/laravel-localized-routes), producing /ar/about. Bundled business modules do not localize their routes (URLs like /booking and /shop carry no prefix); the active locale comes from the session/cookie via SetLocale and FrontTheme. Decide per plugin; if you localize, build links with route() only.
App\Http\Controllers\Controller. Keep controllers thin; put logic in services under Modules/{Name}/app/Services.authorize() returns true.view('myalias::admin.index', [...]).->with('success', __('...')) / ->with('error', ...); the layouts render them.setting('admin_pagination', 10) in admin and ->withQueryString().| Component | Renders | Use for |
|---|---|---|
<x-admin-layout> | layouts.app (admin theme) | Every admin screen |
<x-admin-guest> | layouts.guest (admin) | Admin login/reset |
<x-front-layout> | layouts.app-layout (Echo) | Public pages with header, breadcrumbs, footer |
<x-front-fullwidth-layout> | layouts.fullwidth-layout | Edge-to-edge public pages |
<x-front-guest> | layouts.guest-layout | Login/registration style pages |
Modules/Playground/resources/views/front/index.blade.php
<x-front-layout>
<div class="container py-5"><h1>Hello from a plugin</h1></div>
</x-front-layout>Props and slots of <x-front-layout>:
:breadcrumbs="false" hides the breadcrumb bar; :footer="false" hides the footer. Extra attributes (class="container") are merged onto the .page-content wrapper.<x-slot:sidebar> renders a right column next to the content — put a widget area there: @widgetGroup('post-sidebar') (see Widgets).@push into: page_header (head), template_styles (after the theme stylesheet), page_scripts (end of body), body_portals (before scripts). Page-builder shortcode assets are injected by <x-page-builder-assets> automatically.override_theme_options($page->settings) if your page is backed by a CMS page).Wrap the layout in your own component when your plugin always needs the same assets (Booking's <x-booking::front.layout> pushes its Vite assets once and offers a sidebarGroup prop).
use App\Facades\Meta;
Meta::setTitle('Playground page');
Meta::setDescription('A page served by a plugin.');
Meta::setMeta($model); // macro: title, description, canonical, Open Graph, Twitter card from a model
// (needs title/meta_title, meta_description, optional 'og-image' media)routes/breadcrumbs.php
Breadcrumbs::for('playground.index', function (BreadcrumbTrail $trail): void {
$trail->parent('front'); // "Home"
$trail->push('Playground', route('playground.index'));
});The public layout renders breadcrumbs for the current route name, so a named public route without a definition simply shows none (the front breadcrumbs are lenient); the admin ones are not.
Booking and TruCommerce register page types ("Shop page", "Booking services") that let an administrator assign a designed CMS page to a module route. The module controller then redirects to that page when one is assigned:
php (pattern from BookingPageRedirector)
$target = AppPages::selectedPageUrl('booking.services'); // URL of the chosen CMS page, or null
if ($target !== null && rtrim($target, '/') !== rtrim(url()->current(), '/')) {
return redirect()->to($target, 301);
}resources/views and are addressed as alias::path.to.view.resources/views/components/foo.blade.php are used as <x-alias::foo />; class components in app/View/Components as <x-alias::foo-bar /> (namespace registered automatically). To expose a class component under a plain tag (needed for shortcode tags) register it: Blade::component('shortcode-foo', Foo::class).resources/themes/{theme}/vendor/{alias}/{same/path}.blade.php (for example resources/themes/echo/vendor/testimonials/components/testimonials.blade.php). A site can also publish views to resources/views/modules/{alias}.@extends('widgets.template') for widget editors, <x-input-error>, <x-text-input>, etc., resolved from the active theme's components/ folder.@lang('alias::file.key') / __().See Package, install and update (distributable plugins) and Build and assets (in-repo modules). In short: pre-built files published to public/modules/{alias} and referenced with asset(), or Vite entries added to the root vite.config.js and loaded with @vite() inside the page_header/page_scripts stacks.
<meta name="csrf-token"> is in both layouts; axios in the admin bundle adds it)./api/v1/...) behind auth:sanctum.->middleware('throttle:10,1') (the core limiter names: api, page-builder-ai).payments/webhook/{gateway}.