Install into the Craft project (not the front-end build folder) and let Composer wire up the plugin:
composer require misterbk/mix
php craft plugin/install mix
The plugin registers a single Twig function, mix(), which reads Laravel Mix's mix-manifest.json and resolves a source path to its versioned output path. That's the whole surface area — there is no control panel section, no element type, and no database table, so "installing" is essentially "enabling a Twig helper."
Before the first template call, confirm the build side is actually producing a manifest:
// webpack.mix.js
const mix = require('laravel-mix');
mix.js('resources/js/app.js', 'web/js')
.postCss('resources/css/app.css', 'web/css')
.version(); // <-- without this there is no mix-manifest.json
Run npm run production (or npm run dev with versioning enabled) and verify web/mix-manifest.json exists at the web root Craft serves from. Then, in the first layout you touch:
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<script src="{{ mix('js/app.js') }}" defer></script>
Where to look first: the plugin's config/mix.php (copy it into the project's config/ folder to override defaults) and the README on the repository. Those two files answer 90% of the questions — the manifest location and the public prefix applied to resolved paths. Everything else is just Twig usage.
The first real use case is a base layout (templates/_layouts/base.twig) that both the site front end and any entry templates extend, so asset URLs are resolved in exactly one place.
Single entry point in the base layout. Keep mix() calls out of individual entry templates. One layout, two or three calls, and every page inherits versioned URLs. This also keeps the manifest lookups cheap and predictable.
Per-page chunks with a guard. If you split bundles per section, check the key exists before emitting markup rather than letting a missing key raise:
{% set chunk = 'js/account.js' %}
{% if mix_exists is defined and attribute(manifest, chunk) is defined %}
...
{% endif %}
In practice most teams just wrap the call in a try-free pattern by keeping a matching entry for every template, but the guard is worth it during a refactor when the build lags behind the templates.
Use Craft's native asset tags. Since the function returns a URL string, it composes cleanly with Craft 3.5+ {% css %} / {% js %} tags, which handle deduplication and attribute output:
{% css mix('css/app.css') %}
{% js mix('js/app.js') with { defer: true } %}
Separate "base path" from "manifest path." The manifest values are web-root-relative. If your site is served from a subdirectory or a CDN, the resolved output needs that prefix; the plugin's config exposes the manifest location and public prefix for exactly this. Set it once in config/mix.php per environment (using .env vars) instead of hardcoding in templates.
Environment parity. Local dev often runs npm run watch in a subdomain or with Vite/Mix hot reload; production reads the manifest. Keep the manifest committed to the deploy artifact (or generated as a build step on deploy), and treat mix-manifest.json as a build output that must travel with the assets. A deploy that copies web/js but not mix-manifest.json is the single most common production breakage.
Preload the entry bundles. Because you know the resolved filenames at render time, add <link rel="preload"> for the CSS and the JS entry next to the normal tags — trivial to do and noticeably better LCP on large front ends.
Fallback path if the plugin is abandoned. It is a ~50-star, single-maintainer package with effectively no dependents. A custom module with a two-line Twig extension reading json_decode(file_get_contents(Craft::getAlias('@webroot/mix-manifest.json'))) is a 20-minute replacement. If you are cautious about supply chain, write that module yourself and skip the dependency; if you install the plugin, keep the manifest read isolated behind one Twig include so swapping later is a one-file change.
mix.version() is mandatory. Without it, mix-manifest.json is not written and every mix() call fails. This is the number one "the plugin is broken" report.
Hot-reload mode does not use the manifest. npm run hot serves assets from localhost:8080 and typically writes no useful manifest. If a developer sees unversioned or 404ing assets locally, they're in hot mode — don't debug the plugin.
Leading slash normalization. Mix writes manifest keys with a leading slash (/js/app.js). Calling mix('js/app.js') and mix('/js/app.js') should both resolve, but be consistent across templates; a mismatch during a partial migration produces confusing "some pages work" symptoms.
Stale manifests and Craft caches. The plugin reads the manifest at request time in most setups, so a rebuild is usually picked up immediately — but template caches, Craft's {% cache %} tag, and any full-page/CDN cache in front of the site will still serve old hashed URLs. Clear those after a deploy, not just the build folder.
Missing keys throw, they don't return empty. A typo in mix('js/aps.js') is a 500, not a broken link. Grep templates for mix( after renaming any build entry.
Function name collision. mix() is a generic name. If another plugin or your own module registers the same Twig function, you get a registration conflict. Check installed plugins before blaming the build.
publicPath vs. web root. The plugin assumes the manifest and assets live under the directory Craft serves. If the web root is a symlinked current/ release directory (Deployer, Envoyer, Capistrano), make sure the build writes into the release's web/ — a manifest in the wrong release directory resolves to assets that were deleted on the previous deploy.
Multi-site and multi-environment. Different sites may need different public prefixes (e.g. a CDN per brand). Drive the config from .env rather than from a static file, and verify the config file is actually being loaded — Craft silently falls back to plugin defaults if config/mix.php is misnamed or returns a bad array.
No CSP nonce or SRI support. The helper only returns a path. If you require Subresource Integrity or CSP nonces, generate hashes in the build pipeline and add them yourself; don't expect the plugin to help.
Debugging checklist, in order: does web/mix-manifest.json exist and is it valid JSON → does it contain the exact key you're calling → does the config's public prefix match how the site is served → is the asset file physically present at the resolved path → is a cache serving the old value. That sequence resolves essentially every issue this plugin can produce.
How can I help you explore Laravel packages today?