In a Laravel Blade layout, what does the @vite directive output, and how does that differ between npm run dev and npm run build?
answer
- one directive, two modes
- public/hot decides the mode
- @vite/client plus dev-server URLs
- public/build/manifest.json maps source to hashed file
- 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 linesimport { 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
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.
Explain the switch: the hot file sends tags to the dev server, otherwise the manifest maps source paths to hashed files and preload links.
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.
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