Laravel 13 + Vite: HMR, Asset Builds, and Self-Hosted Fonts
Vite has been Laravel's default asset bundler since version 9, and in Laravel 13 its official plugin also resolves, optimizes, and serves self-hosted fonts through the @fonts directive. Meanwhile, the @vite directive decides on its own: it connects to the dev server locally and resolves versioned files in production.
Why Laravel Uses Vite (and Not Mix or Webpack)
If you have worked with Laravel for years, you remember the Laravel Mix era and its webpack.mix.js configuration. Every project dragged its own Webpack recipe, with compile times that kept growing and a development experience that depended on third-party plugins to get anything close to hot reloading.
From Laravel Mix to Vite: The 2022 Change
With Laravel 9, the team replaced Mix with Vite as the default build tool. The official integration lives in the laravel-vite-plugin package, which bridges the bundler and the application: it registers entries, generates the manifest for the backend, and exposes the Blade directives you use in your templates. Laravel Mix moved into maintenance mode, and new projects have shipped with Vite ever since.
What You Gain: HMR, Content Hashing, and Second-Long Builds
Vite is built on esbuild and, in recent versions, on Rolldown: production builds finish in seconds even on medium-sized projects. In development it does not compile the whole project; it serves modules on demand and applies Hot Module Replacement, so saving a CSS or JavaScript file updates the page without a full reload. In production, every file gets a content-based hash, which enables aggressive browser caching without the risk of serving stale versions.
Base Setup: vite.config.js and laravel-vite-plugin
A fresh Laravel 13 project already ships with a ready-to-use configuration. The vite.config.js file defines the plugin with the default entries, and with that, npm run dev and npm run build work without touching anything else.
The Default Entries: resources/css/app.css and resources/js/app.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
Entries are the starting points Vite processes: everything you import from app.css or app.js gets bundled, resolved, and included in the final build. If you add more entry files, just include them in the input array.
The @vite Directive in Blade
In your layout, a single line loads everything you need:
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
The @vite directive automatically detects whether Vite's dev server is running: if it is, it injects the Vite client and enables HMR; if not, it resolves the compiled files from the manifest and generates the matching link and script tags, including CSS imported from JavaScript.
Development with HMR
npm run dev and the public/hot File
When you run npm run dev, Vite starts its dev server and creates a public/hot file containing the server URL. When Laravel detects that file, it knows to serve assets from the dev server instead of from disk. If a stale hot file survives a deploy, the browser tries to load assets from an address that no longer exists: the classic symptom of a page with no styles in production.
HMR on Custom Domains: server.host and server.hmr.host
By default, Vite listens on localhost and the HMR client points there. If your application is served on a development domain like blenderdeluxe.test, assets will fail because the browser requests resources from localhost. The fix is to declare the host in the configuration:
server: {
host: 'blenderdeluxe.test',
hmr: {
host: 'blenderdeluxe.test',
},
},
Vite in Docker and Remote Environments: publicDirectory
Inside a container, Vite must listen on an interface reachable from the host, and the plugin needs to know where the application's public folder lives. With the --host flag and the plugin's publicDirectory option, the dev server responds correctly even when the application lives in /var/www/html:
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
publicDirectory: '/var/www/html/public',
})
Production: npm run build and the Manifest
public/build/manifest.json and Content Hashes
npm run build generates the public/build folder with optimized files and a manifest.json that maps each original name to its hashed version, for example app.js becoming app-3f8c1d2a.js. The @vite directive reads that manifest at runtime, so you never write hashed names by hand: they change on every build and the backend resolves them automatically.
Deploying with CI and Serving from a CDN
The common pattern is to run npm ci and npm run build inside your continuous integration pipeline and upload the public/build folder with the rest of the deploy. Because filenames include a content hash, you can serve assets from a CDN with very long cache headers: when content changes, the name changes and the browser fetches the new file.
Self-Hosted Fonts in Laravel 13
The Plugin Resolves, Emits, and Generates the Font CSS
Depending on Google Fonts means handing third-party requests on every visit: another connection, another DNS lookup, and an availability dependency you do not control. In Laravel 13, the Vite plugin can handle fonts for you: you import the font in your CSS, the plugin resolves the files, emits them as hashed Vite assets, generates the corresponding @font-face CSS, and writes a font manifest with the final files.
The @fonts Directive and the Font Manifest
To load those fonts on the page, Blade provides the @fonts directive, which reads the font manifest and emits the needed preloads and declarations:
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@fonts
</head>
The result is that the browser loads fonts from your own domain, with the same hashing and caching strategy as the rest of your assets and zero third-party requests.
Multiple Entries, Lazy Loading, and Code Splitting
@vite with Entry Arrays
If your application has an admin area with its own styles, add the entry to the plugin and pass it to the directive:
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/admin.js'])
Dynamic Imports to Trim Initial JavaScript
For pages where a component weighs more than it is worth loading upfront, dynamic imports split the bundle: the code downloads only when needed, and Vite generates the corresponding chunks in the build.
const chart = await import('./charts');
chart.render(document.getElementById('stats'));
Common Problems and Fixes
@vite Loads Nothing: Missing Plugin, hot File, and .mjs
When the page appears without styles or JavaScript, the usual culprits are three: laravel-vite-plugin is not installed or not declared in vite.config.js, a stale public/hot file points to a dev server that no longer exists, or the config file is named vite.config.mjs and the plugin is not being loaded. Check those three points in that order and the problem usually resolves itself.
CSS Minification with Lightning CSS
Vite minifies CSS with Lightning CSS by default, a fast Rust-based compiler that can also transform modern properties into syntax compatible with older browsers. If you ever need to fall back to the classic esbuild minifier, the build.cssMinify option allows it without touching the rest of the pipeline.
Complete Example: Tailwind + Alpine Served with Vite on Laravel 13
This very blog is the example: Tailwind CSS for styling and Alpine.js for interactivity, both served with Vite on Laravel 13. The app.js entry imports Tailwind's CSS, boots Alpine, and leaves everything ready to use directives like x-data in Blade templates:
import '../css/app.css';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
In development, npm run dev keeps HMR active and Tailwind changes appear instantly. In production, npm run build generates the hashed files and the manifest; the blog font is served self-hosted with @fonts, and the deploy uploads public/build with no extra ceremony.
Conclusion
Understanding what happens between npm run dev and npm run build turns Vite into a predictable tool: the hot file in development, the hashed manifest in production, the HMR host on custom domains, and self-hosted fonts with @fonts in Laravel 13. Once you master this pipeline, assets stop being a black box and become just another part of the deploy. If you want to keep sharpening your Laravel 13 stack, this blog already has guides on Tailwind, Alpine, and caching performance.