npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

html-render-webpack-plugin

v3.1.0

Published

webpack plugin for rendering static HTML in a multi-config webpack build

Readme

html-render-webpack-plugin

npm

Render HTML files with JavaScript during a webpack build.

Requirements

  • Node >= 22.13
  • webpack ^5.0.0
  • Express >= 4, only if you use createDevRouter

Install

pnpm add webpack html-render-webpack-plugin
# Only needed for the development server:
pnpm add express

Express is an optional peer dependency. Build-only usage does not require it.

Quick start

A single webpack compilation can render HTML when it targets Node and exports a function. This is the smallest useful setup.

src/render.js

export default function render({ route }) {
  return `<!doctype html>
<html lang="en">
  <body>
    <h1>Rendered ${route}</h1>
  </body>
</html>`;
}

webpack.config.js

import path from "node:path";

import { HtmlRenderPlugin } from "html-render-webpack-plugin";

const htmlRenderPlugin = new HtmlRenderPlugin({
  routes: ["/"],
  renderDirectory: path.resolve("dist"),
});

export default {
  name: "render",
  target: "node",
  mode: "production",
  entry: path.resolve("src/render.js"),
  output: {
    // Keep the Node renderer out of the folder you deploy.
    path: path.resolve(".render"),
    filename: "render.js",
    // The plugin evaluates the default export, so expose it as a UMD2 module.
    library: { type: "umd2", export: "default" },
  },
  plugins: [htmlRenderPlugin.rendererPlugin],
};

The build writes dist/index.html. The renderer bundle is written to .render/ and is not part of the static site.

CommonJS configs can use:

const { HtmlRenderPlugin } = require("html-render-webpack-plugin");

Multiple configurations

Use two compilations when the renderer needs information from a browser build, such as a hashed script filename.

Add htmlRenderPlugin.statsCollectorPlugin to every compilation whose stats the renderer needs. Add htmlRenderPlugin.rendererPlugin to the compilation that renders HTML.

webpack.config.js

import path from "node:path";

import {
  HtmlRenderPlugin,
  getEntrypointScriptUrls,
} from "html-render-webpack-plugin";

const htmlRenderPlugin = new HtmlRenderPlugin({
  routes: ["/", "/about"],
  renderDirectory: path.resolve("dist"),
  // Keep the renderer boundary small: pass URLs, not the whole stats object.
  mapStatsToParams: ({ webpackStats }) => ({
    scripts: getEntrypointScriptUrls(webpackStats),
  }),
});

export default [
  {
    name: "client",
    target: "web",
    mode: "production",
    entry: path.resolve("src/client.js"),
    output: {
      path: path.resolve("dist"),
      filename: "assets/[name].[contenthash].js",
      chunkFilename: "assets/[name].[contenthash].js",
      publicPath: "/",
    },
    plugins: [htmlRenderPlugin.statsCollectorPlugin],
  },
  {
    name: "render",
    target: "node",
    mode: "production",
    // Optional: the plugin already waits for every compilation. Declaring the
    // dependency makes the intended build order explicit.
    dependencies: ["client"],
    entry: path.resolve("src/render.js"),
    output: {
      path: path.resolve(".render"),
      filename: "[name].js",
      library: { type: "umd2", export: "default" },
    },
    plugins: [htmlRenderPlugin.rendererPlugin],
  },
];

src/render.js

export default function render({ route, scripts }) {
  return `<!doctype html>
<html lang="en">
  <body>
    <div id="app">Rendered ${route}</div>
    ${scripts
      .map((src) => `<script src="${src}" defer></script>`)
      .join("\n    ")}
  </body>
</html>`;
}

getEntrypointScriptUrls(webpackStats, entrypoint = "main", compilation?) returns public URLs for the JavaScript assets of an entrypoint. It handles hashed filenames, publicPath values with or without a trailing slash, string assets and asset objects. An unknown entrypoint or compilation name throws instead of returning an empty list.

Inside mapStatsToParams the stats are already client-only: the plugin only collects compilations that use statsCollectorPlugin. The stats webpack passes to its build callback include every compilation, including the renderer, so pass the compilation name (the webpack name of that compilation) there. When several compilations define the entrypoint, compilation is required; scripts from different compilations are never combined.

If you need more control, mapStatsToParams receives the route and the client stats, and whatever it returns is merged into the renderer's parameters.

Routes

routes accepts strings or objects. A route may start with or without a leading slash. Paths are appended with index.html unless they already end in .html. Normalization applies to the generated file path and the dev-router path only: the renderer still receives the route string exactly as configured, so "about" stays "about" in the page while writing about/index.html.

const routes = [
  "/", // dist/index.html
  "/about", // dist/about/index.html
  "contact", // dist/contact/index.html
  { route: "/blog/hello", title: "Hello", category: "news" },
];

Extra keys on a route object are passed to the renderer, which makes routes a convenient place for page metadata:

new HtmlRenderPlugin({
  routes: [
    { route: "/", title: "Home", description: "Welcome" },
    { route: "/about", title: "About", description: "How it works" },
  ],
  // mapStatsToParams also receives the route:
  mapStatsToParams: ({ title, webpackStats }) => ({
    title,
    scripts: getEntrypointScriptUrls(webpackStats),
  }),
});

The renderer receives { route, title, scripts, webpackStats, ... } depending on what mapStatsToParams returns.

Options

routes

Default: [""]

Routes to render. A file is created for each route. "" and "/" both render to index.html.

renderEntry

Default: "main"

The webpack entry in the renderer compilation. Set this when the renderer uses object entry syntax.

new HtmlRenderPlugin({ renderEntry: "myRender" });

renderDirectory

Default: "dist"

The directory to write HTML files to, resolved from the current working directory. This can differ from the renderer compilation's output.path; that is the recommended setup, so the Node renderer bundle is not deployed with the static site.

mapStatsToParams

Default: ({ webpackStats }) => ({ webpackStats: webpackStats.toJson() })

Builds the values passed to the render function for each route. The function receives the route fields plus webpackStats, and its return value is merged into the render parameters.

Prefer passing only what the renderer needs:

mapStatsToParams: ({ webpackStats }) => ({
  scripts: getEntrypointScriptUrls(webpackStats),
});

In a single-compilation setup the renderer's own stats are used. If you do not need stats at all, return an empty object:

mapStatsToParams: () => ({});

transformFilePath

Default: (route) => route.route

Changes the output path for a route.

new HtmlRenderPlugin({
  routes: [
    { route: "/about", language: "en-us" },
    { route: "/about", language: "en-au" },
  ],
  transformFilePath: ({ route, language }) => `${language}${route}`,
});

This writes en-us/about/index.html and en-au/about/index.html.

renderConcurrency

Default: "serial"

Use "parallel" when renders do a lot of asynchronous work.

new HtmlRenderPlugin({ renderConcurrency: "parallel" });

skipAssets

Default: false

When true, routes are not rendered to files after the build. Use this in watch mode with createDevRouter or renderWhenReady, where pages are rendered on demand.

transformExpressPath

Default: (route) => route.route

Changes the path used by the dev router. Routes are normalized to a leading slash, so "about" and "/about" both work. The default "" route is served at /. Return undefined to exclude a route from the dev router; an empty string excludes every route except the default one.

new HtmlRenderPlugin({
  transformExpressPath: (route) =>
    route.route === "/draft" ? undefined : `/static${route.route}`,
});

getRouteFromRequest

Used by the dev router to choose a route for a request. It must return one of the configured routes, or undefined to pass the request to the next middleware.

new HtmlRenderPlugin({
  routes: [
    { route: "/books/1", id: "1" },
    { route: "/books/2", id: "2" },
  ],
  getRouteFromRequest: (req, routes) =>
    routes.find((route) => route.id === req.params.id),
});

extraGlobals

Extra values to expose as free variables in the evaluated renderer bundle. Names must be valid JavaScript identifiers and cannot be exports, require, module, __filename or __dirname.

Development server

The dev router renders configured routes on demand with the same renderer bundle used by the production build. Use it with Express and webpack-dev-middleware so client assets are served from memory.

Set skipAssets: true on the plugin when using the dev router. Share the same plugin instance between the webpack configuration and the server.

webpack.config.js

export const htmlRenderPlugin = new HtmlRenderPlugin({
  routes: ["/", "/about"],
  skipAssets: true,
});

dev-server.js

import express from "express";
import webpack from "webpack";
import devMiddleware from "webpack-dev-middleware";

import config, { htmlRenderPlugin } from "./webpack.config.js";

const compiler = webpack(config);
const app = express();

// Mount the renderer first, then serve client assets from webpack.
app.use(htmlRenderPlugin.createDevRouter());
app.use(devMiddleware(compiler, { publicPath: "/" }));

app.listen(3000, () => {
  console.log("http://localhost:3000");
});

If you use webpack-dev-server v5 instead, mount the router in setupMiddlewares:

devServer: {
  setupMiddlewares(middlewares, devServer) {
    devServer.app.use(htmlRenderPlugin.createDevRouter());
    return middlewares;
  },
}

The dev router does not add hot module replacement. It rerenders a route when it is requested after a rebuild.

Just-in-time rendering

renderWhenReady renders any route using the same renderer. It waits until the build is ready, so it is safe to call during startup.

app.get("/books/:id", (req, res) => {
  res.send(await htmlRenderPlugin.renderWhenReady({ route: "/books/_id" }));
});

Only use routes that also exist in the production build. Errors contain a webpackStats property when stats are available.

Output layout

A typical multi-compilation project looks like this:

dist/                     # deploy this
  index.html
  about/index.html
  assets/main.<hash>.js
  assets/<chunk>.<hash>.js
.render/                  # renderer bundle, do not deploy
  render.js

Keep the renderer output outside renderDirectory. Avoid output.clean across compilations that share an output directory; clean the directories yourself before the build instead.

TypeScript

The package ships declarations and exports the public option and route types. createDevRouter returns a structural DevRouter type, so build-only usage does not need Express types.

import {
  HtmlRenderPlugin,
  getEntrypointScriptUrls,
  type BaseRoute,
  type MapStatsToParams,
  type Options,
} from "html-render-webpack-plugin";

interface Route extends BaseRoute {
  title: string;
}

const mapStatsToParams: MapStatsToParams<Route> = ({
  route,
  title,
  webpackStats,
}) => ({
  route,
  title,
  scripts: getEntrypointScriptUrls(webpackStats),
});

const options: Options<Route> = {
  routes: [{ route: "/", title: "Home" }],
  mapStatsToParams,
};

const plugin = new HtmlRenderPlugin<Route>(options);

Examples

The fixtures under tests/test-cases are complete, buildable examples, including single and multi-compilation setups, a dev server, route metadata and asynchronous rendering.

Migration

Upgrading from v1? See the v1 to v2 migration guide.