Vite’s multi-page application (MPA) setup is deceptively simple, and the magic happens not in a single index.html but in how Vite intelligently handles multiple index.html files as distinct application roots.

Let’s see this in action. Imagine you have a project with two distinct pages: a main landing page and an admin dashboard.

Here’s a typical directory structure:

my-mpa-project/
├── index.html
├── admin.html
├── src/
│   ├── main.js
│   ├── admin.js
│   └── styles/
│       └── main.css
└── vite.config.js

And the content of our HTML files:

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Main Page</title>
    <link rel="stylesheet" href="/src/styles/main.css">
</head>
<body>
    <h1>Welcome to the Main Page!</h1>
    <script type="module" src="/src/main.js"></script>
</body>
</html>

admin.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Dashboard</title>
    <link rel="stylesheet" href="/src/styles/main.css">
</head>
<body>
    <h1>Admin Dashboard</h1>
    <script type="module" src="/src/admin.js"></script>
</body>
</html>

And our JavaScript entry points:

src/main.js:

import './styles/main.css';
console.log('Main page script loaded!');

src/admin.js:

import './styles/main.css';
console.log('Admin dashboard script loaded!');

When you run vite, it scans the root of your project for index.html files. By default, it finds index.html. If you have other HTML files at the root, Vite treats each one as a separate entry point.

To explicitly tell Vite about all your entry points, you configure vite.config.js:

vite.config.js:

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: 'index.html', // Explicitly define 'main' entry point
        admin: 'admin.html'  // Explicitly define 'admin' entry point
      },
      output: {
        entryFileNames: '[name]-[hash].js',
        chunkFileNames: 'chunks/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]'
      }
    }
  }
});

Running vite dev will start a development server. By default, it will serve index.html at http://localhost:5173/. To access the admin page, you’d navigate to http://localhost:5173/admin.html. Vite automatically handles the routing and serves the correct index.html based on the URL path.

When you run vite build, Vite will process each entry point defined in rollupOptions.input. It will generate separate bundles for main.js and admin.js, and create distinct index.html files (or update them if they exist) with references to these bundles. The output will be in your dist directory, with HTML files and their corresponding JS/CSS assets.

The key to understanding this is that Vite doesn’t enforce a single index.html. Instead, it uses index.html files as the anchors for your different application sections. Each index.html file can import its own JavaScript and CSS, and Vite’s build process will create separate, optimized bundles for each. The rollupOptions.input configuration is where you tell Vite which HTML files are your application’s entry points.

The entryFileNames, chunkFileNames, and assetFileNames within rollupOptions.output are crucial for organizing your build output. They allow you to control how your generated files are named and structured in the dist directory, ensuring clean separation between different pages and their assets, especially important in larger MPA projects.

What most people don’t realize is that Vite’s MPA mode is not just about having multiple index.html files; it’s about Vite’s ability to treat each index.html as a distinct application root, allowing for independent development and building of different parts of your application while still leveraging Vite’s core features like hot module replacement and optimized builds for each entry. You can even have different HTML files in subdirectories, and Vite will respect those paths as long as they are correctly specified in the input configuration.

The next hurdle you’ll likely encounter is managing shared dependencies across these multiple entry points efficiently to avoid code duplication.

Want structured learning?

Take the full Vite course →