webpacklodashhtml-webpack-pluginwebpack-html-loaderwebpack.config.js

How to generate html template for static page from header/footer (and css/js inject) in Webpack 4 (lodash template not working)?


I'm trying to build static html pages with webpack 4. I'm using html-webpack-plugin. I want to define some header.html and footer.html and import them later in all html pages. But I want also then my output js and css files automatically inject to this header and footer. Something like this:

header.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Some Title</title>
    <% for(var i=0; i < htmlWebpackPlugin.files.css.length; i++) {%>
        <link type="text/css" rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[i] %>">
    <% } %>
</head>
<body>

footer.html:

    <% for(var i=0; i < htmlWebpackPlugin.files.js.length; i++) {%>
        <script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
    <% } %>
</body>
</html>

index.hbs (or index.html):

<%= _.template(require('./../includes/header.html'))() %>

    <div class="content">
        <img src="img/bg/desktop/bg-index-03.jpg" width="500" height="500"/>
        <div class="imgtest"></div>
    </div> <!-- .content -->

<%= _.template(require('./../includes/footer.html'))() %>

But I get in dist/index.html some bug. These lines still the same and don't loading header/footer to index.html:

<%= _.template(require('./../includes/header.html'))() %>
...
<%= _.template(require('./../includes/footer.html'))() %>

For some reason lodash doesn't work...

UPD:

I changed src/html/views/index.hbs to src/html/views/index.html (and in plugin)

new HtmlWebpackPlugin({
  template: './src/html/views/index.html',
...

Add include line here:

{
    test: /\.html$/,
    include: path.resolve(__dirname, 'src/html/includes'),
    use: ['html-loader']
},

In header/footer I removed lodash code <% ... %>, leaving only clean html - so it work! Header/footer imported in index.html, but without css/js.

If I return lodash back in footer.html (or header.html)

<% for(var i=0; i < htmlWebpackPlugin.files.js.length; i++) {%>
    <script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
<% } %>

I get error:

ERROR in Template execution failed: ReferenceError: htmlWebpackPlugin is not defined

ERROR in   ReferenceError: htmlWebpackPlugin is not defined

  - lodash.templateSources[2]:10 eval
    lodash.templateSources[2]:10:19

  - index.html:102 
    D:/.../src/html/views/index.html:102:110

  - index.html:104 ./node_modules/html-webpack-plugin/lib/loader.js!./src/html/views/index.html.module.exports
    D:/.../src/html/views/index.html:104:3

  - index.js:393 
    [project]/[html-webpack-plugin]/index.js:393:16

  - runMicrotasks

  - task_queues.js:93 processTicksAndRejections
    internal/process/task_queues.js:93:5

  - async Promise.all

Why it happens? What's wrong? How to make lodash works in header/footer?

Or what is the best way to generate html templates?

file tree:

dist
│   index.html
├───css
│       main.css
│       main.css.map
├───fonts
├───img
├───js
│       main.js
│       main.js.map
│       vendors~main.js
│       vendors~main.js.map
src
├───favicon
├───fonts
├───html
│   ├───includes
│   │       footer.html
│   │       header.html
│   └───views
│           index.hbs
├───img
├───js
│       index.js
├───scss
│       fonts.scss
│       icomoon.scss
│       style.scss
package.json
package-lock.json
webpack.config.js

webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const OptimizeCssAssetWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';
const isProd = !isDev;

const optimization = () => {
  const config = {
    splitChunks: {
      chunks: 'all'
    }
  }
  if (isProd) {
    config.minimizer = [
      new OptimizeCssAssetWebpackPlugin(),
      new TerserWebpackPlugin()
    ]
  }
  return config
}

const filename = ext => isDev ? `[name].${ext}` : `[name].[hash].${ext}`;

const cssLoaders = extra => {
  const loaders = [
    {
      loader: MiniCssExtractPlugin.loader,
      options: {
        hmr: isDev,
        reloadAll: true
      },
    },
    {
      loader: 'css-loader',
      options: {
        url: false
      }
    }
  ];
  if (extra) {
    loaders.push(extra)
  }
  return loaders
}

const babelOptions = preset => {
  const opts = {
    presets: [
      '@babel/preset-env'
    ],
    plugins: [
      '@babel/plugin-proposal-class-properties'
    ]
  }

  if (preset) {
    opts.presets.push(preset)
  }

  return opts
}

module.exports = {
  mode: 'development',
  entry: [
    '@babel/polyfill',
    './src/js/index.js'
  ],
  output: {
    filename: 'js/' + filename('js'),
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    port: 4250,
    hot: isDev
  },
  devtool: isDev ? 'source-map' : '',
  resolve: {
    //extensions: ['.js', '.json', '.png'],
    alias: {
      '@views': path.resolve(__dirname, 'src/html/views'),
      '@includes': path.resolve(__dirname, 'src/html/includes'),
      '@scss': path.resolve(__dirname, 'src/scss'),
      '@': path.resolve(__dirname, 'src'),
    }
  },
  optimization: optimization(),
  module: {
    rules: [
      {
        test: /\.html$/,
        //include: path.resolve(__dirname, 'src/html/includes'),
        use: ['html-loader']
      },
      {
        test: /\.hbs$/,
        loader: 'handlebars-loader'
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: {
          loader: 'babel-loader',
          options: babelOptions()
        }
      },
      {
        test: /\.css$/,
        use: cssLoaders()
      },
      {
        test: /\.s[ac]ss$/,
        use: cssLoaders('sass-loader')
      },
      {
        test: /\.(png|jpg|svg|gif)$/,
        exclude: path.resolve(__dirname, 'src/fonts'),
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
              outputPath: path.resolve(__dirname, 'dist/img')
            }
          }
        ]
      },
      {
        test: /\.(ttf|otf|svg|woff|woff2|eot)$/,
        exclude: path.resolve(__dirname, 'src/img'),
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name].[ext]',
              outputPath: path.resolve(__dirname, 'dist/fonts')
            }
          }
        ]
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/html/views/index.hbs',
      minify: {
        collapseWhitespace: isProd
      },
      inject: false
    }),
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: 'css/' + filename('css'),
    }),
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, 'src/favicon'),
        to: path.resolve(__dirname, 'dist')
      },
      {
        from: path.resolve(__dirname, 'src/fonts'),
        to: path.resolve(__dirname, 'dist/fonts')
      },
      {
        from: path.resolve(__dirname, 'src/img'),
        to: path.resolve(__dirname, 'dist/img')
      }
    ])
  ]
};

package.json:

{
  "name": "Name",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack --mode development",
    "build": "cross-env NODE_ENV=production webpack --mode production",
    "watch": "cross-env NODE_ENV=development webpack --mode development --watch",
    "start": "cross-env NODE_ENV=development webpack-dev-server --mode development --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.9.0",
    "@babel/plugin-proposal-class-properties": "^7.8.3",
    "@babel/preset-env": "^7.9.5",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "copy-webpack-plugin": "^5.1.1",
    "cross-env": "^7.0.2",
    "css-loader": "^3.5.2",
    "file-loader": "^6.0.0",
    "html-loader": "^1.1.0",
    "html-webpack-plugin": "^4.2.0",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.13.1",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "raw-loader": "^4.0.1",
    "resolve-url-loader": "^3.1.1",
    "sass-loader": "^8.0.2",
    "style-loader": "^1.1.4",
    "terser-webpack-plugin": "^2.3.5",
    "webpack": "^4.42.1",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.10.3"
  },
  "browserslist": "defaults",
  "dependencies": {
    "@babel/polyfill": "^7.8.7",
    "bootstrap": "^4.4.1",
    "handlebars": "^4.7.6",
    "handlebars-loader": "^1.7.1",
    "jquery": "^3.5.0",
    "popper.js": "^1.16.1"
  }
}

Solution

  • Solved!

    I changed inject to true in HtmlWebpackPlugin.

    And I also added function generateHtmlPlugins() to auto adding all html-files to plugin from ./src/html/views.

    src/html/includes/header.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>Some Title</title>
    </head>
    <body>
    <header>header</header>
    

    src/html/includes/footer.html:

        <footer>footer</footer>
    </body>
    </html>
    

    src/html/views/index.html:

    <%= _.template(require('./../includes/header-main.html'))() %>
        <div class="content">
            <img src="img/bg/desktop/bg-index-03.jpg" width="500" height="500"/>
            <div class="imgtest"></div>
        </div>
    <%= _.template(require('./../includes/footer.html'))() %>
    

    dist/index.html:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
        <title>Some Title</title>
    <link href="css/main.43f2075150009f972ed4.css" rel="stylesheet"></head>
    <body>
    <header>header</header>
        <div class="content">
            <img src="img/bg/desktop/bg-index-03.jpg" width="500" height="500">
            <div class="imgtest"></div>
        </div>
        <footer>footer</footer>
    <script src="js/vendors~main.43f2075150009f972ed4.js"></script><script src="js/main.43f2075150009f972ed4.js"></script></body>
    </html>
    

    webpack.config.js:

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const TerserWebpackPlugin = require('terser-webpack-plugin');
    const OptimizeCssAssetWebpackPlugin = require('optimize-css-assets-webpack-plugin');
    const CopyWebpackPlugin = require('copy-webpack-plugin');
    const fs = require('fs');
    
    const isDev = process.env.NODE_ENV === 'development';
    const isProd = !isDev;
    
    function generateHtmlPlugins(templateDir) {
      const templateFiles = fs.readdirSync(path.resolve(__dirname, templateDir));
      return templateFiles.map(item => {
        const parts = item.split('.');
        const name = parts[0];
        const extension = parts[1];
        return new HtmlWebpackPlugin({
          filename: `${name}.html`,
          template: path.resolve(__dirname, `${templateDir}/${name}.${extension}`),
          minify: {
            collapseWhitespace: isProd
          },
          inject: true,
        })
      })
    }
    
    const htmlPlugins = generateHtmlPlugins('./src/html/views')
    
    const optimization = () => {
      const config = {
        splitChunks: {
          chunks: 'all'
        }
      }
      if (isProd) {
        config.minimizer = [
          new OptimizeCssAssetWebpackPlugin(),
          new TerserWebpackPlugin()
        ]
      }
      return config
    }
    
    const filename = ext => isDev ? `[name].${ext}` : `[name].[hash].${ext}`;
    
    const cssLoaders = extra => {
      const loaders = [
        {
          loader: MiniCssExtractPlugin.loader,
          options: {
            hmr: isDev,
            reloadAll: true
          },
        },
        {
          loader: 'css-loader',
          options: {
            url: false
          }
        }
      ];
      if (extra) {
        loaders.push(extra)
      }
      return loaders
    }
    
    const babelOptions = preset => {
      const opts = {
        presets: [
          '@babel/preset-env'
        ],
        plugins: [
          '@babel/plugin-proposal-class-properties'
        ]
      }
    
      if (preset) {
        opts.presets.push(preset)
      }
    
      return opts
    }
    
    module.exports = {
      mode: 'development',
      entry: [
        '@babel/polyfill',
        './src/js/index.js'
      ],
      output: {
        filename: 'js/' + filename('js'),
        path: path.resolve(__dirname, 'dist')
      },
      devServer: {
        port: 4250,
        hot: isDev
      },
      devtool: isDev ? 'source-map' : '',
      resolve: {
        //extensions: ['.js', '.json', '.png'],
        alias: {
          '@views': path.resolve(__dirname, 'src/html/views'),
          '@includes': path.resolve(__dirname, 'src/html/includes'),
          '@scss': path.resolve(__dirname, 'src/scss'),
          '@': path.resolve(__dirname, 'src'),
        }
      },
      optimization: optimization(),
      module: {
        rules: [
          {
            test: /\.html$/,
            include: path.resolve(__dirname, 'src/html/includes'),
            loader: 'html-loader',
            options: {
              minimize: false,
            }
          },
          {
            test: /\.js$/,
            exclude: /node_modules/,
            loader: {
              loader: 'babel-loader',
              options: babelOptions()
            }
          },
          {
            test: /\.css$/,
            use: cssLoaders()
          },
          {
            test: /\.s[ac]ss$/,
            use: cssLoaders('sass-loader')
          },
          {
            test: /\.(png|jpg|svg|gif)$/,
            exclude: path.resolve(__dirname, 'src/fonts'),
            use: [
              {
                loader: 'file-loader',
                options: {
                  name: '[name].[ext]',
                  outputPath: path.resolve(__dirname, 'dist/img')
                }
              }
            ]
          },
          {
            test: /\.(ttf|otf|svg|woff|woff2|eot)$/,
            exclude: path.resolve(__dirname, 'src/img'),
            use: [
              {
                loader: 'file-loader',
                options: {
                  name: '[name].[ext]',
                  outputPath: path.resolve(__dirname, 'dist/fonts')
                }
              }
            ]
          },
        ]
      },
      plugins: [
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
          filename: 'css/' + filename('css'),
        }),
        new CopyWebpackPlugin([
          {
            from: path.resolve(__dirname, 'src/favicon'),
            to: path.resolve(__dirname, 'dist')
          },
          {
            from: path.resolve(__dirname, 'src/fonts'),
            to: path.resolve(__dirname, 'dist/fonts')
          },
          {
            from: path.resolve(__dirname, 'src/img'),
            to: path.resolve(__dirname, 'dist/img')
          }
        ])
      ].concat(htmlPlugins)
    };