From f57f75914007e1c3833726141b9b4908976bcf10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Andrietti?= Date: Fri, 24 Apr 2026 10:59:59 +0200 Subject: [PATCH 01/10] Add WebP support and update dependencies - Added imagemin-webp to package.json and updated yarn.lock. - Configured WebP generation in webpack.common.js. - Updated SCSS files to support WebP images. - Removed obsolete logo SVG file. --- config/webpack.common.js | 9 +++++ package.json | 1 + src/img/static/logo-beapi.svg | 1 - src/scss/02-tools/_m-background-static.scss | 6 +-- src/scss/login.scss | 3 +- yarn.lock | 45 ++++++++++++++++++--- 6 files changed, 54 insertions(+), 11 deletions(-) delete mode 100644 src/img/static/logo-beapi.svg diff --git a/config/webpack.common.js b/config/webpack.common.js index 1e12ba68..acc3f6ef 100644 --- a/config/webpack.common.js +++ b/config/webpack.common.js @@ -29,6 +29,15 @@ module.exports = { ], }, }, + generator: [ + { + preset: 'webp', + implementation: ImageMinimizerPlugin.imageminGenerate, + options: { + plugins: ['imagemin-webp'], + }, + }, + ], }), new TerserPlugin({ parallel: true, diff --git a/package.json b/package.json index bab325c0..94ca3486 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "imagemin-jpegtran": "^7.0.0", "imagemin-optipng": "^8.0.0", "imagemin-svgo": "^10.0.1", + "imagemin-webp": "^8.0.0", "mini-css-extract-plugin": "^1.5.0", "postcss": "^8.4.24", "postcss-import": "^15.1.0", diff --git a/src/img/static/logo-beapi.svg b/src/img/static/logo-beapi.svg deleted file mode 100644 index 5cf0fca4..00000000 --- a/src/img/static/logo-beapi.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/scss/02-tools/_m-background-static.scss b/src/scss/02-tools/_m-background-static.scss index 0da1447e..99be8bba 100644 --- a/src/scss/02-tools/_m-background-static.scss +++ b/src/scss/02-tools/_m-background-static.scss @@ -13,15 +13,15 @@ * */ -@mixin background-static($filename, $retina: true, $position: center center, $size: auto 100%, $type: "png" ) { - background-image: url(../img/static/#{$filename}.#{$type}); +@mixin background-static($filename, $retina: true, $position: center center, $size: auto 100%, $type: "png") { + background-image: url(../img/static/#{$filename}.#{$type}?as=webp); background-repeat: no-repeat; background-position: $position; background-size: $size; @if ($retina) { @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { - background-image: url(../img/static/#{$filename}@2x.#{$type}); + background-image: url(../img/static/#{$filename}@2x.#{$type}?as=webp); } } } diff --git a/src/scss/login.scss b/src/scss/login.scss index ad1fdd5f..6a69ad0e 100644 --- a/src/scss/login.scss +++ b/src/scss/login.scss @@ -1,7 +1,7 @@ // SCSS variables to customise the login page styles // The CSS file generated in dist/ by webpack is called automatically thanks to the WP Login Page mu-plugin present in the WP Skeleton. -$login-logo-path: "../../src/img/static/logo.jpg"; +$login-logo-path: "../../src/img/static/logo.jpg?as=webp"; $login-logo-size: 312px 43px; $login-logo-height: 60px; $login-body-bg-color: #666; @@ -16,6 +16,7 @@ $login-btn-hover-bg-color: #222; html, body { + &::before, &::after { display: none; diff --git a/yarn.lock b/yarn.lock index ff870edd..dd941a9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2280,6 +2280,7 @@ __metadata: imagemin-jpegtran: "npm:^7.0.0" imagemin-optipng: "npm:^8.0.0" imagemin-svgo: "npm:^10.0.1" + imagemin-webp: "npm:^8.0.0" lazysizes: "npm:^5.3.2" mini-css-extract-plugin: "npm:^1.5.0" oneloop.js: "npm:^5.2.1" @@ -2363,7 +2364,7 @@ __metadata: languageName: node linkType: hard -"bin-wrapper@npm:^4.0.0": +"bin-wrapper@npm:^4.0.0, bin-wrapper@npm:^4.0.1": version: 4.1.0 resolution: "bin-wrapper@npm:4.1.0" dependencies: @@ -2678,9 +2679,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001688, caniuse-lite@npm:^1.0.30001702": - version: 1.0.30001712 - resolution: "caniuse-lite@npm:1.0.30001712" - checksum: 10/1831ac3260b9657c5a0236d21c02bea6a6b88fd67a451a0ff166d27da17c95ab398c5721e08aeb24f766bced1d38f562f07c8de84e91a10a065808e83835e89e + version: 1.0.30001790 + resolution: "caniuse-lite@npm:1.0.30001790" + checksum: 10/2625ba0b9c2648d14b4b02daf2fe7013d4efe087a45b034f40849c97077d435dbc610b47a34d3d6360cd62b7972864ae16978955205b7b8f7397303ba793e0ed languageName: node linkType: hard @@ -3383,6 +3384,18 @@ __metadata: languageName: node linkType: hard +"cwebp-bin@npm:^8.0.0": + version: 8.0.0 + resolution: "cwebp-bin@npm:8.0.0" + dependencies: + bin-build: "npm:^3.0.0" + bin-wrapper: "npm:^4.0.1" + bin: + cwebp: cli.js + checksum: 10/cdf14da37bcc225a5ec577955ad32dfc97469f574af37520030d4da8858e74b9433e8dca5f892041f88f8e19554cdbdc94851afde1e0f22271308f0dd270e767 + languageName: node + linkType: hard + "data-view-buffer@npm:^1.0.2": version: 1.0.2 resolution: "data-view-buffer@npm:1.0.2" @@ -4461,7 +4474,7 @@ __metadata: languageName: node linkType: hard -"exec-buffer@npm:^3.0.0": +"exec-buffer@npm:^3.0.0, exec-buffer@npm:^3.2.0": version: 3.2.0 resolution: "exec-buffer@npm:3.2.0" dependencies: @@ -4728,7 +4741,7 @@ __metadata: languageName: node linkType: hard -"file-type@npm:^10.4.0": +"file-type@npm:^10.4.0, file-type@npm:^10.5.0": version: 10.11.0 resolution: "file-type@npm:10.11.0" checksum: 10/787ab64574316dbd423eccbadac2876879c5d2f1d24309948debdaf1dfbd0f5f25f881a716f44d294090bf435407f6938da41c833895c888a78127113337a608 @@ -5779,6 +5792,17 @@ __metadata: languageName: node linkType: hard +"imagemin-webp@npm:^8.0.0": + version: 8.0.0 + resolution: "imagemin-webp@npm:8.0.0" + dependencies: + cwebp-bin: "npm:^8.0.0" + exec-buffer: "npm:^3.2.0" + is-cwebp-readable: "npm:^3.0.0" + checksum: 10/707404df20f346b83b5651bc99017c2f272f400b87238ed64236a0dce112dd9c5d77d10ab365af6356e6ce33afd6a2d79fe534c82a8717ea746d9bc6f70b6804 + languageName: node + linkType: hard + "imagemin@npm:^8.0.1": version: 8.0.1 resolution: "imagemin@npm:8.0.1" @@ -6021,6 +6045,15 @@ __metadata: languageName: node linkType: hard +"is-cwebp-readable@npm:^3.0.0": + version: 3.0.0 + resolution: "is-cwebp-readable@npm:3.0.0" + dependencies: + file-type: "npm:^10.5.0" + checksum: 10/768ae017586ba2fb0831d3cc9cfb4cd56c9580b71684ea5584cf61910597c5fe91a419490ed85422424c6339fe9c327df3643c3496145134d4d0385fb479b591 + languageName: node + linkType: hard + "is-data-descriptor@npm:^1.0.1": version: 1.0.1 resolution: "is-data-descriptor@npm:1.0.1" From db240d6df2ac84cb78313859294859b382724385 Mon Sep 17 00:00:00 2001 From: Milan Ricoul Date: Fri, 24 Apr 2026 11:21:23 +0200 Subject: [PATCH 02/10] ci: use Corepack for Yarn 4.5.0 instead of latest Berry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace yarn set version berry with corepack enable to match packageManager and avoid lockfile v8→v9 migration blocked in PRs. Upgrade actions/setup-node to v4 with yarn cache. --- .github/workflows/node.js.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index d09be165..f49f41dc 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -21,9 +21,11 @@ jobs: steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - run: yarn set version berry + cache: yarn + - name: Enable Corepack + run: corepack enable - run: yarn - run: yarn build From 50732ce4872d744a78a909e84254617b1d076342 Mon Sep 17 00:00:00 2001 From: mricoul Date: Fri, 24 Apr 2026 15:38:46 +0200 Subject: [PATCH 03/10] ci(node.js): change order of tasks --- .github/workflows/node.js.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index f49f41dc..4783470a 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -20,12 +20,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Enable Corepack + # Must run before setup-node's yarn cache: resolves Yarn from packageManager, not v1. + # See: https://github.com/actions/setup-node/issues/1027 + run: corepack enable - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: yarn - - name: Enable Corepack - run: corepack enable - run: yarn - run: yarn build From 56120c3c93bce3b85f29907340cb0b17c19b057d Mon Sep 17 00:00:00 2001 From: Milan Ricoul Date: Fri, 24 Apr 2026 16:19:10 +0200 Subject: [PATCH 04/10] fix(webpack): register WebP imagemin in plugins for dev, avoid duplicate instance Move ImageMinimizerPlugin to plugins (generator + optional prod minifier) so ?as=webp works when minimization is off. Remove it from optimization.minimizer to keep a single instance and avoid asset name conflicts. Minor shorthand for mode in dev/prod config. --- config/plugins.js | 33 +++++++++++++++++++++++++++++++++ config/webpack.common.js | 27 --------------------------- config/webpack.dev.js | 2 +- config/webpack.prod.js | 2 +- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/config/plugins.js b/config/plugins.js index dd5858ee..17e125dc 100644 --- a/config/plugins.js +++ b/config/plugins.js @@ -1,4 +1,6 @@ const path = require('path') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') +const svgoconfig = require('./svgo.config') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const { WebpackManifestPlugin } = require('webpack-manifest-plugin') const ESLintPlugin = require('eslint-webpack-plugin') @@ -15,7 +17,38 @@ const SpriteHashPlugin = require('./webpack-sprite-hash-plugin') module.exports = { get: function (mode) { + const isProduction = mode === 'production' + // A single instance: `optimization.minimizer` is only `apply()`'d when `minimize: true` + // (see webpack `WebpackOptionsApply.js`), so WebP `?as=webp` must live on the main + // `plugins` list to work with `yarn start` / dev. Image minify runs on `processAssets` + // from this same plugin in production only (keeps dev watch fast). + const imageMinimizerOptions = { + loader: true, + generator: [ + { + preset: 'webp', + implementation: ImageMinimizerPlugin.imageminGenerate, + options: { + plugins: ['imagemin-webp'], + }, + }, + ], + } + if (isProduction) { + imageMinimizerOptions.minimizer = { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + ['gifsicle', { interlaced: true }], + ['jpegtran', { progressive: true }], + ['optipng', { optimizationLevel: 5 }], + ['svgo', { svgoconfig }], + ], + }, + } + } const plugins = [ + new ImageMinimizerPlugin(imageMinimizerOptions), new WebpackThemeJsonPlugin({ watch: mode !== 'production', }), diff --git a/config/webpack.common.js b/config/webpack.common.js index acc3f6ef..c3886ef4 100644 --- a/config/webpack.common.js +++ b/config/webpack.common.js @@ -1,8 +1,6 @@ const path = require('path') const entries = require('./entries') -const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') -const svgoconfig = require('./svgo.config') module.exports = { entry: entries, @@ -14,31 +12,6 @@ module.exports = { }, optimization: { minimizer: [ - new ImageMinimizerPlugin({ - minimizer: { - implementation: ImageMinimizerPlugin.imageminMinify, - options: { - // Lossless optimization with custom option - // Feel free to experiment with options for better result for you - plugins: [ - ['gifsicle', { interlaced: true }], - ['jpegtran', { progressive: true }], - ['optipng', { optimizationLevel: 5 }], - // Svgo configuration here https://github.com/svg/svgo#configuratio - ['svgo', { svgoconfig }], - ], - }, - }, - generator: [ - { - preset: 'webp', - implementation: ImageMinimizerPlugin.imageminGenerate, - options: { - plugins: ['imagemin-webp'], - }, - }, - ], - }), new TerserPlugin({ parallel: true, terserOptions: { diff --git a/config/webpack.dev.js b/config/webpack.dev.js index 834f8892..fef55411 100644 --- a/config/webpack.dev.js +++ b/config/webpack.dev.js @@ -6,7 +6,7 @@ const loaders = require('./loaders') const mode = 'development' module.exports = merge(common, { - mode: mode, + mode, stats: 'errors-only', devtool: 'inline-source-map', devServer: { diff --git a/config/webpack.prod.js b/config/webpack.prod.js index 4c0cd0fd..ec3c1ea3 100644 --- a/config/webpack.prod.js +++ b/config/webpack.prod.js @@ -5,7 +5,7 @@ const loaders = require('./loaders') const mode = 'production' module.exports = merge(common, { - mode: mode, + mode, stats: 'minimal', output: { filename: '[name]-min.js', From d8070be9e79c993fb918b68c7a6bfeca9ac0cb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Andrietti?= Date: Mon, 27 Apr 2026 10:32:22 +0200 Subject: [PATCH 05/10] remove: delete unused background-static SCSS mixin --- src/scss/02-tools/_m-background-static.scss | 27 --------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/scss/02-tools/_m-background-static.scss diff --git a/src/scss/02-tools/_m-background-static.scss b/src/scss/02-tools/_m-background-static.scss deleted file mode 100644 index 99be8bba..00000000 --- a/src/scss/02-tools/_m-background-static.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Background retina - Make an alignment (left or right) - * - * @author Cédric Andrietti - * - * @param $direction - * - * Examples : - * - * .test { - * @include background-static("your-image"); - * } - * - */ - -@mixin background-static($filename, $retina: true, $position: center center, $size: auto 100%, $type: "png") { - background-image: url(../img/static/#{$filename}.#{$type}?as=webp); - background-repeat: no-repeat; - background-position: $position; - background-size: $size; - - @if ($retina) { - @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { - background-image: url(../img/static/#{$filename}@2x.#{$type}?as=webp); - } - } -} From 803eb39c905dafd98f8f4da0c7fbfd3a645c2c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Andrietti?= Date: Mon, 27 Apr 2026 11:58:40 +0200 Subject: [PATCH 06/10] Add WebpackStaticImagesPlugin to handle static image processing - Introduced WebpackStaticImagesPlugin to copy and convert static images to WebP format. - Configured plugin in plugins.js with input and output directories, quality settings, and console output options. --- config/plugins.js | 7 ++ config/webpack-static-images-plugin.js | 125 +++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 config/webpack-static-images-plugin.js diff --git a/config/plugins.js b/config/plugins.js index 17e125dc..fc8647a5 100644 --- a/config/plugins.js +++ b/config/plugins.js @@ -14,6 +14,7 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl const WebpackImageSizesPlugin = require('./webpack-image-sizes-plugin') const WebpackThemeJsonPlugin = require('./webpack-theme-json-plugin') const SpriteHashPlugin = require('./webpack-sprite-hash-plugin') +const WebpackStaticImagesPlugin = require('./webpack-static-images-plugin') module.exports = { get: function (mode) { @@ -85,6 +86,12 @@ module.exports = { defaultImageFormat: 'jpg', // Generated image format (jpg, png, webp, avif) silence: true, // Suppress console output }), + new WebpackStaticImagesPlugin({ + inputDir: 'src/img/static', + outputDir: 'dist/images', + quality: 80, + silence: false, // Suppress console output + }), ] if (mode === 'production') { diff --git a/config/webpack-static-images-plugin.js b/config/webpack-static-images-plugin.js new file mode 100644 index 00000000..f955013e --- /dev/null +++ b/config/webpack-static-images-plugin.js @@ -0,0 +1,125 @@ +const fs = require('fs') +const path = require('path') + +// Try to require sharp, fallback gracefully if not available +let sharp +try { + sharp = require('sharp') +} catch (error) { + console.warn('⚠️ Sharp not available. WebP conversion will be disabled.') +} + +/** + * Webpack plugin to copy and automatically convert static images in a folder to WebP. + */ +class WebpackStaticImagesPlugin { + /** + * Creates an instance of WebpackStaticImagesPlugin. + * + * @param {Object} [options={}] - Configuration options + * @param {string} [options.inputDir='src/img/static'] - Input directory + * @param {string} [options.outputDir='dist/images'] - Output directory + * @param {number} [options.quality=80] - WebP compression quality + * @param {boolean} [options.silence=false] - Disable console output + */ + constructor(options = {}) { + this.options = { + inputDir: 'src/img/static', + outputDir: 'dist/images', + quality: 80, + silence: false, + ...options, + } + } + + /** + * Logs a message to the console if silence option is not enabled. + */ + log(level, ...args) { + if (!this.options.silence) { + console[level](...args) + } + } + + /** + * Entry point for Webpack. + */ + apply(compiler) { + if (!sharp) { + return + } // If Sharp is not installed, silently cancel. + + // Use afterEmit to ensure that Webpack has created the dist folder. + compiler.hooks.afterEmit.tapPromise('WebpackStaticImagesPlugin', async (compilation) => { + const { context } = compiler + const inputPath = path.resolve(context, this.options.inputDir) + const outputPath = path.resolve(context, this.options.outputDir) + + // Check if the source directory exists. + if (!fs.existsSync(inputPath)) { + this.log('warn', `⚠️ Source directory not found: ${inputPath}`) + return + } + + // Create the output directory if it doesn't exist. + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath, { recursive: true }) + } + + try { + this.log('log', '🔄 Starting static images processing...') + await this.processImages(inputPath, outputPath) + this.log('log', '🎉 Static images processing completed!') + } catch (error) { + this.log('error', '❌ Error during static images processing:', error) + } + }) + } + + /** + * Processes the images in the directory. + */ + async processImages(inputPath, outputPath) { + const files = fs.readdirSync(inputPath) + const promises = [] + let count = 0 + + for (const file of files) { + // Only process JPG and PNG files. + if (file.match(/\.(png|jpe?g)$/i)) { + const filePath = path.join(inputPath, file) + const fileName = path.parse(file).name + + // Create the output paths. + const outputOriginal = path.join(outputPath, file) + const outputWebp = path.join(outputPath, `${fileName}.webp`) + + // Prepare the Sharp instances. + // (Sharp returns Promises, it is crucial to wait for them.) + const copyPromise = sharp(filePath).toFile(outputOriginal) + const webpPromise = sharp(filePath).webp({ quality: this.options.quality }).toFile(outputWebp) + + // Group the two actions for this file. + const filePromise = Promise.all([copyPromise, webpPromise]) + .then(() => { + this.log('log', ` ✅ Converted: ${file} (+ .webp version)`) + }) + .catch((err) => { + this.log('error', ` ❌ Error on ${file}:`, err.message) + }) + + promises.push(filePromise) + count++ + } + } + + // Wait for all images to be generated before returning to Webpack. + if (count > 0) { + await Promise.all(promises) + } else { + this.log('log', ' ℹ️ No images to process in the directory.') + } + } +} + +module.exports = WebpackStaticImagesPlugin From 17492e8701d716243101398ea6f716a420023afd Mon Sep 17 00:00:00 2001 From: mricoul Date: Mon, 27 Apr 2026 14:14:47 +0200 Subject: [PATCH 07/10] feat(webpack): optimize static image processing in watch mode Improve build performance by skipping image processing when no files in the input directory have changed. The plugin now registers the input directory as a context dependency and checks modified files during subsequent builds to avoid redundant processing. --- config/webpack-static-images-plugin.js | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/config/webpack-static-images-plugin.js b/config/webpack-static-images-plugin.js index f955013e..b6795075 100644 --- a/config/webpack-static-images-plugin.js +++ b/config/webpack-static-images-plugin.js @@ -30,6 +30,7 @@ class WebpackStaticImagesPlugin { silence: false, ...options, } + this.hasBeenBuiltOnce = false } /** @@ -49,6 +50,13 @@ class WebpackStaticImagesPlugin { return } // If Sharp is not installed, silently cancel. + compiler.hooks.compilation.tap('WebpackStaticImagesPlugin', (compilation) => { + const inputPath = path.resolve(compiler.context, this.options.inputDir) + if (fs.existsSync(inputPath)) { + compilation.contextDependencies.add(inputPath) + } + }) + // Use afterEmit to ensure that Webpack has created the dist folder. compiler.hooks.afterEmit.tapPromise('WebpackStaticImagesPlugin', async (compilation) => { const { context } = compiler @@ -61,6 +69,24 @@ class WebpackStaticImagesPlugin { return } + // Skip re-processing in watch when nothing under inputDir changed (see WebpackImageSizesPlugin). + let hasChanges = false + if (this.hasBeenBuiltOnce && compilation.modifiedFiles) { + for (const filePath of compilation.modifiedFiles) { + if (this.isFileUnderDir(filePath, inputPath)) { + hasChanges = true + break + } + } + } + + if (this.hasBeenBuiltOnce && !hasChanges) { + this.log('log', `✅ No changes detected in ${this.options.inputDir}`) + return + } + + this.hasBeenBuiltOnce = true + // Create the output directory if it doesn't exist. if (!fs.existsSync(outputPath)) { fs.mkdirSync(outputPath, { recursive: true }) @@ -76,6 +102,19 @@ class WebpackStaticImagesPlugin { }) } + /** + * Returns true if `filePath` is `inputDir` or a file inside it (cross-platform). + */ + isFileUnderDir(filePath, inputDirResolved) { + const resolvedFile = path.resolve(filePath) + const resolvedDir = path.resolve(inputDirResolved) + if (resolvedFile === resolvedDir) { + return true + } + const relative = path.relative(resolvedDir, resolvedFile) + return !relative.startsWith('..') && !path.isAbsolute(relative) + } + /** * Processes the images in the directory. */ From 8ad12c8babc5e8bd97c08b46bfeeceed07697242 Mon Sep 17 00:00:00 2001 From: mricoul Date: Mon, 27 Apr 2026 14:15:03 +0200 Subject: [PATCH 08/10] feat(webpack): use byte-for-byte copy for original images in static plugin Switch from Sharp to fs.copyFile for original assets to ensure they are preserved exactly as-is, maintaining source quality and metadata (EXIF/ICC). Sharp is now used exclusively for generating the WebP derivatives. --- config/webpack-static-images-plugin.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/config/webpack-static-images-plugin.js b/config/webpack-static-images-plugin.js index b6795075..e38dfe2f 100644 --- a/config/webpack-static-images-plugin.js +++ b/config/webpack-static-images-plugin.js @@ -10,7 +10,7 @@ try { } /** - * Webpack plugin to copy and automatically convert static images in a folder to WebP. + * Webpack plugin to copy static images byte-for-byte and generate WebP derivatives with Sharp. */ class WebpackStaticImagesPlugin { /** @@ -19,7 +19,7 @@ class WebpackStaticImagesPlugin { * @param {Object} [options={}] - Configuration options * @param {string} [options.inputDir='src/img/static'] - Input directory * @param {string} [options.outputDir='dist/images'] - Output directory - * @param {number} [options.quality=80] - WebP compression quality + * @param {number} [options.quality=80] - WebP output quality (originals are not re-encoded) * @param {boolean} [options.silence=false] - Disable console output */ constructor(options = {}) { @@ -133,15 +133,13 @@ class WebpackStaticImagesPlugin { const outputOriginal = path.join(outputPath, file) const outputWebp = path.join(outputPath, `${fileName}.webp`) - // Prepare the Sharp instances. - // (Sharp returns Promises, it is crucial to wait for them.) - const copyPromise = sharp(filePath).toFile(outputOriginal) + // Byte copy preserves source quality, EXIF/ICC, etc. WebP is generated separately. + const copyPromise = fs.promises.copyFile(filePath, outputOriginal) const webpPromise = sharp(filePath).webp({ quality: this.options.quality }).toFile(outputWebp) - // Group the two actions for this file. const filePromise = Promise.all([copyPromise, webpPromise]) .then(() => { - this.log('log', ` ✅ Converted: ${file} (+ .webp version)`) + this.log('log', ` ✅ ${file} (original copied, .webp generated)`) }) .catch((err) => { this.log('error', ` ❌ Error on ${file}:`, err.message) From 0a61e3bac3952ffeb2c9ac09134b1b91a572f575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Andrietti?= Date: Tue, 28 Apr 2026 16:14:29 +0200 Subject: [PATCH 09/10] refactor: remove JPEG images and update to WebP format - Deleted unused JPEG images (default.jpg, logo.jpg) and replaced them with WebP versions (default.webp, logo.webp). - Updated SCSS and Webpack configuration to reference the new WebP images. - Removed imagemin-webp from package.json and yarn.lock as it is no longer needed. - Generate default images in webp format by default --- config/plugins.js | 50 +++++++++------------------ config/webpack-image-sizes-plugin.js | 7 ++-- package.json | 1 - src/img/static/default.jpg | Bin 6737 -> 0 bytes src/img/static/default.webp | Bin 0 -> 3930 bytes src/img/static/logo.jpg | Bin 6737 -> 0 bytes src/img/static/logo.webp | Bin 0 -> 3930 bytes src/scss/login.scss | 2 +- yarn.lock | 39 ++------------------- 9 files changed, 24 insertions(+), 75 deletions(-) delete mode 100644 src/img/static/default.jpg create mode 100644 src/img/static/default.webp delete mode 100644 src/img/static/logo.jpg create mode 100644 src/img/static/logo.webp diff --git a/config/plugins.js b/config/plugins.js index fc8647a5..491be8ec 100644 --- a/config/plugins.js +++ b/config/plugins.js @@ -18,38 +18,7 @@ const WebpackStaticImagesPlugin = require('./webpack-static-images-plugin') module.exports = { get: function (mode) { - const isProduction = mode === 'production' - // A single instance: `optimization.minimizer` is only `apply()`'d when `minimize: true` - // (see webpack `WebpackOptionsApply.js`), so WebP `?as=webp` must live on the main - // `plugins` list to work with `yarn start` / dev. Image minify runs on `processAssets` - // from this same plugin in production only (keeps dev watch fast). - const imageMinimizerOptions = { - loader: true, - generator: [ - { - preset: 'webp', - implementation: ImageMinimizerPlugin.imageminGenerate, - options: { - plugins: ['imagemin-webp'], - }, - }, - ], - } - if (isProduction) { - imageMinimizerOptions.minimizer = { - implementation: ImageMinimizerPlugin.imageminMinify, - options: { - plugins: [ - ['gifsicle', { interlaced: true }], - ['jpegtran', { progressive: true }], - ['optipng', { optimizationLevel: 5 }], - ['svgo', { svgoconfig }], - ], - }, - } - } const plugins = [ - new ImageMinimizerPlugin(imageMinimizerOptions), new WebpackThemeJsonPlugin({ watch: mode !== 'production', }), @@ -81,9 +50,9 @@ module.exports = { outputImageLocations: 'image-locations.json', // Output locations file name outputImageSizes: 'image-sizes.json', // Output sizes file name generateDefaultImages: true, // Generate default images - defaultImageSource: 'src/img/static/default.jpg', // Source image for generation + defaultImageSource: 'src/img/static/default.webp', // Source image for generation defaultImagesOutputDir: 'dist/images', // Default images output directory - defaultImageFormat: 'jpg', // Generated image format (jpg, png, webp, avif) + defaultImageFormat: 'webp', // Generated image format (jpg, png, webp, avif) silence: true, // Suppress console output }), new WebpackStaticImagesPlugin({ @@ -95,6 +64,21 @@ module.exports = { ] if (mode === 'production') { + plugins.push( + new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + ['gifsicle', { interlaced: true }], + ['jpegtran', { progressive: true }], + ['optipng', { optimizationLevel: 5 }], + ['svgo', { svgoconfig }], + ], + }, + }, + }) + ) plugins.push( new BundleAnalyzerPlugin({ analyzerMode: 'json', diff --git a/config/webpack-image-sizes-plugin.js b/config/webpack-image-sizes-plugin.js index d7a9618f..1b30a9af 100644 --- a/config/webpack-image-sizes-plugin.js +++ b/config/webpack-image-sizes-plugin.js @@ -41,9 +41,9 @@ class WebpackImageSizesPlugin { outputImageLocations: 'image-locations.json', outputImageSizes: 'image-sizes.json', generateDefaultImages: false, - defaultImageSource: 'src/img/static/default.jpg', + defaultImageSource: 'src/img/static/default.webp', defaultImagesOutputDir: 'dist/images', - defaultImageFormat: 'jpg', + defaultImageFormat: 'webp', silence: false, ...options, } @@ -543,8 +543,7 @@ class WebpackImageSizesPlugin { this.log( 'log', - `🖼️ Processing ${sizeKeys.length} default images (${format.toUpperCase()}) from ${ - this.options.defaultImageSource + `🖼️ Processing ${sizeKeys.length} default images (${format.toUpperCase()}) from ${this.options.defaultImageSource }` ) diff --git a/package.json b/package.json index 94ca3486..bab325c0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "imagemin-jpegtran": "^7.0.0", "imagemin-optipng": "^8.0.0", "imagemin-svgo": "^10.0.1", - "imagemin-webp": "^8.0.0", "mini-css-extract-plugin": "^1.5.0", "postcss": "^8.4.24", "postcss-import": "^15.1.0", diff --git a/src/img/static/default.jpg b/src/img/static/default.jpg deleted file mode 100644 index 82629726f9c830c6d8da5719583fa5757202211e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6737 zcmeI%L2JS=6bJAZBTi^X6PFr@BZU?1L9hks*`5UbT>BvV7CQ=KhaGp^^x`+k(&{0K zc-!s&atV*uCj1VMkhmXrB(Ys@*2EYQ^Ml-hUP(kz6h|VCg-#w5JyAlaNv7M#tTsNh zwfVG|=C&-C%d%`PgUKY3Pcj{iQYHN`YCKkEX3VoOvnsbf@@l>)iefRZ>#C~jrdfr* za33Ux{r%vJvlr?~#wBx~ly=*sFa~Zs5V#O%+ej*|k0Wo#^ScRK e(azDsu5C85dqxSq{yv>qm@n!d?o{w|#qS@41V0M^ diff --git a/src/img/static/default.webp b/src/img/static/default.webp new file mode 100644 index 0000000000000000000000000000000000000000..02b7b6f68cab62e9b4c625cddcd59c5e30e3008b GIT binary patch literal 3930 zcmWIYbaM;hXJ80-bqWXzuuyOVvNxPzoXed$ze1bjFt|g prQ&F9Fj`5DHV#G`MWb!S(KhmE-(a-Mgw|E8VEFg{Q47QH0{|3mSa1LU literal 0 HcmV?d00001 diff --git a/src/img/static/logo.jpg b/src/img/static/logo.jpg deleted file mode 100644 index 82629726f9c830c6d8da5719583fa5757202211e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6737 zcmeI%L2JS=6bJAZBTi^X6PFr@BZU?1L9hks*`5UbT>BvV7CQ=KhaGp^^x`+k(&{0K zc-!s&atV*uCj1VMkhmXrB(Ys@*2EYQ^Ml-hUP(kz6h|VCg-#w5JyAlaNv7M#tTsNh zwfVG|=C&-C%d%`PgUKY3Pcj{iQYHN`YCKkEX3VoOvnsbf@@l>)iefRZ>#C~jrdfr* za33Ux{r%vJvlr?~#wBx~ly=*sFa~Zs5V#O%+ej*|k0Wo#^ScRK e(azDsu5C85dqxSq{yv>qm@n!d?o{w|#qS@41V0M^ diff --git a/src/img/static/logo.webp b/src/img/static/logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..02b7b6f68cab62e9b4c625cddcd59c5e30e3008b GIT binary patch literal 3930 zcmWIYbaM;hXJ80-bqWXzuuyOVvNxPzoXed$ze1bjFt|g prQ&F9Fj`5DHV#G`MWb!S(KhmE-(a-Mgw|E8VEFg{Q47QH0{|3mSa1LU literal 0 HcmV?d00001 diff --git a/src/scss/login.scss b/src/scss/login.scss index 6a69ad0e..7f2726aa 100644 --- a/src/scss/login.scss +++ b/src/scss/login.scss @@ -1,7 +1,7 @@ // SCSS variables to customise the login page styles // The CSS file generated in dist/ by webpack is called automatically thanks to the WP Login Page mu-plugin present in the WP Skeleton. -$login-logo-path: "../../src/img/static/logo.jpg?as=webp"; +$login-logo-path: "../../src/img/static/logo.webp"; $login-logo-size: 312px 43px; $login-logo-height: 60px; $login-body-bg-color: #666; diff --git a/yarn.lock b/yarn.lock index dd941a9a..8b063e38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2280,7 +2280,6 @@ __metadata: imagemin-jpegtran: "npm:^7.0.0" imagemin-optipng: "npm:^8.0.0" imagemin-svgo: "npm:^10.0.1" - imagemin-webp: "npm:^8.0.0" lazysizes: "npm:^5.3.2" mini-css-extract-plugin: "npm:^1.5.0" oneloop.js: "npm:^5.2.1" @@ -2364,7 +2363,7 @@ __metadata: languageName: node linkType: hard -"bin-wrapper@npm:^4.0.0, bin-wrapper@npm:^4.0.1": +"bin-wrapper@npm:^4.0.0": version: 4.1.0 resolution: "bin-wrapper@npm:4.1.0" dependencies: @@ -3384,18 +3383,6 @@ __metadata: languageName: node linkType: hard -"cwebp-bin@npm:^8.0.0": - version: 8.0.0 - resolution: "cwebp-bin@npm:8.0.0" - dependencies: - bin-build: "npm:^3.0.0" - bin-wrapper: "npm:^4.0.1" - bin: - cwebp: cli.js - checksum: 10/cdf14da37bcc225a5ec577955ad32dfc97469f574af37520030d4da8858e74b9433e8dca5f892041f88f8e19554cdbdc94851afde1e0f22271308f0dd270e767 - languageName: node - linkType: hard - "data-view-buffer@npm:^1.0.2": version: 1.0.2 resolution: "data-view-buffer@npm:1.0.2" @@ -4474,7 +4461,7 @@ __metadata: languageName: node linkType: hard -"exec-buffer@npm:^3.0.0, exec-buffer@npm:^3.2.0": +"exec-buffer@npm:^3.0.0": version: 3.2.0 resolution: "exec-buffer@npm:3.2.0" dependencies: @@ -4741,7 +4728,7 @@ __metadata: languageName: node linkType: hard -"file-type@npm:^10.4.0, file-type@npm:^10.5.0": +"file-type@npm:^10.4.0": version: 10.11.0 resolution: "file-type@npm:10.11.0" checksum: 10/787ab64574316dbd423eccbadac2876879c5d2f1d24309948debdaf1dfbd0f5f25f881a716f44d294090bf435407f6938da41c833895c888a78127113337a608 @@ -5792,17 +5779,6 @@ __metadata: languageName: node linkType: hard -"imagemin-webp@npm:^8.0.0": - version: 8.0.0 - resolution: "imagemin-webp@npm:8.0.0" - dependencies: - cwebp-bin: "npm:^8.0.0" - exec-buffer: "npm:^3.2.0" - is-cwebp-readable: "npm:^3.0.0" - checksum: 10/707404df20f346b83b5651bc99017c2f272f400b87238ed64236a0dce112dd9c5d77d10ab365af6356e6ce33afd6a2d79fe534c82a8717ea746d9bc6f70b6804 - languageName: node - linkType: hard - "imagemin@npm:^8.0.1": version: 8.0.1 resolution: "imagemin@npm:8.0.1" @@ -6045,15 +6021,6 @@ __metadata: languageName: node linkType: hard -"is-cwebp-readable@npm:^3.0.0": - version: 3.0.0 - resolution: "is-cwebp-readable@npm:3.0.0" - dependencies: - file-type: "npm:^10.5.0" - checksum: 10/768ae017586ba2fb0831d3cc9cfb4cd56c9580b71684ea5584cf61910597c5fe91a419490ed85422424c6339fe9c327df3643c3496145134d4d0385fb479b591 - languageName: node - linkType: hard - "is-data-descriptor@npm:^1.0.1": version: 1.0.1 resolution: "is-data-descriptor@npm:1.0.1" From fc805aeb61303eaf8486587a2d30ac291d179b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Andrietti?= Date: Tue, 28 Apr 2026 16:16:50 +0200 Subject: [PATCH 10/10] refactor: remove WebpackStaticImagesPlugin and its configuration - Deleted WebpackStaticImagesPlugin to streamline image processing. - Removed related configuration from plugins.js, eliminating the static image processing setup. - This change simplifies the build process by relying on existing image handling methods. --- config/plugins.js | 7 -- config/webpack-static-images-plugin.js | 162 ------------------------- 2 files changed, 169 deletions(-) delete mode 100644 config/webpack-static-images-plugin.js diff --git a/config/plugins.js b/config/plugins.js index 491be8ec..7af5f90e 100644 --- a/config/plugins.js +++ b/config/plugins.js @@ -14,7 +14,6 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl const WebpackImageSizesPlugin = require('./webpack-image-sizes-plugin') const WebpackThemeJsonPlugin = require('./webpack-theme-json-plugin') const SpriteHashPlugin = require('./webpack-sprite-hash-plugin') -const WebpackStaticImagesPlugin = require('./webpack-static-images-plugin') module.exports = { get: function (mode) { @@ -55,12 +54,6 @@ module.exports = { defaultImageFormat: 'webp', // Generated image format (jpg, png, webp, avif) silence: true, // Suppress console output }), - new WebpackStaticImagesPlugin({ - inputDir: 'src/img/static', - outputDir: 'dist/images', - quality: 80, - silence: false, // Suppress console output - }), ] if (mode === 'production') { diff --git a/config/webpack-static-images-plugin.js b/config/webpack-static-images-plugin.js deleted file mode 100644 index e38dfe2f..00000000 --- a/config/webpack-static-images-plugin.js +++ /dev/null @@ -1,162 +0,0 @@ -const fs = require('fs') -const path = require('path') - -// Try to require sharp, fallback gracefully if not available -let sharp -try { - sharp = require('sharp') -} catch (error) { - console.warn('⚠️ Sharp not available. WebP conversion will be disabled.') -} - -/** - * Webpack plugin to copy static images byte-for-byte and generate WebP derivatives with Sharp. - */ -class WebpackStaticImagesPlugin { - /** - * Creates an instance of WebpackStaticImagesPlugin. - * - * @param {Object} [options={}] - Configuration options - * @param {string} [options.inputDir='src/img/static'] - Input directory - * @param {string} [options.outputDir='dist/images'] - Output directory - * @param {number} [options.quality=80] - WebP output quality (originals are not re-encoded) - * @param {boolean} [options.silence=false] - Disable console output - */ - constructor(options = {}) { - this.options = { - inputDir: 'src/img/static', - outputDir: 'dist/images', - quality: 80, - silence: false, - ...options, - } - this.hasBeenBuiltOnce = false - } - - /** - * Logs a message to the console if silence option is not enabled. - */ - log(level, ...args) { - if (!this.options.silence) { - console[level](...args) - } - } - - /** - * Entry point for Webpack. - */ - apply(compiler) { - if (!sharp) { - return - } // If Sharp is not installed, silently cancel. - - compiler.hooks.compilation.tap('WebpackStaticImagesPlugin', (compilation) => { - const inputPath = path.resolve(compiler.context, this.options.inputDir) - if (fs.existsSync(inputPath)) { - compilation.contextDependencies.add(inputPath) - } - }) - - // Use afterEmit to ensure that Webpack has created the dist folder. - compiler.hooks.afterEmit.tapPromise('WebpackStaticImagesPlugin', async (compilation) => { - const { context } = compiler - const inputPath = path.resolve(context, this.options.inputDir) - const outputPath = path.resolve(context, this.options.outputDir) - - // Check if the source directory exists. - if (!fs.existsSync(inputPath)) { - this.log('warn', `⚠️ Source directory not found: ${inputPath}`) - return - } - - // Skip re-processing in watch when nothing under inputDir changed (see WebpackImageSizesPlugin). - let hasChanges = false - if (this.hasBeenBuiltOnce && compilation.modifiedFiles) { - for (const filePath of compilation.modifiedFiles) { - if (this.isFileUnderDir(filePath, inputPath)) { - hasChanges = true - break - } - } - } - - if (this.hasBeenBuiltOnce && !hasChanges) { - this.log('log', `✅ No changes detected in ${this.options.inputDir}`) - return - } - - this.hasBeenBuiltOnce = true - - // Create the output directory if it doesn't exist. - if (!fs.existsSync(outputPath)) { - fs.mkdirSync(outputPath, { recursive: true }) - } - - try { - this.log('log', '🔄 Starting static images processing...') - await this.processImages(inputPath, outputPath) - this.log('log', '🎉 Static images processing completed!') - } catch (error) { - this.log('error', '❌ Error during static images processing:', error) - } - }) - } - - /** - * Returns true if `filePath` is `inputDir` or a file inside it (cross-platform). - */ - isFileUnderDir(filePath, inputDirResolved) { - const resolvedFile = path.resolve(filePath) - const resolvedDir = path.resolve(inputDirResolved) - if (resolvedFile === resolvedDir) { - return true - } - const relative = path.relative(resolvedDir, resolvedFile) - return !relative.startsWith('..') && !path.isAbsolute(relative) - } - - /** - * Processes the images in the directory. - */ - async processImages(inputPath, outputPath) { - const files = fs.readdirSync(inputPath) - const promises = [] - let count = 0 - - for (const file of files) { - // Only process JPG and PNG files. - if (file.match(/\.(png|jpe?g)$/i)) { - const filePath = path.join(inputPath, file) - const fileName = path.parse(file).name - - // Create the output paths. - const outputOriginal = path.join(outputPath, file) - const outputWebp = path.join(outputPath, `${fileName}.webp`) - - // Byte copy preserves source quality, EXIF/ICC, etc. WebP is generated separately. - const copyPromise = fs.promises.copyFile(filePath, outputOriginal) - const webpPromise = sharp(filePath).webp({ quality: this.options.quality }).toFile(outputWebp) - - const filePromise = Promise.all([copyPromise, webpPromise]) - .then(() => { - this.log('log', ` ✅ ${file} (original copied, .webp generated)`) - }) - .catch((err) => { - this.log('error', ` ❌ Error on ${file}:`, err.message) - }) - - promises.push(filePromise) - count++ - } - } - - // Wait for all images to be generated before returning to Webpack. - if (count > 0) { - await Promise.all(promises) - } else { - this.log('log', ' ℹ️ No images to process in the directory.') - } - } -} - -module.exports = WebpackStaticImagesPlugin