Build a static site generator with webpack

pldg profile image Luca Updated on ・3 min read

Write html contents using javascript template literals and output static html files on webpack compile time:

  • Use only HtmlWebpackPlugin, no additional plugins needed.
  • Automatically load the specific configuration for each page.
  • Router implementation, no .html extension in the url.
  • Support webpack-dev-server.

Source code


How to

Create a webpack entry point src/index.js.

console.log('Hello from index.js');

Instead of creating html pages using files with .html extension, write the html markup using javascript template literal. Create some javascript files inside src/views/. Each of those files represent an html page. You can name those files whatever you want (for example page1.js, page2.js, page3.js and so on). Those files will be converted by HtmlWebpackPlugin in static .html files.

const page1 = `<h1>Page 1</h1>`;

module.exports = page1;

Create a custom template for HtmlWebpackPlugin to dynamically load the correct page inside src/views/ folder.

  <%= require(`./src/views/${htmlWebpackPlugin.options.pageName}.js`) %>

Setup a blueprint configuration for HtmlWebpackPlugin. Use this blueprint to create different HtmlWebpackPlugin configurations for each page.

const generatePage = ({
  path = '',
  template = './html-template.ejs',
} = {}) => ({
  plugins: [
    new HtmlWebpackPlugin({
      filename: `${path && path + '/'}index.html`,

module.exports = generatePage;

Create a readFiles function which use nodejs to read all page files in the src/views/ directory and return their names.

// create an array list of all pages filenames (without extension)
const pageNames = [];
readFiles('./src/views', ({ name }) => pageNames.push(name));

Loop through each page name and load their specific HtmlWebpackPlugin configuration.

const pages = pageNames.map(pageName => generatePage({
  title: pageName,
  // if 'page1' then filename is 'index.html'
  // otherwise filename is 'page2/index.html'
  // and so on for every other page
  path: pageName === 'page1' ? '' : pageName,
  // `pageName` is a custom HtmlWebpackPlugin option
  // which is loaded in html-template.ejs

module.exports = pages;

Require all pages in webpack.config.js, you can use webpack-merge to compose multiple configurations.

const merge = require('webpack-merge');

module.exports = merge(...pages);

If you run webpack the structure in the dist folder will looks like this:

│    index.html
│--- page2/
│         index.html
│--- page3/

You can also implement a router in your app to navigate between pages. You don't have to add index.html at the end of your links. With this method you'll not see .html extension in the url.

const router = `
  <li><a href="/">Go to page1</a></li>
  <li><a href="/page2">Go to page2</a></li>
  <li><a href="/page3">Go to page3</a></li>

module.exports = router;

Then edit your src/views pages to include the router.

const router = require('./partials/router');

const page1 = `<h1>Page 1</h1>`;

module.exports = page1 + router;

Run webpack again and try to navigate between pages: http://www.mywebsite.com/ or http://www.mywebsite.com/page2 and so on.


You can't view your app via file:/// protocol because your router links are set as absolute path, you must use a local or a remote server.

HMR doesn't work. If you add HotModuleReplacementPlugin and devServer.hot: true views files will not be connected to devServer.


You can do a lot more: import variables inside template literals, use loop to generate multiple html tags, add other options to HtmlWebpackPlugin to further customize html-template.ejs, and so on.

If you're using VS Code and want a better typing experience for writing html inside template literals, I suggest template literal editor extension.

This article is part of my learn-webpack repositories collection.

Posted on by:


markdown guide

Is there a way to use this approach with css components?
When I try, style loader always fails with: ERROR in Template execution failed: ReferenceError: window is not defined.


Can you setup a minimal repository to view?


I'll set up a minimal repo when I find time, but meanwhile I figured out what the problem is in case anyone trips over the same problem.

Style-loader expects a DOM, which isn't there yet in the context of HtmlWebpackPlugin rendering the pages:


Minicssextract has the same issue:


So this boils down to a SSR problem for which the mini-css-extract issue discusses some possible solutions.

I ended up going the conventional way and included the css in a js-entrypoint.