Skip to main content

Views & Assets

This guide covers creating Blade templates, working with the Metronic theme, managing static assets via the public/ directory, and how symlinks make your assets web-accessible.

View Structure

Addon views are stored in Resources/views/:

Resources/views/
├── layouts/
│ └── master.blade.php # Optional: addon-specific layout
├── index.blade.php # Main page
├── settings.blade.php # Settings page
├── reports/
│ ├── overview.blade.php
│ └── details.blade.php
└── partials/
├── header.blade.php
└── stats-card.blade.php

Extending the Main Layout

Mumara Campaigns uses the Metronic theme. Extend the main layout:

{{-- Resources/views/index.blade.php --}}
@extends('layouts.master2')

@section('title', 'Email Validator')

@section('content')
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">

{{-- Page Header --}}
<div class="kt-subheader kt-grid__item" id="kt_subheader">
<div class="kt-container kt-container--fluid">
<div class="kt-subheader__main">
<h3 class="kt-subheader__title">Email Validator</h3>
<span class="kt-subheader__separator kt-subheader__separator--v"></span>
<span class="kt-subheader__desc">Validate and verify email addresses</span>
</div>
<div class="kt-subheader__toolbar">
<a href="{{ route('emailvalidator.validate.form') }}" class="btn btn-brand btn-elevate btn-icon-sm">
<i class="la la-plus"></i>
New Validation
</a>
</div>
</div>
</div>

{{-- Main Content --}}
<div class="kt-content kt-grid__item kt-grid__item--fluid">
<div class="kt-portlet">
<div class="kt-portlet__head">
<div class="kt-portlet__head-label">
<h3 class="kt-portlet__head-title">Recent Validations</h3>
</div>
</div>
<div class="kt-portlet__body">
@include('emailvalidator::partials.recent-table', ['results' => $recentResults])
</div>
</div>
</div>

</div>
@endsection

@section('scripts')
<script>
// Page-specific JavaScript
</script>
@endsection

View Namespacing

Access addon views using the module alias as namespace:

// In controllers
return view('emailvalidator::index');
return view('emailvalidator::settings.general');
return view('emailvalidator::partials.table');
{{-- In Blade templates --}}
@include('emailvalidator::partials.header')
@extends('emailvalidator::layouts.master')

Passing Data to Views

// Array syntax
return view('emailvalidator::index', [
'title' => 'Dashboard',
'stats' => $stats,
]);

// Using compact
return view('emailvalidator::index', compact('title', 'stats'));

// Using with()
return view('emailvalidator::index')
->with('title', 'Dashboard')
->with('stats', $stats);

View Composers

Share data across multiple views automatically:

// In service provider boot()
public function boot(): void
{
view()->composer('emailvalidator::*', function ($view) {
$view->with('addonVersion', config('emailvalidator.version'));
});
}

Metronic UI Components

Portlets (Cards)

<div class="kt-portlet">
<div class="kt-portlet__head">
<div class="kt-portlet__head-label">
<span class="kt-portlet__head-icon">
<i class="flaticon-email"></i>
</span>
<h3 class="kt-portlet__head-title">
Card Title
<small>Subtitle text</small>
</h3>
</div>
<div class="kt-portlet__head-toolbar">
<a href="#" class="btn btn-clean btn-sm btn-icon btn-icon-md">
<i class="la la-refresh"></i>
</a>
</div>
</div>
<div class="kt-portlet__body">
{{-- Card content --}}
</div>
<div class="kt-portlet__foot">
<div class="kt-form__actions">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>

Data Tables

<table class="table table-striped table-bordered table-hover" id="myTable">
<thead>
<tr>
<th>Email</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($results as $result)
<tr>
<td>{{ $result->email }}</td>
<td>
@if($result->status === 'valid')
<span class="kt-badge kt-badge--success kt-badge--inline">Valid</span>
@else
<span class="kt-badge kt-badge--danger kt-badge--inline">Invalid</span>
@endif
</td>
<td>
<a href="{{ route('emailvalidator.result', $result->id) }}"
class="btn btn-sm btn-clean btn-icon btn-icon-md" title="View">
<i class="la la-eye"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>

@push('scripts')
<script>
$('#myTable').DataTable({ responsive: true, order: [[1, 'desc']] });
</script>
@endpush

Alerts

@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<div class="alert-icon"><i class="flaticon-warning"></i></div>
<div class="alert-text">{{ session('success') }}</div>
<div class="alert-close">
<button type="button" class="close" data-dismiss="alert">
<span><i class="la la-close"></i></span>
</button>
</div>
</div>
@endif

Asset Management

The public/ Directory

Every addon has a public/ directory for static assets that need to be served by the web server:

Addons/YourAddonName/
└── public/
├── css/
│ ├── addon.css
│ └── settings.css
├── js/
│ ├── addon.js
│ └── charts.js
├── images/
│ ├── logo.png
│ └── icons/
│ └── feature.svg
├── fonts/
│ └── custom-font.woff2
└── plugins/
└── some-library/
├── lib.min.js
└── lib.min.css

When an addon is installed, the system creates a symlink so the browser can access the public/ directory:

public/Addons/YourAddonName/ → Addons/YourAddonName/public/

This means:

File on diskURL in browser
Addons/YourAddonName/public/css/addon.css/Addons/YourAddonName/css/addon.css
Addons/YourAddonName/public/js/addon.js/Addons/YourAddonName/js/addon.js
Addons/YourAddonName/public/images/logo.png/Addons/YourAddonName/images/logo.png

The symlink is created automatically during addon installation. On Linux/macOS this creates a standard symbolic link; on Windows it creates a junction (which works without administrator privileges).

To manually create the symlink:

# Linux / macOS
mkdir -p public/Addons
ln -s "$(pwd)/Addons/YourAddonName/public" public/Addons/YourAddonName

# Windows (Junction, no admin needed)
mklink /J public\Addons\YourAddonName Addons\YourAddonName\public
Why Symlinks?

The symlink approach keeps your addon self-contained -- all code and assets live under Addons/YourAddonName/. Only the public/ subdirectory is exposed to the web, so PHP source code, configuration, routes, and other sensitive files remain inaccessible from the browser.

When an addon is uninstalled, the symlink is removed:

# Linux / macOS
rm public/Addons/YourAddonName

# Windows
rmdir public\Addons\YourAddonName

Including Assets in Views

Reference your addon's public assets in Blade templates:

@section('styles')
{{-- From your addon's public/ directory --}}
<link href="/Addons/YourAddonName/css/addon.css" rel="stylesheet">
<link href="/Addons/YourAddonName/plugins/some-library/lib.min.css" rel="stylesheet">
@endsection

@section('scripts')
<script src="/Addons/YourAddonName/js/addon.js"></script>
<script src="/Addons/YourAddonName/plugins/some-library/lib.min.js"></script>
@endsection

For assets compiled by Laravel Mix into the main public/ directory:

@section('styles')
<link href="{{ asset('css/youraddonname.css') }}" rel="stylesheet">
@endsection

@section('scripts')
<script src="{{ asset('js/youraddonname.js') }}"></script>
@endsection

CSS url() References Within Assets

When writing CSS files that reference images or fonts, use paths relative to the web root since your assets are served from /Addons/YourAddonName/:

/* public/css/addon.css */

/* Reference images in your addon's public directory */
.logo {
background-image: url(/Addons/YourAddonName/images/logo.png);
}

/* Relative paths also work (relative to the CSS file location) */
.icon {
background-image: url(../images/icon.svg);
}

/* Reference fonts */
@font-face {
font-family: 'CustomFont';
src: url(../fonts/custom-font.woff2) format('woff2');
}
tip

Prefer relative paths (../images/icon.svg) over absolute paths (/Addons/YourAddonName/images/icon.svg) within CSS files. Relative paths continue to work even if the addon is renamed or the URL structure changes.

Source Assets & Compilation

Source files that need compilation (SASS, ES6+, TypeScript) live in Resources/assets/ and are compiled to public/:

Resources/assets/           → (compile) → public/
├── sass/app.scss → ├── css/addon.css
├── js/app.js → ├── js/addon.js
└── (not web-accessible) └── (web-accessible)

Laravel Mix Configuration

// webpack.mix.js
const mix = require('laravel-mix');

// Compile into your addon's public/ directory
mix.js(__dirname + '/Resources/assets/js/app.js', __dirname + '/public/js/addon.js');
mix.sass(__dirname + '/Resources/assets/sass/app.scss', __dirname + '/public/css/addon.css');

// Production versioning
if (mix.inProduction()) {
mix.version();
}

Building Assets

cd Addons/YourAddonName

# Install dependencies
npm install

# Development build
npm run dev

# Watch for changes
npm run watch

# Production build (minified)
npm run production

Static Assets (No Build Step)

If you don't need SASS or ES6 compilation, simply place your CSS and JavaScript files directly in public/:

public/
├── css/
│ └── addon.css # Hand-written CSS
├── js/
│ └── addon.js # Hand-written JavaScript
└── images/
└── logo.png

No build step required -- these files are served directly by the web server through the symlink.

AJAX Requests

@push('scripts')
<script>
$(document).ready(function() {
$('#validateBtn').on('click', function() {
var email = $('#emailInput').val();

$.ajax({
url: '{{ route("emailvalidator.validate.ajax") }}',
method: 'POST',
data: {
email: email,
_token: '{{ csrf_token() }}'
},
beforeSend: function() {
KTApp.blockPage({
overlayColor: '#000000',
opacity: 0.1,
state: 'primary',
message: 'Validating...'
});
},
success: function(response) {
KTApp.unblockPage();
if (response.success) {
toastr.success(response.message);
}
},
error: function(xhr) {
KTApp.unblockPage();
toastr.error('Validation failed. Please try again.');
}
});
});
});
</script>
@endpush

Modals

{{-- Trigger Button --}}
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#confirmModal">
Delete Selected
</button>

{{-- Modal --}}
<div class="modal fade" id="confirmModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the selected items?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDelete">Delete</button>
</div>
</div>
</div>
</div>

Publishing Views

Allow users to customize your addon's views by publishing them:

// In service provider
$this->publishes([
module_path($this->moduleName, 'Resources/views')
=> resource_path('views/modules/' . $this->moduleNameLower),
], 'views');

Published views in resources/views/modules/youraddonname/ take precedence over the addon's original views.

php artisan vendor:publish --tag=youraddonname-views