Vite

Manifest tags, the dev-server hot file, asset base URLs, prefetching and CSP nonces.

ViteAssets (a singleton, also usable directly as vite.RenderTags(httpContext, "resources/js/app.tsx")) renders the tags for the @vite(...) token and the <vite /> tag helper.

Production

In production Inertia.Net reads wwwroot/build/.vite/manifest.json (falling back to wwwroot/build/manifest.json for older Vite). It parses the manifest once and pre-renders the tags to UTF-8. For each entry it emits, in order:

  1. <link rel="stylesheet"> for the entry's CSS and the CSS of its static imports, recursively and deduplicated;
  2. <script type="module" src="..."> for the entry file;
  3. <link rel="modulepreload"> for each static import.

URLs are {PathBase}/{BuildDirectory}/{file} so apps under a path base work, or {AssetBaseUrl}/{file}. An entry missing from the manifest throws with the list of available entries. A missing manifest throws with the paths it looked in.

Development

While the dev server runs, the tags are @vite/client and the entry from the dev server origin, plus the React Refresh preamble for @viteReactRefresh. The dev server counts as running when:

  • o.Vite.DevServerUrl is set (always, in any environment), or
  • the environment is Development and the hot file (wwwroot/hot by default) exists and contains the origin.

The hot file is written by your Vite config. The @inertiajs/vite plugin does not write one, so the samples include a small plugin that does.

base must differ between dev and build

Built assets are served from /build/, so the Vite base is /build/ for a build. The dev server must serve from the root because Inertia.Net emits {origin}/@vite/client and {origin}/{entry}. Use base: command === 'build' ? '/build/' : '/'.

Options

Text
builder.Services.AddInertia(o =>
{
    o.Vite.PublicDirectory = "wwwroot";                        // default
    o.Vite.BuildDirectory = "build";                           // default
    o.Vite.ManifestPath = null;                                // default wwwroot/build/.vite/manifest.json
    o.Vite.HotFilePath = null;                                 // default wwwroot/hot
    o.Vite.DevServerUrl = builder.Environment.IsDevelopment() ? "http://localhost:5173" : null;
    o.Vite.AssetBaseUrl = "https://cdn.example.com/build";     // serve built assets from a CDN
});

Only plain http(s) origins without quotes or markup are accepted from the hot file or DevServerUrl, because the origin is echoed into a script.

With AssetBaseUrl set, Vite's own base must match so lazily imported chunks resolve against the CDN too:

Text
base: 'https://cdn.example.com/build/'

Prefetching

Page components resolved with lazy import.meta.glob are separate chunks that the browser downloads only when a page needs them. o.Vite.Prefetch downloads them after the page loads, at low priority, so the next navigation does not wait:

Text
o.Vite.Prefetch = VitePrefetch.Waterfall;   // PrefetchConcurrency (default 3) at a time
o.Vite.Prefetch = VitePrefetch.Aggressive;  // all at once

The tags get one small inline script with the list of chunks the entries can import later (dynamic imports, then their static imports and CSS), minus those already loaded. It is rendered once per entry set.

Content Security Policy

With a nonce-based CSP, set o.CspNonce to the request's nonce. Every script and stylesheet tag Inertia.Net writes then carries it: the page <script data-page>, the Vite tags, the React refresh preamble and the prefetch script (whose links get it too).

Text
builder.Services.AddInertia(o => o.CspNonce = ctx => ctx.Items["csp-nonce"] as string);

app.Use((ctx, next) =>
{
    var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
    ctx.Items["csp-nonce"] = nonce;
    ctx.Response.Headers.ContentSecurityPolicy =
        $"script-src 'nonce-{nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'";
    return next(ctx);
});

The nonce must be base64 or base64url; anything else throws. Cached tags are split where the nonce goes, so a nonce costs one copy of the tag bytes per request and no re-rendering.

Static assets

Use app.UseStaticFiles() with a long Cache-Control for /build/assets, or app.MapStaticAssets() when the client is built before dotnet publish. See Deploying.