machy8/webloader
Machy8 WebLoader is a PHP asset manager that combines, minifies, and caches CSS and JavaScript files, supports LESS/SCSS compilation, versioned caching, and easy integration with Laravel or Nette to speed up page loads.
Install with Composer. v2.0.1 is the current patch; pin an explicit version rather than floating a constraint if anything downstream depends on the exact tag markup (v2.0.1 changed what the generate methods emit):
composer require machy8/webloader:^2.0
Create a writable, web-accessible cache directory. Bundles are written to disk and served as static files, so the directory must be both writable by PHP and reachable by the browser:
mkdir -p public/assets/cache && chmod -R 775 public/assets/cache
Read the README first, then skim src/WebLoader.php (the public API) and any minifier class. The library is small; the constructor (cache path + public cache URL) and the addFile/addFiles + generateCss/generateJs pair are the whole surface you need on day one.
First use case — bundle and emit a stylesheet/script tag in a layout:
use Machy8\WebLoader\WebLoader;
$loader = new WebLoader(
base_path('public/assets/cache'), // filesystem path
'/assets/cache' // public URL of that path
);
$loader->addFiles([
resource_path('css/reset.css'),
resource_path('css/app.css'),
]);
echo $loader->generateCss('app');
// <link rel="stylesheet" href="/assets/cache/app.abc123.css">
As of v2.0.1 the emitted tags are plain HTML5 — no type="text/css" on <link>/<style> and no type="text/javascript" on <script> — so validated HTML stays warning-free out of the box.
In Laravel, bind it as a singleton in a service provider (see below) so you configure paths once and reuse it across views and helpers.
Service provider + config. Keep paths in config/webloader.php and bind one or more loaders:
// config/webloader.php
return [
'cache_dir' => public_path('assets/cache'),
'cache_url' => '/assets/cache',
'minify' => env('WEBLOADER_MINIFY', true),
];
// AppServiceProvider::register()
$this->app->singleton(WebLoader::class, fn () => new WebLoader(
config('webloader.cache_dir'),
config('webloader.cache_url')
));
Named bundles as separate instances. Different pages (admin vs. public, checkout vs. marketing) usually want different bundles. Either create a loader per request inside a Blade component, or expose a small WebLoaderFactory that hands out a fresh instance for a named bundle. Keeping bundles separate avoids shipping the checkout JS to the marketing page.
Blade component or directive for output. Don't scatter echo $loader->generate…() in views. Wrap it:
// app/View/Components/WebLoaderScripts.php
public function render()
{
$loader = app(WebLoader::class);
$loader->addFiles([...]);
return '<x-raw-html :content="$loader->generateJs()" />';
}
or a simple @webloaderJs('app') directive that resolves the loader, adds the configured files, and echoes the tag.
Environment switch. Bundle in production, emit individual <link>/<script> tags in local so you get readable source and no cache staleness. A simple if (app()->isLocal()) helper that loops over files and prints raw tags is worth the 10 lines — it prevents most "why is my CSS change not visible" incidents.
Conditional, ordered additions. Order matters for JS. Add vendor/polyfill files before app code, and add page-specific files at the end of the chain. Because you can add files incrementally, one shared base bundle plus a per-page loader that appends only that page's files is clean.
Cache-busting. If the generated filename is content/mtime-derived, references update automatically. If you get stable filenames, append a version query string yourself using the bundle's mtime — otherwise browsers will serve stale bundles for a long time.
Pre-warm at deploy. Bundling on first request means the first visitor pays the cost and you risk concurrent writes. Add a deploy step (an artisan command that instantiates the loaders and calls the generate methods) so the cache directory is populated before traffic hits. Clear the cache directory in the same step to avoid stale bundles.
Keep it out of the Vite/Mix pipeline. If the app already uses Vite or Mix for JS, webloader is redundant and risks double-processing. It fits best in small/legacy apps, or as a thin CSS concatenator alongside another JS tool.
www-data, you get a failed write or broken tag rather than an obvious error. Check permissions before debugging anything else, and add the directory to .gitignore but not to your deploy excludes.type attributes are gone from <link>, <script> and <style> output. Tests that snapshot full tags (assertSee('<script type="text/javascript" src=…')), Blade partials that hard-code the old markup, and any JS/regex that located bundles via type="text/javascript" will all fail on upgrade. Assert on the src/href URL or the file's existence instead of the whole tag.type attributes, v2.0.1 output will validate as invalid there. Build the tag yourself around the generated URL in those paths rather than fighting the library.text="text/javascript" instead of type=). The change described is the real one, but it's a hint that the changelog is hand-written — read the actual diff of src/ when upgrading rather than trusting the notes to be exhaustive.base_path(). Always pass absolute paths from base_path()/public_path()/resource_path(). Remote URLs (https://…) are almost certainly not supported — download or bundle them upstream instead.url() breakage. When CSS files from different directories are concatenated into one file in the cache dir, relative url(fonts/…) / url(images/…) references can resolve against the cache location and 404. Either keep the cache dir at the same depth/root as the sources, use absolute URLs in CSS, or post-process the output. This is the most common visual regression after switching to bundling.??=, top-level await) can be mangled or left unminified. Leave those files out of minification, or minify upstream with a real tool and point webloader at the result.defer/async/media/integrity. v2.0.1 only removed the type attribute; it didn't add attribute plumbing, so if these aren't supported, build the tag yourself around the generated URL rather than hacking the output string.null/'' when a file is missing or the cache isn't writable. Assert the output in a feature test (bundle file exists, tag contains the cache URL) so a misconfigured directory fails CI rather than production.ls the cache dir and confirm a bundle was written, (2) diff its contents against your source files for missing additions, (3) inspect the filename to infer the hash input, (4) clear and regenerate, (5) check the browser network tab — a 404 on the cache URL usually means a path/URL mismatch between constructor arguments.How can I help you explore Laravel packages today?