-
Hi there! I use:
I would like to remove unused styles for each HTML page compiled using a template engine PurgeCSS can remove unused styles. I tried purgecss-webpack-plugin. But it can't see template parts, if I specify uncompiled templates in the arguments. My case. error-template.eta <!DOCTYPE html>
<html lang="en" class="h-100">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{= title }}</title>
<meta name="description" content="{{= description }}">
<link rel="stylesheet" href="./scss/error.scss">
</head>
<body class="d-flex w-100 h-100 mx-auto flex-column">
{{~ include("./parts/header.eta") }}
{{~ include("./parts/main.eta") }}
{{~ include("./parts/footer.eta") }}
</body>
</html> webpack config example: const sass = require('sass');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
// Configs
const isProduction = process.env.NODE_ENV === 'production';
const postCssConfig = require('./config/postcss.config');
// Paths
const srcDir = path.resolve(__dirname, 'src');
const outputDir = path.resolve(__dirname, 'public');
const outputAssetDir = 'static';
/** @type {import('webpack').Configuration} */
module.exports = {
mode: isProduction ? 'production' : 'development',
output: {
path: outputDir,
filename: `${outputAssetDir}/[name].[fullhash:7][ext][query]`,
publicPath: '',
assetModuleFilename: `${outputAssetDir}/[name][ext][query]`,
clean: true,
},
plugins: [
new HtmlBundlerPlugin({
preprocessor: 'eta',
preprocessorOptions: {
tags: ['{{', '}}'],
},
entry: {
index: `${srcDir}/index.eta`,
'error-404': {
import: `${srcDir}/error-template.eta`,
data: {
title: 'Not found',
description: 'The page has been moved, deleted, or the link is incorrect. We apologize for the inconvenience.',
},
},
'error-500': {
import: `${srcDir}/error-template.eta`,
data: {
title: 'Server error',
description: 'The server cannot respond to the request. Please try again later. We apologize for the inconvenience.',
},
},
'error-503': {
import: `${srcDir}/error-template.eta`,
data: {
title: 'We are improving our website',
description: 'This page is currently under maintenance. We will back soon with new and improved features.'
},
},
},
css: {
filename: `${outputAssetDir}/css/[name].[contenthash:7].css[query]`,
inline: true,
},
}),
],
module: {
rules: [
{
test: /\.s?css$/i,
use: [
{
loader: 'css-loader',
options: {},
},
isProduction && {
loader: 'postcss-loader',
options: {},
},
{
loader: 'sass-loader',
/** @type {import('sass-loader').Options} */
options: {
api: 'modern',
webpackImporter: true,
warnRuleAsWarning: true,
implementation: sass,
/** @type { import('sass').Options } */
sassOptions: {
loadPaths: ['node_modules'],
outputStyle: 'expanded',
},
},
},
].filter(Boolean),
},
{
test: /\.(jpe?g|png|webp|svg|ico)$/i,
type: 'asset',
},
],
},
}; Any idea? |
Beta Was this translation helpful? Give feedback.
Replies: 9 comments 2 replies
-
Hello @vralle, Thanks for the interesting use case. |
Beta Was this translation helpful? Give feedback.
-
I have created the purgecss example. add bootstrap in your SCSS file @use 'bootstrap';
// your styles add PurgeCSS plugin in Webpack config: const path = require('path');
const glob = require('glob'); // <= install
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin'); // <= install
// Configs
const isProduction = process.env.NODE_ENV === 'production';
const postCssConfig = require('./config/postcss.config');
// Paths
const srcDir = path.resolve(__dirname, 'src');
const outputDir = path.resolve(__dirname, 'public');
const outputAssetDir = 'static';
/** @type {import('webpack').Configuration} */
module.exports = {
mode: isProduction ? 'production' : 'development',
output: {
path: outputDir,
publicPath: '',
assetModuleFilename: `${outputAssetDir}/[name][ext][query]`,
clean: true,
},
plugins: [
new HtmlBundlerPlugin({
preprocessor: 'eta',
preprocessorOptions: {
tags: ['{{', '}}'],
},
entry: {
index: `${srcDir}/index.eta`,
'error-404': {
import: `${srcDir}/error-template.eta`,
data: {
title: 'Not found',
description: 'The page has been moved, deleted, or the link is incorrect. We apologize for the inconvenience.',
},
},
'error-500': {
import: `${srcDir}/error-template.eta`,
data: {
title: 'Server error',
description: 'The server cannot respond to the request. Please try again later. We apologize for the inconvenience.',
},
},
'error-503': {
import: `${srcDir}/error-template.eta`,
data: {
title: 'We are improving our website',
description: 'This page is currently under maintenance. We will back soon with new and improved features.'
},
},
},
js: {
// define JS output filename here to keep same settings in one place
filename: `${outputAssetDir}/js/[name].[contenthash:7].js[query]`, // <= important: use the `contenthash`
},
css: {
filename: `${outputAssetDir}/css/[name].[contenthash:7].css[query]`,
inline: true,
},
}),
isProduction && new PurgeCSSPlugin({
// scan *.html and *.eta files in src subdirs
paths: glob.sync(`${srcDir}/**/*.+(html|eta)`, { nodir: true }),
}),
].filter(Boolean),
module: {
rules: [
{
test: /\.s?css$/i,
use: [
{
loader: 'css-loader',
options: {},
},
isProduction && {
loader: 'postcss-loader',
options: {},
},
{
loader: 'sass-loader',
/** @type {import('sass-loader').Options} */
options: {},
},
].filter(Boolean),
},
{
test: /\.(jpe?g|png|webp|svg|ico)$/i,
type: 'asset',
},
],
},
}; |
Beta Was this translation helpful? Give feedback.
-
Hi @webdiscus. Thanks for the help! But when I tried to make the styles for the error-* pages as small as possible, I couldn't. The reason is obvious, the styles of simple error-* pages contain the styles of the landing page index.* and others. For this reason, the size of error-* pages will increase significantly. To keep the size of styles to a minimum, you need to clean each page individually. I imagined it looking like this: ...
postprocess(content, info, compilation) {
const compiledStyles = howGetCompiledStyleForPage(arguments);
const cleanedStyles = await new PurgeCSS().purge({
content: [
/** @type {import('purgecss').RawContent | string} */
{
raw: content,
extension: 'html',
}
],
/** */
css: [
/** @type {import('purgecss').RawCSS | string} */
compiledStyles
],
fontFace: true,
keyframes: true,
variables: true,
});
howToSaveCleanedStyles(cleanedStyles);
return content;
}
... Here I was lost due to lack of understanding of webpack hooks 😕 At the moment I'm using postcss-purgecss purgeCss.plugin.js: /**
* @typedef {import('@fullhuman/postcss-purgecss').UserDefinedOptions} PostCssPurgeCssOptions
* @typedef {PostCssPurgeCssOptions} Options
*/
const path = require('node:path');
const { globSync } = require('glob');
const paths = {
src: path.resolve(__dirname, '../../src'),
output: path.resolve(__dirname, '../../public'),
};
/**
* @type {{string: Options}} plugin
*/
const plugin = {
'@fullhuman/postcss-purgecss': {
contentFunction: (sourceInputFileName) => {
// ? Support for error pages
if (/error\.scss$/.test(sourceInputFileName)) {
return [`${paths.output}/error-404.html`];
}
return globSync(
`${paths.output}/*.html`,
{
nodir: true,
ignore: {
ignored: (globPath) => /error-[\w\d-]+\.html$/.test(globPath.name),
},
},
);
},
keyframes: true,
variables: true,
},
};
module.exports = plugin; postcss.config.js: const purgeCss = require('./purgeCss.plugin.js');
/** @type {import('postcss-load-config').Config} */
const config = {
map: false,
plugins: {
...purgeCss,
autoprefixer: {
cascade: false,
},
},
};
module.exports = config; This works as I planned. But it has an obvious drawback. I need to specify already pre-compiled html files, which causes errors when updating the template. And the code will be hard to maintain when the number of pages starts to increase 🥴 |
Beta Was this translation helpful? Give feedback.
-
ah, now I understand the whole problem. Hmm, this is realy a very complex use case. I don't have a ready answer. The variant with
I can imagine this as
I have to think about this variant... Therefore, now the possible solution is your variant with postcss plugin. It's not comfortable, but it works. P.S. do you know a solution using other plugins, e.g. |
Beta Was this translation helpful? Give feedback.
-
I think the complexity of implementation is much higher than using I have created the purgecss-postcss example where each page contains own style file and compiled CSS contains only used on the page classes. Idea:
the entry option with
the contentFunction: (sourceInputFileName) => {
const { name } = path.parse(sourceInputFileName);
// works after the second build, because at the first time the dist dir is empty
return [`${paths.output}/${name}.html`];
}, |
Beta Was this translation helpful? Give feedback.
-
@webdiscus thanks! I tried. The directory structure solves almost all style cleanup issues other than using uncompiled templates that include parts. You helped a lot. Today was a good day. For the most part, style cleanup issues have been resolved. Also, the search for a solution led me to the discovery of UnoCss. I even managed to fall a little in love with this library, which will replace, if not Bootstrap, then probably Tailwind in my projects. Can we close this discussion as resolved? |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
I looked a little into the hooks. Let me summarize. https://gist.github.com/vralle/d394e32a0f9d30798e60b3670c654fd1 |
Beta Was this translation helpful? Give feedback.
-
@webdiscus, I'm trying improve my code, but think I can't do without your help.
I also think that you will be a little angry after reading these questions) |
Beta Was this translation helpful? Give feedback.
I think the complexity of implementation is much higher than using
postcss-purgecss
with thecontentFunction()
therefore solve your special case on thepostcss-purgecss
side.I have created the purgecss-postcss example where each page contains own style file and compiled CSS contains only used on the page classes.
Idea:
name
name
(the key of theentry
option)name
, e.g. forabout.html
->about.scss
contentFunction
to retrieve thename
form style filenamecontentFunction
returns the HTML file based in thename
of style