webdiscus / html-bundler-webpack-plugin

Alternative to html-webpack-plugin ✅ Renders Eta, EJS, Handlebars, Nunjucks, Pug, Twig templates "out of the box" ✅ Resolves source files of scripts, styles, images in HTML ✅ Uses a template as entry point
ISC License
138 stars 14 forks source link

Specifying custom path in output .html file for .css file url inside <link> tag #70

Closed sam1git closed 8 months ago

sam1git commented 8 months ago

I have directory structure as shown below. My webpack.prod.js file uses html-bundler-webpack-plugin as shown below.

The output as expected gives all .css and .js files inside build/static directory and all .html files inside build/html directory. The output html files have <link> tags that point to css like so href=/static/<filename>.css

Can I specify a custom path so that href looks like href=/filename.css while still preserving the output directory structure shown below and having all .css files inside build/static.

Why I want to do this? When I serve the output html files using node js, the path inside <link> is interpreted as relative to the url in the web browser. This way, I can have express.static serve the static folder with all static .css files inside it while controlling access to the html files rather than having all html files and static files inside one directory.

new HtmlBundlerWebpackPlugin({
    entry: './src/templates/',
    outputPath: path.join(__dirname, 'build/html/'),
    js: {
        filename: 'static/[name]-[contenthash].js',
    },
    css: {
        filename: 'static/[name]-[contenthash].css',
    },
})
| build
  | html
  | static
| package.json
| webpack.prod.js
| src
  | css
    | main.css
    | another.css
    | ...
  | images
    | image1.png
      ...
  | scripts
    | script1.js
    | script2.js
  | templates
    | file1.html
    | file2.html
      ...
| ...
webdiscus commented 8 months ago

Hello @sam1git,

thanks for the question.

I try to clarify your question. There is the source HTML template:

<html>
  <head>
    <link href="../styles/main.css" rel="stylesheet" />
    <script src="../scripts/main.js" defer="defer"></script>
  </head>
  <body>
    <img src="../images/image.webp" />
  </body>
</html>l

The plugin generate the HTML:

<html>
  <head>
    <link href="/static/main-e2ac2b4a.css" rel="stylesheet" />
    <script src="/static/main-5317c1f6.js" defer="defer"></script>
  </head>
  <body>
    <img src="/static/file1-1234abcd.png" />
  </body>
</html>l

But you want to have the output paths of assets w/o the /static leading path:

<html>
  <head>
    <link href="/main-e2ac2b4a.css" rel="stylesheet" />
    <script src="/main-5317c1f6.js" defer="defer"></script>
  </head>
  <body>
    <img src="/file1-1234abcd.png" />
  </body>
</html>l

I can have express.static serve the static folder with all static files

This means that the static prefix must be removed in the CSS files too. E.g. there is the source CSS:

.img {
  width: 160px;
  height: 130px;
  background-image: url(../images/file1.png);
}

defaults will be generated

.img {
  width: 160px;
  height: 130px;
  background-image: url(/static/file1-1234abcd.png);
}

but you want to have the following output path:

.img {
  width: 160px;
  height: 130px;
  background-image: url(/file1-1234abcd.png);
}

Is it right?

webdiscus commented 8 months ago

Because the generated /static/main-e2ac2b4a.cs output path is correct URL relative by build/ directory, is no right way to customise the publicPath different from the path where will be saved generated assets.

But if you will have incorrect (in terms of Webpack config) URLs in the generated HTML, then you can force transform the generated HTML before it will be saved. See please the beforeEmit callback.

For example:

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

module.exports = {
  output: {
    path: path.join(__dirname, 'dist/'),
    publicPath: '/',
  },

  plugins: [
    new HtmlBundlerPlugin({
      entry: {
        home: './src/views/home.html',
      },
      outputPath: 'html/', // html path relative by `dist/`
      js: {
        filename: 'static/[name]-[contenthash:8].js',
      },
      css: {
        filename: 'static/[name]-[contenthash:8].css',
      },
      // you can modify the generated HTML content here
      beforeEmit: (content, { assetFile }, compilation) => {
        console.log(compilation.assets); // => all assets used in the current entry template
        // remove the `/static/` prefix in generated HTML
        return content.replaceAll('="/static/', '="/');
      },
    }),
  ],

  module: {
    rules: [
      {
        test: /\.(css)$/,
        use: ['css-loader'],
      },
      {
        test: /\.(png|jpe?g|webp|ico|svg)$/,
        type: 'asset/resource',
        generator: {
          filename: 'static/[name].[hash:8][ext]',
        },
      },
    ],
  },
};

If you want remove the prefix in the CSS files, then you can modify the compilation.assets object:

      beforeEmit: (content, { assetFile }, compilation) => {
        const { RawSource } = compilation.compiler.webpack.sources;

        // remove the prefix in CSS files used in the HTML
        for (let filename in compilation.assets) {
          // skip not css files
          if (!/\.(css)$/.test(filename)) continue;

          let assetContent = compilation.assets[filename].source();
          assetContent = assetContent.replaceAll('/static/', '/');
          compilation.updateAsset(filename, new RawSource(assetContent));

          //console.log('>>: ', { filename }, assetContent);
        }

        // remove the prefix in the generated HTML
        return content.replaceAll('="/static/', '="/');
      },
sam1git commented 8 months ago

@webdiscus thank you for the answer. your understanding of the questions is correct and went one step ahead. I wasn't thinking of resolving paths correctly (or should I say incorrectly from webpacks perspective) in the .css output asset files. Suggested solution works like a charm. This is what my config file looks like now:

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

module.exports = {
    mode: "production",
    output: {
        path: path.resolve(__dirname, 'build'),
        publicPath: '/',
        assetModuleFilename: "static/[name]-[contenthash][ext]",
        clean: true,
    },
    plugins: [
        new HtmlBundlerWebpackPlugin({
            entry: './src/templates/',
            outputPath: path.join(__dirname, 'build/html/'),
            minify: {
                collapseWhitespace: true,
                keepClosingSlash: true,
                removeComments: true,
                removeRedundantAttributes: false,
                removeScriptTypeAttributes: true,
                removeStyleLinkTypeAttributes: true,
                useShortDoctype: true,
                minifyCSS: true,
                minifyJS: true,
            },              
            js: {
                filename: 'static/[name]-[contenthash].js',
            },
            css: {
                filename: 'static/[name]-[contenthash].css',
            },
            beforeEmit: (content, {assetFile}, compilation) => {
                const { RawSource } = compilation.compiler.webpack.sources;
                for (let filename in compilation.assets) {
                    if (!/\.(css)$/.test(filename)) continue;

                    let assetContent = compilation.assets[filename].source();
                    assetContent = assetContent.replaceAll('/static/', '/');
                    compilation.updateAsset(filename, new RawSource(assetContent));
                }
                return content.replaceAll('="/static/', '="/');
            }
        })
    ],
    module: {
        rules: [
            {
                test: /\.js$/i,
                exclude: [/node_modules/],
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                    },
                },
            },
            {
                test: /\.(png|jpe?g|gif|svg)$/i,
                type: "asset/resource",     
            },
            {
                test: /\.css$/i,
                use: [
                    'css-loader'
                ],
            }
        ],
    },
};
sam1git commented 8 months ago

Unrelated to the question in the heading and seems unrelated to applied code changes but for some reason CSS files are not being minified.

package.json:

  "devDependencies": {
    "@babel/preset-env": "^7.23.6",
    "babel-loader": "^9.1.3",
    "css-loader": "^6.8.1",
    "dotenv": "^16.3.1",
    "html-bundler-webpack-plugin": "^3.4.7",
    "nodemon": "^3.0.1",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  }
webdiscus commented 8 months ago

@sam1git

to minify CSS, add the sass-loader:

npm i sass-loader sass --save-dev

modify your webpack config:

{
  test: /\.css$/i,
  use: ['css-loader', 'sass-loader'],
}

The sass-loader can minify CSS.

webdiscus commented 8 months ago

Note: the minify.minifyCSS and minify.minifyJS options minify inlined CSS and JS code in HTML, not in separate files.

sam1git commented 8 months ago

Note: the minify.minifyCSS and minify.minifyJS options minify inlined CSS and JS code in HTML, not in separate files.

ah, i see. thank you.