skip to content

Vite Asset Bundling

The laravel-vite-plugin builds resources/js and resources/css entries into hashed files that @vite resolves from the hot file or the manifest. Interviewers ask why assets 404 after a deploy.

on this pageshow

explore

questions

6

In a Laravel Blade layout, what does the @vite directive output, and how does that differ between npm run dev and npm run build?

level: juniorimportance: must knowfreq 62%

answer

  1. one directive, two modes
  2. public/hot decides the mode
  3. @vite/client plus dev-server URLs
  4. public/build/manifest.json maps source to hashed file
  5. entry must be a plugin input

basics

~20 s

@vite prints the script and stylesheet tags for the entry points you name. With npm run dev it points them at the Vite dev server (plus the @vite/client HMR script); after npm run build it reads public/build/manifest.json and links the hashed, compiled files.

solid answer

~40 s

`@vite(['resources/css/app.css', 'resources/js/app.js'])` compiles to a call on the `Illuminate\Foundation\Vite` singleton. While `npm run dev` is running, laravel-vite-plugin has written the dev server's URL into `public/hot`, so the directive emits `@vite/client` plus one tag per entry pointing at the dev server, and changes arrive through hot module replacement. After `npm run build`, there is no hot file: the directive reads `public/build/manifest.json`, looks up each entry by its source path and emits tags for the content-hashed files in `public/build/assets`, plus `modulepreload`/`preload` links for imported chunks and any CSS the JavaScript imports. The names you pass must be `input` entries in `vite.config.js`, or the build lookup fails with a `ViteException`.

code

javascript · 11 lines
javascript
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,
        }),
    ],
});

go deeper

for a junior

Recall the two scripts, npm run dev and npm run build, and that @vite in the layout head is the only place assets are wired in.

for a middle

Explain the switch: the hot file sends tags to the dev server, otherwise the manifest maps source paths to hashed files and preload links.

for a senior

Tie the modes to failures you have seen: a missing manifest after a deploy, a mismatched entry name, CSS imported from JavaScript in Inertia apps.

for a principal

Frame hashed assets as a caching contract: immutable file names let the CDN cache forever, and the manifest is the one source of truth that must ship with the code.

## What the directive is `@vite` is a Blade directive that Laravel's compiler turns into a single PHP call: `app(Illuminate\Foundation\Vite::class)(...)`. The `Vite` class is registered as a **container singleton** by the foundation service provider, and the same object sits behind the `Vite` facade. Its job is to turn a list of **entry points** — source paths such as `resources/css/app.css` and `resources/js/app.js` — into the HTML tags a browser needs. You normally place it once, in the `<head>` of the root layout: ```html <head> <meta charset="utf-8"> @vite(['resources/css/app.css', 'resources/js/app.js']) </head> ``` The Laravel 13 skeleton's `welcome.blade.php` calls it the same way, wrapped in a `file_exists()` check on the manifest and the hot file that falls back to inline styles, and its `vite.config.js` lists the same two files as the plugin's `input`. ## The two modes The class decides which mode it is in with one check: does the **hot file** (`public/hot` by default) exist? | | `npm run dev` (runs `vite`) | `npm run build` (runs `vite build`) | |---|---|---| | What the plugin writes | the dev server URL into `public/hot` | hashed files in `public/build/assets` and `public/build/manifest.json` | | What `@vite` emits | `<script type="module">` for `@vite/client`, then one tag per entry at the dev server URL | `<link rel="stylesheet">` and `<script type="module">` for the compiled files | | Updates | hot module replacement from the dev server | none; a new build means new file names | | Extra tags | none | `modulepreload` / `preload` links for imported chunks and their CSS | In **dev mode** nothing is written into `public/build`; the dev server compiles modules on request and the browser loads them from the dev server's origin, while Laravel still serves the HTML. That is why you open your app at `APP_URL`, not at the dev server port — visiting the dev server's own address directly shows a placeholder page telling you where the app lives. In **build mode** the directive reads the **manifest**, a JSON map whose keys are source paths and whose values carry the output `file`, the `css` it pulled in, and its `imports`. For each entry it emits the entry tag, a stylesheet link for every CSS file the JavaScript imported, and preload hints for the shared chunks, so the browser can fetch them in parallel. ## Rules that trip people up - **Entry names must match.** In build mode each name you pass to `@vite` is looked up as a manifest key. A path that is not an `input` entry (and not otherwise in the manifest) throws `ViteException` with "Unable to locate file in Vite manifest". - **No hot file and no manifest** means the page throws `ViteManifestNotFoundException` ("Vite manifest not found at: …/public/build/manifest.json") — the usual first error on a fresh clone before `npm install && npm run build`, once a layout calls `@vite` without the welcome page's `file_exists()` guard. - **CSS can be imported by JavaScript.** Apps whose CSS is imported from `app.js` (the Inertia pattern) only list the JavaScript entry; build mode still links the CSS because the manifest records it under that entry's `css` key. - **A second argument sets the build directory**, relative to `public`, for packages that ship their own build: `@vite('resources/js/app.js', 'vendor/courier/build')`. - **Both files are gitignored.** The skeleton's `.gitignore` lists `/public/hot` and `/public/build`, so a build has to be produced for every environment, on the server or in CI, rather than committed. ## Why it is designed this way Hashing file names makes every build **cache-busting by construction**: a changed file gets a new name, so browsers and CDNs can cache assets forever. The price is that HTML cannot hard-code those names, which is exactly the problem the manifest lookup solves at render time. The hot-file switch gives one layout that works in both modes without an `if (app()->isLocal())` branch in the template. ## History worth recognising Before Vite, Laravel projects used **Laravel Mix** (webpack) with a `webpack.mix.js` file and the `mix()` helper. Vite replaced Mix as the default front-end tooling, and current skeletons ship `vite`, `laravel-vite-plugin` and Tailwind's Vite plugin in `package.json`. A candidate describing `mix()` as today's way is describing an older app. ## What to say in an interview 1. `@vite` resolves entry points to tags through the `Vite` singleton. 2. `public/hot` present → dev server URLs plus `@vite/client`. 3. Otherwise → `public/build/manifest.json` lookup, hashed files, preload links. 4. Entry names must be plugin inputs; missing build → `ViteManifestNotFoundException`.

  • Why do you open the app at APP_URL rather than at the Vite dev server's port during development?
    Laravel still renders every page; the dev server only serves JavaScript and CSS modules. `@vite` writes absolute dev-server URLs into Laravel's HTML, so the browser pulls modules from the dev server while the page comes from `APP_URL`. Visiting the dev server's port directly returns a placeholder page from laravel-vite-plugin telling you to go to your `APP_URL`.
  • An Inertia app imports its CSS from resources/js/app.js and lists only the JS entry. Does the production page still get a stylesheet link?
    Yes. In build mode the manifest entry for `resources/js/app.js` carries a `css` array of every stylesheet it imported, and `@vite` emits a `<link rel="stylesheet">` (and a preload hint) for each one, so only the JavaScript entry needs naming.
  • What does the second argument of @vite do?
    It overrides the build directory, relative to `public`, for that call only: `@vite('resources/js/app.js', 'vendor/courier/build')` reads `public/vendor/courier/build/manifest.json`. Packages that ship their own compiled front end use it so their manifest does not collide with the app's `public/build`.

saying these in an interview costs you the question

  • @vite just prints a script tag for /js/app.js with no manifest lookup
  • npm run dev writes compiled bundles into public/build
  • Any path can be passed to @vite even if it is not a plugin input
  • Laravel Mix and the mix() helper are the current default asset pipeline
  • You browse the app at the Vite dev server port during development
open as a page

How does Laravel's Vite class choose between the dev server and built assets, and what breaks when public/hot or the manifest is wrong?

level: middleimportance: must knowfreq 46%

basics

~20 s

Laravel's Vite class calls isRunningHot(), which is just is_file() on public/hot. If the file exists, every tag points at the URL inside it; otherwise tags come from public/build/manifest.json. A stale hot file or a missing manifest breaks every page.

open as a page

In a Laravel app, what do laravel-vite-plugin's refresh option and the @viteReactRefresh Blade directive each do, and when do you need them?

level: middleimportance: should knowfreq 27%

basics

~20 s

The refresh option makes the dev server trigger a full page reload when Blade views, routes, lang files or Livewire classes change. @viteReactRefresh injects React Fast Refresh's preamble into Laravel's HTML during npm run dev and must precede @vite.

open as a page

In Laravel 13, how do you serve a versioned image referenced only from a Blade template through Vite, and what changed in laravel-vite-plugin 3?

level: middleimportance: should knowfreq 36%

basics

~20 s

List the files in the laravel-vite-plugin assets option so the build emits them with hashed names into the manifest, then print Vite::asset('resources/images/logo.png') in Blade. Plugin 3 (Vite 8) replaced the old import.meta.glob trick with this option.

open as a page

After a florist's shop Laravel app deploys a new Vite build, some visitors get old CSS and others get 404s on /build/assets files; how do you diagnose it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Hashed file names mean the browser cache is rarely at fault; the HTML and the files on disk disagree. Check for a stale public/hot, a manifest cached by long-lived workers, cached HTML pointing at deleted hashes, and servers or a CDN missing the new build.

open as a page

In a Laravel app under a nonce-based Content-Security-Policy, how do you get Vite-generated tags to carry the nonce, and what does Vite::useCspNonce() cover?

level: seniorimportance: nice to knowfreq 18%

basics

~10 s

Call Vite::useCspNonce() in a middleware before the view renders and send the returned nonce in the Content-Security-Policy header. Every tag the Vite class prints then carries nonce="…"; other inline scripts use Vite::cspNonce().

open as a page