Vite and Frontend Assets
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, with Vite 8.3, laravel-vite-plugin 3.2 and
Tailwind CSS 4.3, checked on 2026-10-05. Every example is built and served in the Bookshelf application used across
this guide. It covers how Laravel and Vite fit together; the tools themselves are explained on their own pages:
Tailwind CSS, React,
Vue, JavaScript and
TypeScript.
The configuration of a new application
A new application ships package.json (Vite, the Laravel plugin, Tailwind 4 and its Vite plugin), vite.config.js,
resources/css/app.css and resources/js/app.js. Bookshelf’s configuration, after two changes explained below:
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { fontsource } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
assets: ['resources/images/**'], // files referenced only from Blade
refresh: true,
fonts: [
fontsource('Instrument Sans', { // from node_modules, no network at build time
weights: [400, 500, 600],
}),
],
}),
tailwindcss(),
],
server: {
watch: {
ignored: ['**/storage/framework/views/**'],
},
},
});
/* resources/css/app.css, as generated */
@import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
Source: Laravel — Configuring Vite
input lists the entry points. refresh: true reloads the browser when a Blade view, route file or translation
changes. The two @source lines tell Tailwind 4 to scan the pagination views and compiled Blade files for class
names, since Tailwind 4 has no tailwind.config.js content list; Tailwind’s own configuration is on
Tailwind Customization and Configuration.
The generated configuration used bunny('Instrument Sans', ...), which downloads the font from fonts.bunny.net
during npm run build. In a build environment without access to that host the build failed in
resolveRemoteFont with "0 modules transformed". fontsource() reads the same font from the
@fontsource/instrument-sans npm package instead (npm install -D @fontsource/instrument-sans), so the build needs
only the npm registry. local() serves fonts committed to resources/fonts.
@vite in production and in development
npm run build writes hashed files and public/build/manifest.json; @vite([...]) reads the manifest and emits the
tags:
$ npm run build
vite v8.3.2 building client environment for production...
public/build/manifest.json 1.47 kB │ gzip: 0.33 kB
public/build/fonts-manifest.json 5.74 kB │ gzip: 0.71 kB
public/build/assets/instrument-sans-400-normal-DRC__1Mx.woff2 16.86 kB
...
public/build/assets/cover-D5pAZEeb.png 7.02 kB
public/build/assets/app-hF-j6T4A.css 55.14 kB │ gzip: 11.74 kB
public/build/assets/app-B2JTsE0B.js 0.10 kB │ gzip: 0.11 kB
✓ built in 405ms
{{-- resources/views/shelf/assets.blade.php --}}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
@fonts
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans">
<img src="{{ Vite::asset('resources/images/cover.png') }}" alt="A sample cover">
<div data-shelf class="p-4 text-lg">Shelf</div>
</body>
</html>
<!-- production: tags from the manifest (one line in the response, split here) -->
<link rel="preload" as="font" href="http://localhost:8000/build/assets/instrument-sans-400-normal-DRC__1Mx.woff2" type="font/woff2" crossorigin="anonymous" />
<style>@font-face { font-family: "Instrument Sans"; ... }</style>
<link rel="preload" as="style" href="http://localhost:8000/build/assets/app-hF-j6T4A.css" />
<link rel="modulepreload" as="script" href="http://localhost:8000/build/assets/app-B2JTsE0B.js" />
<link rel="stylesheet" href="http://localhost:8000/build/assets/app-hF-j6T4A.css" />
<script type="module" src="http://localhost:8000/build/assets/app-B2JTsE0B.js"></script>
<img src="http://localhost:8000/build/assets/cover-D5pAZEeb.png" alt="A sample cover">
npm run dev (or composer run dev, which also starts the PHP server, the queue listener and Pail) starts the Vite
dev server and writes its URL to public/hot. While that file exists, @vite points at the dev server instead of
the manifest, and the browser receives modules on demand with hot module replacement:
$ npx vite --port 5173
VITE v8.3.2 ready in 449 ms
➜ Local: http://localhost:5173/
[laravel:fonts] Optimized font fallbacks require the optional "fontaine" package. Install it, or set "optimizedFallbacks: false" on your fonts to disable the feature.
LARAVEL v13.34.0 plugin v3.2.0
➜ APP_URL: http://localhost:8000
$ cat public/hot
http://127.0.0.1:5173
$ curl -s localhost:8000/shelf/assets | grep -o '<script[^>]*></script>\|<link rel="stylesheet"[^>]*>\|<img[^>]*>'
<script type="module" src="http://127.0.0.1:5173/@vite/client"></script>
<link rel="stylesheet" href="http://127.0.0.1:5173/resources/css/app.css" />
<script type="module" src="http://127.0.0.1:5173/resources/js/app.js"></script>
<img src="http://127.0.0.1:5173/resources/images/cover.png" alt="A sample cover">
Source: Laravel — Running Vite
Stopping the dev server deletes public/hot, and the same page served the built files again. A stale public/hot
left by a crashed dev server, or one deployed by mistake, makes production pages load scripts from
127.0.0.1:5173 and render unstyled; public/hot belongs in .gitignore and nowhere near a deployment artifact.
Without either a manifest or a hot file, @vite throws ViteManifestNotFoundException, which is why feature tests
call $this->withoutVite() when the assets have not been built.
Static assets referenced from Blade
Vite only versions files it knows about. Files used only in Blade, such as an image referenced with
Vite::asset(), are listed in the plugin’s assets option (laravel-vite-plugin 3). Bookshelf first used the
import.meta.glob(['../images/**']) line from older tutorials in app.js; under Vite 8 the build emitted no images,
and the page failed with ViteException: Unable to locate file in Vite manifest: resources/images/cover.png. The
plugin’s upgrade guide states the change: from 2.x to 3.x, import.meta.glob for static assets is replaced by
assets: ['resources/images/**']. With that option the build listed cover-D5pAZEeb.png and Vite::asset()
returned its hashed URL.
JavaScript frameworks, environment variables and SSR
React, Vue and Svelte are added as Vite plugins (@vitejs/plugin-react, @vitejs/plugin-vue,
@sveltejs/vite-plugin-svelte) with an entry point such as resources/js/app.jsx; the starter kits configure this,
plus TypeScript and Inertia, out of the box (Inertia and Starter
Kits). Vue needs the plugin’s transformAssetUrls option so that <img src="/images/..."> in single-file
components is not rewritten as a module import.
Only variables prefixed with VITE_ in .env reach the browser bundle, through import.meta.env:
# .env
VITE_APP_NAME="${APP_NAME}"
# the dev server's version of resources/js/app.js
$ curl -s localhost:5173/resources/js/app.js | head -1
import.meta.env = {"BASE_URL": "/", "DEV": true, "MODE": "development", "PROD": false, "SSR": false, "VITE_APP_NAME": "Laravel"};const shelf = document.querySelector('[data-shelf]');
Source: Laravel — Environment Variables
Anything with the prefix is public: never prefix a secret. The value is baked into the bundle at build time, so
changing .env on the server has no effect until the assets are rebuilt.
Server-side rendering of a React, Vue or Svelte front end is enabled with ssr: 'resources/js/ssr.js' in the plugin
options, built with vite build && vite build --ssr, and served by a Node process (php artisan inertia:start-ssr
for Inertia).
CSP nonces, SRI and prefetching
A strict Content Security Policy blocks inline and unknown scripts. Vite::useCspNonce() generates a nonce for the
request and adds it to every tag @vite and @fonts produce, so the policy can allow exactly those:
<?php
// routes/catalogue.php
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Vite;
Route::get('/shelf/assets-csp', function () {
$nonce = Vite::useCspNonce(); // random per request
Vite::prefetch(concurrency: 3);
return response()->view('shelf.assets')
->header('Content-Security-Policy', "script-src 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}'");
});
$ curl -s -D - localhost:8000/shelf/assets-csp | grep -o "Content-Security-Policy: .*\|<script[^>]*>\|<link rel=\"stylesheet\"[^>]*>\|<style[^>]*>"
Content-Security-Policy: script-src 'nonce-…'; style-src 'self' 'nonce-…'
<style nonce="…">
<link rel="stylesheet" href="http://localhost:8000/build/assets/app-hF-j6T4A.css" nonce="…" />
<script type="module" src="http://localhost:8000/build/assets/app-B2JTsE0B.js" nonce="…">
<script nonce="…">
The nonce is 40 random characters, different on every request. The last <script nonce> is the prefetching
snippet: after the load event it adds <link rel="prefetch"> tags for the other chunks in the manifest, three at
a time, so later pages load from cache. In the first run the header was default-src 'self'; frame-ancestors 'none'
instead: the global AddSecurityHeaders middleware from Middleware ran after
the route and overwrote the page’s policy, which would have blocked the nonce-tagged scripts. It now leaves an
existing policy alone. In a real application the nonce and the header belong in one middleware, so every page gets
them.
Subresource Integrity adds integrity="sha384-..." to the tags: install vite-plugin-manifest-sri, which writes
the hashes into the manifest, and @vite uses them (Vite::useIntegrityKey(false) turns it off). SRI matters when
assets are served from a CDN the application does not control. Both CSP and SRI are covered in general on
Web Security Fundamentals.
Choosing a front end
| Approach | How it works | Suits |
|---|---|---|
Blade (with Alpine.js) |
Server-rendered pages, small behaviour in Alpine or vanilla JS |
Content sites, admin pages, teams without a JavaScript framework |
Livewire |
Blade components with server-side state; interactions are AJAX round trips |
Interactive UIs written in PHP (Livewire) |
Inertia |
Laravel routes and controllers return React, Vue or Svelte pages instead of views; no separate API |
Teams that want a JavaScript framework without a separate SPA (Inertia and Starter Kits) |
Separate SPA or mobile app |
Laravel as a JSON API with Sanctum |
Several clients (Building APIs and API Resources) |
Source: Laravel — Frontend
All four use Vite for whatever JavaScript and CSS they have. The starter kits exist for Livewire, React, Vue and Svelte, which makes the choice mostly one of team skills.
Migrating from Laravel Mix
Laravel Mix (a webpack wrapper) was the default until Laravel 9.19 (June 2022). The plugin’s upgrade guide lists the steps:
install vite and laravel-vite-plugin and remove laravel-mix; replace webpack.mix.js with vite.config.js;
change the npm scripts to vite and vite build; rename MIX_ variables to VITE_ and process.env.MIX_X to
import.meta.env.VITE_X; import CSS from the JavaScript entry point or list it in input; replace mix('js/app.js')
with @vite('resources/js/app.js'); give files containing JSX a .jsx extension and import .vue files with their
extension. Coming from plugin 2.x, the one change is the assets option described above.
Where older books and tutorials are outdated
-
Laravel Mix,
webpack.mix.jsandmix()(Stauffer ch. 6) were replaced by Vite in Laravel 9.19; new applications have no Mix configuration. -
import.meta.glob(['../images/**'])inapp.jsfor Blade-only assets stopped working with Vite 8 andlaravel-vite-plugin3; use theassetsoption. -
tailwind.config.jswith acontentarray is replaced in Tailwind 4 by@import 'tailwindcss'and@sourcein the CSS file. -
Google Fonts
<link>tags in the layout can be replaced by the plugin’sfontsoption and@fonts(Laravel 13), which self-hosts and preloads them. -
MIX_environment variables andprocess.envareVITE_andimport.meta.env.