Browse the documentation
Building modules
Assets & frontend
Ship CSS and JavaScript from a module and have the core load them: where assets live, how they are published, and how they are referenced.
A module can bring its own CSS and JavaScript. The application loads these files automatically during the boot process, with no core files to touch. This page traces the path from a compiled file to the <link> or <script> tag in the finished HTML — and explains why most modules can get by without any custom CSS at all.
How module assets are loaded
The process consists of five steps and runs automatically when the module boots:
- The module places compiled CSS/JS in its
dist/directory. - A
dist/manifest.jsonlists those files. - The
ModuleAssetRegistryreads the manifest during boot. - A symlink is created:
public/modules/{slug}/→modules/{slug}/dist/. - The
@moduleAssetsBlade directive outputs the<link>and<script>tags.
The reason for this symlink-and-manifest approach: asset files stay inside the module directory but become reachable under the public public/ path. Each module remains self-contained, and the core does not need to know which files a module brings — it reads them from the manifest.
Directory structure
modules/my-module/
dist/
manifest.json
my-module-abc123.css
my-module-abc123.js
manifest.json
The manifest is a plain list of files to serve:
[
{"type": "css", "file": "my-module-abc123.css"},
{"type": "js", "file": "my-module-abc123.js"}
]
Each entry needs exactly two fields:
type: either"css"or"js"file: filename within thedist/directory
The hash suffix in the filename (e.g. abc123) is recommended because it handles cache busting: when the content changes, the filename changes, and browsers fetch the new file rather than a stale cached version.
Asset URLs
Assets are served from /modules/{slug}/{file}:
/modules/my-module/my-module-abc123.css
/modules/my-module/my-module-abc123.js
Blade directives
@moduleAssets
This directive outputs all registered module CSS and JS. It is already included in the core layouts — you do not need to add it yourself.
The output it generates:
<link rel="stylesheet" href="/modules/my-module/my-module-abc123.css">
<script src="/modules/my-module/my-module-abc123.js" defer></script>
module_asset() helper
To reference an individual asset file within a view, module_asset() returns the correct path:
module_asset('my-module', 'my-module-abc123.css');
// Returns: '/modules/my-module/my-module-abc123.css'
@extensionSlot / @lifecycleFields
Two further core directives let modules inject markup into existing pages rather than ship a whole page of their own:
@extensionSlot('admin.dashboard.top') {{-- render a template slot --}}
@lifecycleFields(\App\Enums\LifecyclePoint::JobEnd) {{-- render driver lifecycle fields --}}
@extensionSlot renders everything modules appended or replaced through the SlotRegistry (see Template slots). @lifecycleFields renders the form fields modules contributed through the LifecycleFieldRegistry for that lifecycle point (see Registries reference). The key thing to understand: both directives are already placed in the core layouts and forms. A module registers content for them — it never calls these directives itself.
Building assets
The core application uses Tailwind CSS. For your own assets there are three approaches — from simplest to most involved.
Option A: Plain CSS (simplest)
Write CSS in a .css file and place it in dist/. Nothing more is needed.
Option B: Build with Vite/Webpack
If your module has more complex frontend requirements, build assets with a bundler. The manifest is then generated by the build process:
modules/my-module/
resources/
css/
module.css
js/
module.js
vite.config.js (or webpack.config.js)
package.json
dist/
manifest.json (auto-generated by build)
my-module-abc123.css
my-module-abc123.js
Option C: Tailwind classes
Because the core already loads Tailwind, a module’s Blade views can use Tailwind classes directly — with no extra CSS at all:
<div class="bg-white rounded-lg shadow p-4 mb-4">
<h3 class="text-sm font-medium text-gray-500">{{ $label }}</h3>
<p class="mt-2 text-lg">{{ $value }}</p>
</div>
For most module views this is the recommended approach: no custom CSS, no build step, no manifest required.
Symlink management
The ModuleManager creates the symlink automatically:
public/modules/my-module → modules/my-module/dist
You do not need to set it manually. The rules for symlinks are:
- Created when a module is enabled or booted.
- Removed when a module is disabled.
- The parent directory
public/modules/is created automatically if it does not exist.
Example: widget view with Tailwind
A complete, runnable widget — built entirely with Tailwind classes, without a single line of custom CSS:
{{-- resources/views/widgets/status-card.blade.php --}}
<div class="bg-white rounded-lg shadow p-4">
<div class="flex items-center gap-3">
<div class="flex-shrink-0">
<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500">{{ $label }}</h3>
<p class="text-lg font-semibold text-gray-900">{{ $data['status'] ?? 'Unknown' }}</p>
</div>
</div>
</div>
How to register a widget like this on the dashboard is covered under Navigation & dashboard. The full API of the ModuleAssetRegistry and the other registries is documented under Registries.