When using: copy-webpack-plugin

And specifying to: as: 'images/[name].[contenthash][ext]'

How do you reference these files in your templates without using html-webpack-plugin or handlebars-loader?

I have solved this with the following solution:

/**
 * Create a new plugin class.
 */
const StoreAssetsInfoKeysPlugin = class StoreAssetsInfoKeysPlugin {
  /**
   * Define `apply` as its prototype method.
   * @param hooks
   */
  apply({ hooks }) {
    /**
     * Using the 'afterCompile' hook:
     * Use the 'assetsInfo' Map which contains the output of all asset paths,
     * construct an array containing all these paths,
     * then write an 'assets.json' file somewhere in your project.
     */
    hooks.afterCompile.tapAsync('StoreAssetsInfoKeysPlugin', ({ assetsInfo }, callback) => {
      fs.writeFileSync('/path/to/assets.json', JSON
        .stringify(Array
          .from(assetsInfo.entries())
          .map(([key]) => `/assets/${key}`)));

      callback();
    });
  }
};
/**
 * The Webpack configuration for this
 * example looks like this.
 */
export default {
  plugins: [
    new StoreAssetsInfoKeysPlugin(),
    new CopyPlugin({
      patterns: [{
        from: '/path/to/images/*',
        to: 'images/[name].[contenthash][ext]',
      }],
    }),
  ],
};
/**
 * Create a helper function which will find the
 * correct path from the 'assets.json'
 */
export const getAssetPath = function getAssetPath(directory, filename, ext) {
  return JSON
    .parse(fs.readFileSync('/path/to/assets.json'))
    .find((value) => value
      .match(`^.*?\\b${directory}\\b.*?\\b${filename}\\b.*?\\.${ext}\\b.*?$`));
};
/**
 * Then use the helper function within your
 * server-side templating engine:
 */
getAssetPath('images', 'my-image', 'svg');

Is this a suitable solution?

This was my first solution to achieve obtaining the file names with a [contenthash] substitution in server-side templates.

I'm new to Webpack so any thoughts on this approach would be appreciated.

1

There are 1 answers

0
biodiscus On

I try to define the goal in detail:

  1. we have a template file (e.g. index.hbs)
  2. in the source template file should be referenced source files of scripts, styles, images, and other assets
  3. in the processed template file should be hashed output filenames containing contenthash
  4. the template file must not be rendered into HTML, to use the template by server-side rendering

Is the goal correct?

If yes, then you can use one powerful "html-bundler-webpack-plugin" without html-webpack-plugin, handlebars-loader, copy-webpack-plugin.

For example, there is the file structure of the example:

src/views/index.hbs
src/scss/styles.scss
src/js/main.js
src/images/picture.png

dist/ <= output directory for processed files

There is source template file src/views/index.hbs (or other HTML file):

<html>
<head>
  <!-- source files with path relative to the template file -->
  <link href="../scss/styles.scss" rel="stylesheet">
  <script src="../js/main.js" defer="defer"></script>
</head>
<body>
  {{> header }}
  <h1>Hello World!</h1>
  
  <!-- source image file with path relative to the template file -->
  <img src="../images/picture.png">

  {{> footer }}
</body>
</html>

The webpack.config.js:

const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'), // output path for processed files
  },,
  plugins: [
    new HtmlBundlerPlugin({
      entry: {
        // define templates here
        // the key is output file path w/o extension, e.g.:
        index: 'src/views/index.hbs', // => dist/index.hbs
      },
      js: {
        // output filename of compiled JavaScript
        filename: 'js/[name].[contenthash:8].js',
      },
      css: {
        // output filename of extracted CSS
        filename: 'css/[name].[contenthash:8].css',
      },
      // defaults used Eta (ESJ like) template engine to render into HTML
      // if you will render .hbs to .html, then define as `handlebars`
      preprocessor: false, // <= disable rendering into HTML to keep original template content
    }),
  ],

  module: {
    rules: [
      {
        test: /\.(scss)$/,
        use: ['css-loader', 'sass-loader'],
      },
      {
        test: /\.(ico|png|jp?g|svg)/,
        type: 'asset/resource',
        generator: {
          // output filename of images
          filename: 'img/[name].[hash:8][ext]',
        },
      },
    ],
  },
};

If you will keep original template content and only replace source asset files with their hashed output filenames, then disable the rendering using the preprocessor: false plugin option.

The processed (not rendered to html) template will be looks like:

<html>
<head>
  <!-- output filenames relative to the dist/ directory -->
  <link href="css/styles.0f23efdf.css" rel="stylesheet" />
  <script src="js/main.5317c1f6.js" defer="defer"></script>
</head>
<body>
  {{> header }}
  <h1>Hello World!</h1>

  <!-- output filenames relative to the dist/ directory -->
  <img src="img/picture.7b396424.png" />

  {{> footer }}
</body>
</html>

If you will render any template into HTML, you can use one of supported templating engines "out of the box": Eta, EJS, Handlebars, Nunjucks, LiquidJS.

The config of the plugin to render handlebars template to HTML:

  new HtmlBundlerPlugin({
      entry: {
        // define templates here
        // the key is output file path w/o extension, e.g.:
        index: 'src/views/index.hbs', // => dist/index.html
      },
      js: {
        // output filename of compiled JavaScript
        filename: 'js/[name].[contenthash:8].js',
      },
      css: {
        // output filename of extracted CSS
        filename: 'css/[name].[contenthash:8].css',
      },
      // specify the `handlebars` template engine
      preprocessor: 'handlebars',
      // define handlebars options
      preprocessorOptions: {
        partials: ['src/views/partials'],
      },
    })

The rendered HTML:

<html>
<head>
  <!-- output filenames relative to the dist/ directory -->
  <link href="css/styles.0f23efdf.css" rel="stylesheet" />
  <script src="js/main.5317c1f6.js" defer="defer"></script>
</head>
<body>
  <div class="header">html of your header partials</div>
  <h1>Hello World!</h1>

  <!-- output filenames relative to the dist/ directory -->
  <img src="img/picture.7b396424.png" />

  <div class="footer">html of your footer partials</div>
</body>
</html>