DEV Community

Mark
Mark

Posted on

Multiple HTML Files Using Webpack

Frameworks like React and Vue are single-page applications, which means generating a single HTML file with all JS referenced and executed within this page.

In this article, we're going to discuss how to package multi-page application in webpack, that is, generating multiple HTML files during the packaging process, because there might still be scenarios where multi-page applications are used in some older project.

We will use the html-webpack-plugin to achieve this.

Let's Write Some Code

Besides the entry file index.js, we will create two more files: list.js and detail.js. We will use these three as the files to be bundled.

// index.js
import React, { Component } from 'react';
import { createRoot } from 'react-dom/client';
class App extends Component {
    render() {
        return <div>Index Page</div>;
    }
}
const root = createRoot(document.getElementById('app'));
root.render(React.createElement(App));

// list.js
import React, { Component } from 'react';
import { createRoot } from 'react-dom/client';
class App extends Component {
    render() {
        return <div>List Page</div>;
    }
}
const root = createRoot(document.getElementById('app'));
root.render(React.createElement(App));

// details.js
import React, { Component } from 'react';
import { createRoot } from 'react-dom/client';
class App extends Component {
    render() {
        return <div>Details Page</div>;
    }
}
const root = createRoot(document.getElementById('app'));
root.render(React.createElement(App));

Enter fullscreen mode Exit fullscreen mode

Now we have three pages, and I want to generate three HTML pages: index.html, list.html, and details.html. How should we configure this?

Configuring Multiple Entries

Now that we have three js files, we need to configure three entry points:

...

module.exports = {
  entry: {
    main: "./src/index.js",
    list: "./src/list.js",
    details: "./src/details.js",
  },
  output: {
    clean: true,
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html', // to import index.html file inside index.js
      filename: 'index.html',
    })
  ],
optimization: {
    usedExports: true,
    runtimeChunk: {
      name: 'runtime',
    },
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          name: 'vendors',
        }
      }
    },
  ...
}

...
Enter fullscreen mode Exit fullscreen mode

After running 'npm run build', we can see that it has bundled details..js and list..js:

Image description

However, there is only one HTML file, and this HTML page includes all the js files we have bundled.

Image description

This is not the result we want. What we want is to include index.js in index.html, list.js in list.html, and details.js in details.html.

In this case, we need to use html-webpack-plugin to help us generate several more pages, and include the corresponding js files or chunks.

Configuring html-webpack-plugin

To generate multiple HTML files, we still need to use the handy tool html-webpack-plugin. It has many parameters that you can refer to in the official html-webpack-plugin documentation. Here, we will use the 'filename' and 'chunks' parameters, which correspond to the names of the generated HTML files and the chunks to be included in the page, respectively. We modify the webpack.config.js configuration and add two more instances of html-webpack-plugin:

...

module.exports = {
  entry: {
    main: "./src/index.js",
    list: "./src/list.js",
    details: "./src/details.js",
  },
  ...
  plugins: [
    ...
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'index.html',
      chunks: ['runtime', 'vendors', 'main'],
    }),
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'list.html',
      chunks: ['runtime', 'vendors', 'list'],
    }),
    new HtmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'details.html',
      chunks: ['runtime', 'vendors', 'details'],
    }),
    ...
  ],
  optimization: {
    usedExports: true,
    runtimeChunk: {
        name: 'runtime',
    },
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          name: 'vendors',
        }
      }
    },
  },
}

...
Enter fullscreen mode Exit fullscreen mode

The 'chunks' configuration in HtmlWebpackPlugin refers to the JavaScript files, which are included into the HTML after being bundled by webpack.

Let's re-package it by running 'npm run build'. Three HTML files are generated in the 'dist' directory, and each HTML file includes the corresponding JavaScript:

Image description

index.html

Image description

list.html

Image description

details.html

Image description

Everything has been successfully bundled. When we open each page, they all run normally.

Configuration Optimization

If we add a new entry point, we need to manually add another html-webpack-plugin and set the corresponding parameters.

How do we automatically add a new html-webpack-plugin based on the entry point, let's just do it.

We can assemble the html-webpack-plugin based on the entry file:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const entry = {
  main: "./src/index.js",
  list: "./src/list.js",
  detail: "./src/detail.js",
}

module.exports = {
...
  entry: entry,
  output: {
    clean: true,
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js'
  },
  plugins: [
    ...Object.keys(entry).map(item => // loop entry files and map HtmlWebpackPlugin
      new HtmlWebpackPlugin({
        template: 'src/index.html',
        filename: `${item}.html`,
        chunks: ['runtime', 'vendors', item]
      })
    )
  ],
  optimization: {
    usedExports: true,
    runtimeChunk: {
      name: 'runtime',
    },
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          name: 'vendors',
        }
      }
    },
  }
...
};

Enter fullscreen mode Exit fullscreen mode

Let's re-package it by running 'npm run build'. The page has been successfully bundled.

At this point, when we add a new entry, all we need to do is configure the entry file. For example, if we want to add a 'userInfo' page, we just need to configure it in the entry file:

entry: {
    index: "./src/index.js",
    list: "./src/list.js",
    details: "./src/details.js",
    userInfo: "./src/userInfo.js"
},
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
webdiscus profile image
webdiscus

instead of html-webpack-plugin you can use modern html-bundler-webpack-plugin to simplify multiple page configuration.

Using the HTML Bundler Plugin:

  • the entrypoint a template file, not JS
  • SCSS and JS/TS source files you can define directly in HTML using a relative path or webpack aliases
  • the plugin resolves source files of styles, scripts, images, fonts, etc. in templates and replaces them in the generated HTML with output filenames

This plugin replaces the functionality of the plugins and loaders:

  • html-webpack-plugin
  • mini-css-extract-plugin
  • html-loader
  • style-loader
  • many other plugins and loaders

Then you webpack config will be yet more simple and clear:

module.exports = {
  // entry: {}, // <= no need more define JS files here, all JS files must defined directly in HTML
  ...
  plugins: [
    ...
    new HtmlBundlerPlugin({
      // automatically processing all HTML templates in the path
      entry: 'src/',
      // - OR - you can define each page manually
      entry: {
        'index':  'src/index.html', // => dist/index.html
        'list': 'src/list.html', // => dist/list.html
        'details': 'src/details.html', // => dist/details.html
      },
    }),
    ...
  ],
}
Enter fullscreen mode Exit fullscreen mode

Each page template should contains its owns SCSS and JS files, e.g.:

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <!-- relative path to favicon source file -->
    <link href="./favicon.ico" rel="icon" />
    <!-- relative path to SCSS source file -->
    <link href="./index.scss" rel="stylesheet" />
  </head>
  <body>
    <div id="root"></div>
    <!-- relative path to JS source file -->
    <script src="./index.js" defer="defer"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

src/details.html

<!DOCTYPE html>
<html>
  <head>
    <!-- relative path to favicon source file -->
    <link href="./favicon.ico" rel="icon" />
    <!-- relative path to SCSS source file -->
    <link href="./details.scss" rel="stylesheet" />
  </head>
  <body>
    <div id="root"></div>
    <!-- relative path to JS source file -->
    <script src="./details.js" defer="defer"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

etc.

So you can exactly define source files of styles and scripts at the exactly position in HTML.

The generated HTML contains URLs of the output filenames:

dist/details.html

<!DOCTYPE html>
<html>
  <head>
    <link href="img/favicon.ico" rel="icon" />
    <link href="css/details.05e4dd86.css" rel="stylesheet" />
  </head>
  <body>
    <div id="root"></div>
    <script src="js/details.f4b855d8.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

etc.