Usage Examples
Pug Template Usage Example
Installation
npm add vite-plugin-view pugpnpm add vite-plugin-view pugyarn add vite-plugin-view pugConfiguration
Configure in vite.config.mjs:
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import vitePluginExternal from 'vite-plugin-external';
import { view } from 'vite-plugin-view';
export default defineConfig({
plugins: [
vitePluginExternal({
logLevel: 'TRACE',
externals: {
vue: 'Vue'
}
}),
vue(),
view({
engine: 'pug',
// entry: 'index.pug', // Default is 'index.pug', can configure multiple templates
engineOptions: {
title: 'Vite + Vue' // Available as `title` variable in templates
},
logLevel: 'TRACE' // Set to 'TRACE' to view all logs
})
],
build: {
rolldownOptions: {
output: {
format: 'iife'
}
}
}
});Using Passed Parameters in Templates
index.pug:
doctype html
html(lang='en')
head
meta(charset='UTF-8')
meta(content='width=device-width, initial-scale=1.0' name='viewport')
title= title
link(href='./index.css' rel='stylesheet')
body
//- ResolvedConfig comes from the configResolved hook
p
| define:
= JSON.stringify(ResolvedConfig.define, null, 2)
p
| env:
= JSON.stringify(ResolvedConfig.env, null, 2)
#root
script(src='//unpkg.com/vue@3.5.13/dist/vue.runtime.global.js')
script(src='./src/main.ts' type='module')EJS Template Usage Example
Installation
npm add vite-plugin-view ejspnpm add vite-plugin-view ejsyarn add vite-plugin-view ejsConfiguration
Configure in vite.config.mjs:
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import vitePluginExternal from 'vite-plugin-external';
import { view } from 'vite-plugin-view';
export default defineConfig({
plugins: [
vitePluginExternal({
logLevel: 'TRACE',
externals: {
react: 'React',
'react-dom/client': 'ReactDOM'
}
}),
react({
jsxRuntime: 'classic'
}),
view({
engine: 'ejs',
// entry: 'index.ejs', // Default is 'index.ejs', can configure multiple templates
engineOptions: {
title: 'Vite + React' // Available as `title` variable in templates
},
logLevel: 'TRACE' // Set to 'TRACE' to view all logs
})
],
build: {
rolldownOptions: {
output: {
format: 'iife'
}
}
}
});Using Passed Parameters in Templates
index.ejs:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title %></title>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<%# ResolvedConfig comes from the configResolved hook %>
<p>alias: <%= JSON.stringify(ResolvedConfig.resolve.alias, null, 2) %></p>
<p>env: <%= JSON.stringify(ResolvedConfig.env, null, 2) %></p>
<div id="root"></div>
<script src="//unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="//unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script type="module" src="./src/index.jsx"></script>
</body>
</html>Nunjucks Template Usage Example
Installation
npm add vite-plugin-view nunjuckspnpm add vite-plugin-view nunjucksyarn add vite-plugin-view nunjucksConfiguration
Configure in vite.config.mjs:
import react from '@vitejs/plugin-react';
import nunjucks from 'nunjucks';
import { defineConfig } from 'vite';
import vitePluginExternal from 'vite-plugin-external';
import { engineSource, view } from 'vite-plugin-view';
const env = new nunjucks.Environment();
env.addFilter('stringify', (obj) => {
return JSON.stringify(obj, null, 2);
});
engineSource.requires.nunjucks = env;
export default defineConfig({
plugins: [
vitePluginExternal({
logLevel: 'TRACE',
externals: {
react: 'React',
'react-dom/client': 'ReactDOM'
}
}),
react({
jsxRuntime: 'classic'
}),
view({
engine: 'nunjucks',
extension: '.njk',
// entry: 'index.njk', // Default is 'index.njk', can configure multiple templates
engineOptions: {
title: 'Vite + React' // Available as `title` variable in templates
},
logLevel: 'TRACE' // Set to 'TRACE' to view all logs
})
],
build: {
rolldownOptions: {
output: {
format: 'iife'
}
}
}
});Using Passed Parameters in Templates
index.njk:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }}</title>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
{# ResolvedConfig comes from the configResolved hook #}
<p>alias: {{ ResolvedConfig.resolve.alias|stringify }}</p>
<p>env: {{ ResolvedConfig.env|stringify }}</p>
<div id="root"></div>
<script src="//unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="//unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script type="module" src="./src/index.jsx"></script>
</body>
</html>Handlebars Template Usage Example
Installation
npm add vite-plugin-view handlebarspnpm add vite-plugin-view handlebarsyarn add vite-plugin-view handlebarsConfiguration
Configure in vite.config.mjs:
import react from '@vitejs/plugin-react';
import Handlebars from 'handlebars';
import { defineConfig } from 'vite';
import vitePluginExternal from 'vite-plugin-external';
import { view } from 'vite-plugin-view';
Handlebars.registerHelper('stringify', (obj) => {
return JSON.stringify(obj, null, 2);
});
export default defineConfig({
plugins: [
vitePluginExternal({
logLevel: 'TRACE',
externals: {
react: 'React',
'react-dom/client': 'ReactDOM'
}
}),
react({
jsxRuntime: 'classic'
}),
view({
engine: 'handlebars',
extension: '.hbs',
// entry: 'index.hbs', // Default is 'index.hbs', can configure multiple templates
engineOptions: {
title: 'Vite + React' // Available as `title` variable in templates
},
logLevel: 'TRACE' // Set to 'TRACE' to view all logs
})
],
build: {
rolldownOptions: {
output: {
format: 'iife'
}
}
}
});Using Passed Parameters in Templates
index.hbs:
Delegating requests to Vite's native pipeline with strategy: { dev: 'delegate' }
When to use it
Use strategy.dev: 'delegate' when you want the dev server request path to match exactly what Vite 8 does with a static .html file — e.g. to investigate HMR differences or to debug Vite's built-in middleware:
- The plugin renders the template to a sibling
.htmlfile next to the source template - Any pre-existing user
.htmlis backed up to.bak_<timestamp> next()is called so Vite's native HTML stack (htmlFallbackMiddleware→indexHtmlMiddleware→transformIndexHtml) processes the URL end-to-end- Generated files are removed and backups are restored when the process exits (SIGINT / SIGTERM / uncaught exceptions)
Installation
npm add vite-plugin-view ejspnpm add vite-plugin-view ejsyarn add vite-plugin-view ejsConfiguration
Configure EJS + MPA + strategy.dev: 'delegate' in vite.config.mjs:
import { defineConfig } from 'vite';
import { view } from 'vite-plugin-view';
export default defineConfig({
plugins: [
view({
engine: 'ejs',
extension: '.ejs',
// Pass strategy as an object; the 'dev' sub-option governs the dev server
strategy: {
dev: 'delegate'
},
// MPA entry object: key = output HTML filename, value = template file
entry: {
index: 'index.ejs',
home: 'home.ejs',
},
engineOptions: {
title: 'EJS Delegate Example',
items: ['Alpha', 'Beta', 'Gamma'],
pageTitle: 'Home (delegate)',
},
}),
],
build: {
// IIFE builds require enabling code splitting explicitly
rolldownOptions: {
output: {
codeSplitting: true,
},
},
},
});Template examples
index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<ul>
<% items.forEach(function(item) { %>
<li><%= item %></li>
<% }); %>
</ul>
<script type="module" src="/src/index.ts"></script>
</body>
</html>home.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Multi-Page: <%= title %></title>
</head>
<body>
<h1>Multi-Page Example · <%= pageTitle %></h1>
<p data-page="home">Home page rendered via vite-plugin-view middleware.</p>
<script type="module" src="/src/index.ts"></script>
</body>
</html>Runtime behavior
- On the first request to
/:- Render
index.ejsand write a siblingindex.html - If a user-owned
index.htmlalready exists, back it up toindex.html.bak_<timestamp> - Call
next()so Vite's nativehtmlFallbackMiddleware→indexHtmlMiddlewareprocesses the URL
- Render
- On the first request to
/home: renderhome.ejs→ writehome.html→ native pipeline takes over - Subsequent visits to the same URL are skipped via the
delegateWrittenMap keyed on URL - On process exit (Ctrl+C / kill / crash): backups are restored and generated files are cleaned up
The build output is governed independently by
strategy.build(defaults to'html'). See the next section.
Emitting raw templates with asset tags via strategy: { build: 'template' }
When to use it
In a decoupled front-end / back-end architecture, the Node back-end often re-renders templates at runtime with dynamic data (user profile, i18n, A/B test variables). In that situation you do not want Vite to compile templates into "static HTML". Instead you want the build to:
- Emit the original template files (
.ejs/.pug/ …) todistwith<%= %>/#{ }syntax left intact - Correctly inject the Vite-built JS/CSS asset tags into each template so the chunk paths match the real built artifacts
- Optionally skip
.htmloutput (or output both, your call)
Set strategy.build to 'template' (or 'both') for this behaviour.
Installation (same as above, skipped)
Configuration
Configure EJS + MPA + strategy.build: 'template' together with the optional injectPlaceholder to precisely control where asset tags land:
import { defineConfig } from 'vite';
import { view } from 'vite-plugin-view';
export default defineConfig({
plugins: [
view({
engine: 'ejs',
extension: '.ejs',
strategy: {
// dev defaults to 'intercept' (in-memory render), build emits templates
build: 'template'
},
// Replace this exact placeholder with the generated asset tags.
// When omitted (or the placeholder is not found) tags are injected
// before </head> (Vite-native injectToHead behavior).
injectPlaceholder: '<!-- VITE_ASSETS -->',
entry: {
index: 'index.ejs',
home: 'home.ejs',
},
engineOptions: {
title: 'EJS Build Template Example',
items: ['Alpha', 'Beta', 'Gamma'],
pageTitle: 'Home (template)',
},
}),
],
build: {
outDir: 'dist',
rolldownOptions: {
output: {
codeSplitting: true,
},
},
},
});Placing the injection placeholder in templates (optional)
When you don't want the default </head>-before injection (e.g. because back-end tags must come after Vite assets), place a custom placeholder in the template:
index.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><%= title %></title>
<!-- VITE_ASSETS -->
<%# other head tags injected by the backend go here %>
</head>
<body>
<h1><%= title %></h1>
<ul>
<% items.forEach(function(item) { %>
<li><%= item %></li>
<% }); %>
</ul>
<script type="module" src="/src/index.ts"></script>
</body>
</html>After building, <!-- VITE_ASSETS --> inside dist/index.ejs is replaced with:
<script type="module" crossorigin src="/assets/index-abc123.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-def456.css">The EJS syntax (<%= title %>, <% items.forEach(...) %>) is preserved verbatim so the back-end can re-render them with dynamic values at request time.
Pug template example
Pug uses indentation-sensitive syntax where raw HTML tags like <script src="x"> are not valid. The plugin automatically converts Vite-generated <script> / <link> tags into Pug-native syntax (e.g. script(type="module", crossorigin, src="...")) before injecting.
Configuration (Pug + strategy.build: 'template' + injectPlaceholder):
import { defineConfig } from 'vite';
import { view } from 'vite-plugin-view';
export default defineConfig({
plugins: [
view({
engine: 'pug',
strategy: {
build: 'template'
},
// Use a //- comment as the placeholder in Pug; it disappears after replacement
injectPlaceholder: '//- VITE_ASSETS',
entry: {
index: 'index.pug'
},
engineOptions: {
title: 'Pug Build Template Example'
}
})
],
build: {
outDir: 'dist',
rolldownOptions: {
output: {
codeSplitting: true
}
}
}
});index.pug:
doctype html
html(lang='en')
head
meta(charset='UTF-8')
title= title
//- VITE_ASSETS
body
h1= title
#root
script(src='./src/main.ts' type='module')After building, dist/index.pug (placeholder replaced with Pug-native tags, template syntax = title / #root preserved, original entry script(src='./src/main.ts' ...) removed):
doctype html
html(lang='en')
head
meta(charset='UTF-8')
title= title
script(type="module", crossorigin, src="/assets/index-abc123.js")
link(rel="stylesheet", crossorigin, href="/assets/index-def456.css")
body
h1= title
#rootIf
injectPlaceholderis not set, the plugin automatically locates theheaddeclaration line and inserts the converted Pug tags at the same indentation level as existing children.
strategy.build modes compared
strategy.build value | Emits .html to dist | Emits raw template + asset tags to dist | Use case |
|---|---|---|---|
'html' (default) | ✅ | ❌ | Purely static front-end deployment (legacy behaviour) |
'template' | ❌ | ✅ .ejs / .pug / ... | Backend re-rendering (SSR / template proxying) |
'both' | ✅ | ✅ | Deploying a static site and back-end templates side by side |