backpack/basset
Backpack Basset is a Laravel asset manager for organizing, combining, minifying, and cache-busting CSS and JavaScript. It offers Blade helpers and simple configuration for loading assets in Backpack admin panels and frontends, improving performance and maintainability.
Install
composer require backpack/basset
The service provider is auto-discovered. Publish the config only if you need to change defaults:
php artisan vendor:publish --provider="Backpack\Basset\BassetServiceProvider"
Where to look first
README.md — the whole API surface is three Blade directives; it's a 10-minute read.config/basset.php — the only knobs you'll ever touch (cache location, dev-mode behaviour, disk/paths).src/BassetServiceProvider.php — see exactly which directives are registered and how the manager is bound.src/BassetManager.php — the registry that enforces "only once per page"; read this before debugging any "my asset didn't load" issue.First use case
You have a Blade partial (resources/views/components/date-picker.blade.php) that needs Flatpickr, and the partial can appear three times on one page — in a modal, in a sidebar, and inside a loop. Without Basset you get three <link> tags or a manual @once/@push dance. With Basset:
{{-- components/date-picker.blade.php --}}
@basset('https://cdn.jsdelivr.net/npm/[email protected]/dist/flatpickr.min.css')
@basset('https://cdn.jsdelivr.net/npm/[email protected]/dist/flatpickr.min.js')
<div x-data="datePicker()">...</div>
Include the partial three times, view source once: one <link>, one <script>. That is the entire value proposition — the "include it wherever it's needed, the framework dedupes" model. It's the asset equivalent of @once, but driven by the asset path rather than by template position, which makes it safe to use inside reusable components you don't control the inclusion count of.
Three-argument form for attributes
@basset('vendor/chartjs/chart.min.js', ['defer' => true, 'data-turbo-track' => 'reload'])
Attributes are applied to the tag that actually renders — which is the first occurrence on the page, not necessarily the one you wrote them on (see Gotchas).
Local, vendor, and absolute paths
@basset('resources/js/widgets/map.js') // resolves against base path
@basset(public_path('vendor/foo/foo.css')) // absolute path, most explicit
@basset('https://cdn.example.com/lib.js') // remote, cached locally in production
Prefer absolute helpers (public_path(), resource_path(), base_path()) over bare relative strings — the registry keys on the resolved string, so mixing forms for the same file defeats deduplication.
Inline blocks for component-scoped code
When a component needs a few lines of CSS/JS that don't justify a separate file, use the block directive instead of scattering <style>/<script> into partials:
@bassetBlock('date-picker-styles')
<style>
.flatpickr-calendar { font-family: inherit; }
</style>
@endBassetBlock
The identifier is the dedupe key. This is the cleanest way to ship a self-contained Blade component: markup, styles, and init script all in one file, still emitted exactly once.
Pair with Blade components, not layouts
The idiomatic pattern is: the component that uses the library declares it. Layouts should not be the place where you enumerate every third-party asset — that's what Basset exists to avoid, and it's how you end up with a 400KB layout for a page that uses none of it. Layouts keep only truly global CSS/JS.
Conditional loading of heavy libraries
Because declaration lives with usage, a page that renders no charts never loads Chart.js — no config, no @if in the layout, no route-based asset maps. This is the single biggest practical win over @push('scripts') in a layout.
Deployment integration
Basset distinguishes development (serve from source, editable without a build step) from production (serve from a generated cache). Add the cache-warming command to your deploy script between composer install and config:cache, e.g.:
php artisan basset:cache
Run php artisan list basset on your installed version to confirm exact command names — they've shifted across releases. Treat this step as mandatory: a deploy without it is the number one cause of "works locally, 404s in prod."
Backpack integration
If you're already in the Backpack ecosystem, Basset is what powers asset loading in v5/v6 CRUD views and widgets. Your custom widgets/fields should use @basset rather than @push, otherwise you'll get duplicate includes when two fields of the same type render on one form.
First occurrence wins — including its attributes. If the same asset is declared twice with different attributes, the second declaration is silently dropped. If you're debugging "why is defer missing," search the page for an earlier declaration of the same path, not the one you're looking at.
Deduplication is per-request, not per-browser-session. The registry is in-memory for a single request. Livewire/AJAX partial renders start a fresh registry, so a re-rendered component can re-emit tags it already emitted. For fragments injected over AJAX, either accept the duplicate (browsers cache it) or guard the markup with @once. Don't expect Basset to solve SPA asset management.
Path string identity matters. 'resources/js/foo.js', resource_path('js/foo.js'), and '/var/www/app/resources/js/foo.js' are three different registry keys pointing at one file. Standardise on helper functions in your codebase — this is the most common source of accidental double-loads.
Dev vs. production divergence. Assets are served from source in development and from a generated cache in production. That means: a new @basset call added in a hotfix will 404 in production until the cache is rebuilt. Conversely, editing a file in production changes nothing until you rebuild. When in doubt, clear and re-warm; also re-run after any config:cache change that affects Basset paths.
Remote assets are localised. CDN URLs are typically downloaded into your own cache in production. Three consequences: (a) deploys need outbound network access, (b) an unreachable CDN at deploy time fails the build rather than the page — usually good, occasionally surprising, (c) integrity/crossorigin attributes written for the CDN may no longer be meaningful once the file is served from your domain. Pin versions explicitly ([email protected]), never @latest, or your cache silently drifts.
Inline block identifiers are a global namespace. Two components using @bassetBlock('styles') will collide, and the second body is discarded silently. Namespace them: 'date-picker.styles', 'datatable.init'.
Debugging checklist, in order: view-source and grep the asset filename → check whether an earlier declaration exists (attribute problem) → check the resolved path string (dedupe-key problem) → check public/ for the cached file (build problem) → run the basset:clear/cache cycle (stale-cache problem). Almost every bug is step one or two.
Extension points. If the directives don't cover a case, don't fork — bind your own class against the Basset manager, or register a custom Blade directive in a service provider that calls the same manager API after normalising the path. Publishing the config lets you relocate the cache and adjust dev-mode detection without touching vendor code. Keep custom directives thin wrappers so you stay on the upgrade path; this package is small and stable, but it does sit directly in Backpack's asset pipeline, so upgrades will arrive whether you want them or not.
How can I help you explore Laravel packages today?