Server rendering
With ssr: true, the plugin renders every page on the server in the dev server, and vite build builds a server bundle next to the browser one. This site is the worked example: an Angular app compiled by vite-plugin-ngp, rendered to static HTML with Angular SSR at build time, and hydrated in the browser. This page shows how to set up the same for your app, and what the plugin doesn't do for you.
Turn it on#
import {defineConfig} from 'vite';
import {ngp} from 'vite-plugin-ngp';
export default defineConfig({
plugins: [ngp({ssr: true})],
});ssr: true expects the server entry at src/main.server.ts. To use another file, write ssr: {entry: 'src/server.ts'}.
The server entry#
The entry renders one page. Its default export takes the URL and the contents of index.html, and resolves to the rendered HTML. This site's src/main.server.ts:
import {mergeApplicationConfig} from '@angular/core';
import {bootstrapApplication, type BootstrapContext} from '@angular/platform-browser';
import {provideServerRendering, renderApplication} from '@angular/platform-server';
import {App} from './app/app';
import {appConfig} from './app/app.config';
const serverConfig = mergeApplicationConfig(appConfig, {providers: [provideServerRendering()]});
export default function render(url: string, document: string): Promise<string> {
return renderApplication((context: BootstrapContext) => bootstrapApplication(App, serverConfig, context), {
document,
url,
allowedHosts: ['localhost'],
});
}The browser side adds hydration, so Angular reuses the server's DOM instead of rendering the page again:
provideClientHydration(withEventReplay())What the plugin does#
In the dev server, a middleware renders every page request through your entry, loaded by Vite's ssr environment. It passes the entry index.html as Vite transformed it, and returns the rendered page for the browser to hydrate. Edit a template and the browser updates in place, while the next server render shows the edit too.
In server environments, the plugin compiles your components with their own ngp compiler, without HMR code, because HMR code imports updates from URLs only a browser can load. It defines ngServerMode as true. It also bundles every Angular library into the server build and links it: @angular/*, plus every package in your dependencies that depends on @angular/core, such as Angular Material or NgRx. Vite usually leaves node_modules for Node to load, and Node would load those libraries unlinked, which only works with the JIT compiler. An Angular library listed under devDependencies isn't found; add it to resolve.noExternal yourself.
In vite build, it builds both environments: the browser bundle into dist/client, and your entry into dist/server/main.server.js. Serving them is up to you, with any Node server that calls render for page requests and serves dist/client for everything else. With the i18n option, both folders get one subfolder per locale, such as dist/server/de/main.server.js, and your server picks the bundle by the URL's first segment. Internationalization has the details.
Prerendering for static hosting#
GitHub Pages serves files, not a Node server, so this site renders every route at build time instead. A short script imports the server bundle, renders each route into the built index.html, and writes the result where Pages expects it:
const template = await readFile('dist/client/index.html', 'utf8');
const {default: render, routes} = await import('./dist/server/main.server.js');
for (const route of routes) {
const html = await render(`http://localhost/vite-angular/${route}`, template);
await writeFile(`dist/client/${route}/index.html`, html);
}routes is a named export from the same server entry. It comes from the list that builds the site's router and sidebar, so a new page gets prerendered without touching the script. The site also renders a 404.html, which GitHub Pages serves for any unknown path. Rendering the site's 14 pages took 5 to 34 ms each in the build of 17 October 2026, on an M2 Max.
Limits#
- No status codes.
renderreturns HTML only. Behind a Node server, a not-found page answers 200. Angular's way to set the status,@angular/ssr's request and response tokens, isn't wired up. - No
@angular/ssrfeatures. Per-route render modes andAngularNodeAppEnginearen't supported. You write the server, or the prerender loop, yourself. - Two analyzers in dev. The browser and server compilers each hold an ngp analyzer, so SSR roughly doubles the dev server's analyzer memory.