Skip to content

Commit 4ee0928

Browse files
committed
feat(svg): adds sprite hash manifest for cache busting
Implements a webpack plugin that generates a JSON file containing content hashes for SVG sprite files. This allows for cache busting of sprite files by appending the hash as a query parameter to the sprite URL. The `Svg` service is updated to read the hash from the generated JSON and append it to the sprite URL.
1 parent ed363aa commit 4ee0928

File tree

3 files changed

+109
-5
lines changed

3 files changed

+109
-5
lines changed

config/plugins.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPl
1111

1212
const WebpackImageSizesPlugin = require('./webpack-image-sizes-plugin')
1313
const WebpackThemeJsonPlugin = require('./webpack-theme-json-plugin')
14+
const SpriteHashPlugin = require('./sprite-hash-plugin')
1415

1516
module.exports = {
1617
get: function (mode) {
1718
const plugins = [
1819
new WebpackThemeJsonPlugin({
1920
watch: mode !== 'production',
2021
}),
22+
new SpriteHashPlugin(),
2123
new CleanWebpackPlugin({
2224
cleanOnceBeforeBuildPatterns: ['**/*', '!images', '!images/**'],
2325
}),

config/sprite-hash-plugin.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const crypto = require('crypto');
4+
5+
/**
6+
* Webpack plugin to generate content hashes for SVG sprite files.
7+
* Creates a sprite-hashes.json file in the dist folder.
8+
*/
9+
class SpriteHashPlugin {
10+
constructor(options = {}) {
11+
this.options = {
12+
outputPath: options.outputPath || 'dist',
13+
spritePath: options.spritePath || 'dist/icons',
14+
outputFilename: options.outputFilename || 'sprite-hashes.json',
15+
hashLength: options.hashLength || 8,
16+
};
17+
}
18+
19+
apply(compiler) {
20+
compiler.hooks.afterEmit.tapAsync(
21+
'SpriteHashPlugin',
22+
(compilation, callback) => {
23+
const spriteDir = path.resolve(
24+
compiler.options.context,
25+
this.options.spritePath
26+
);
27+
const outputFile = path.resolve(
28+
compiler.options.context,
29+
this.options.outputPath,
30+
this.options.outputFilename
31+
);
32+
33+
if (!fs.existsSync(spriteDir)) {
34+
console.warn(
35+
`SpriteHashPlugin: Sprite directory not found: ${spriteDir}`
36+
);
37+
callback();
38+
return;
39+
}
40+
41+
const hashes = {};
42+
const files = fs
43+
.readdirSync(spriteDir)
44+
.filter((file) => file.endsWith('.svg'));
45+
46+
files.forEach((file) => {
47+
const filePath = path.join(spriteDir, file);
48+
const content = fs.readFileSync(filePath);
49+
const hash = crypto
50+
.createHash('md5')
51+
.update(content)
52+
.digest('hex')
53+
.substring(0, this.options.hashLength);
54+
55+
// Store with relative path as key
56+
const relativePath = `icons/${file}`;
57+
hashes[relativePath] = hash;
58+
});
59+
60+
fs.writeFileSync(outputFile, JSON.stringify(hashes, null, 2));
61+
console.log(
62+
`SpriteHashPlugin: Generated ${
63+
this.options.outputFilename
64+
} with ${Object.keys(hashes).length} sprites`
65+
);
66+
67+
callback();
68+
}
69+
);
70+
}
71+
}
72+
73+
module.exports = SpriteHashPlugin;

inc/Services/Svg.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ public function get_the_icon( string $icon_class, array $additionnal_classes = [
5555
$icon_class = substr( $icon_class, $slash_pos + 1 );
5656
}
5757

58-
$icon_slug = strpos( $icon_class, 'icon-' ) === 0 ? $icon_class : sprintf( 'icon-%s', $icon_class );
59-
$classes = [ 'icon', $icon_slug ];
60-
$classes = array_merge( $classes, $additionnal_classes );
61-
$classes = array_map( 'sanitize_html_class', $classes );
58+
$icon_slug = strpos( $icon_class, 'icon-' ) === 0 ? $icon_class : sprintf( 'icon-%s', $icon_class );
59+
$classes = [ 'icon', $icon_slug ];
60+
$classes = array_merge( $classes, $additionnal_classes );
61+
$classes = array_map( 'sanitize_html_class', $classes );
62+
$hash_sprite = $this->get_sprite_hash( $sprite_name );
6263

63-
return sprintf( '<svg class="%s" aria-hidden="true" focusable="false"><use href="%s#%s"></use></svg>', implode( ' ', $classes ), \get_theme_file_uri( sprintf( '/dist/icons/%s.svg', $sprite_name ) ), $icon_slug ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
64+
return sprintf( '<svg class="%s" aria-hidden="true" focusable="false"><use href="%s%s#%s"></use></svg>', implode( ' ', $classes ), \get_theme_file_uri( sprintf( '/dist/icons/%s.svg', $sprite_name ) ), $hash_sprite, $icon_slug ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
6465
}
6566

6667
/**
@@ -104,4 +105,32 @@ public function allow_svg_tag( $tags ) {
104105

105106
return $tags;
106107
}
108+
109+
/**
110+
* Get the hash of the sprite
111+
*
112+
* @param string $sprite_name
113+
*
114+
* @return string
115+
*/
116+
public function get_sprite_hash( $sprite_name ) {
117+
$sprite_hash_file = \get_theme_file_path( '/dist/sprite-hashes.json' );
118+
119+
if ( ! is_readable( $sprite_hash_file ) ) {
120+
return '';
121+
}
122+
123+
$sprite_hash = file_get_contents( $sprite_hash_file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
124+
$sprite_hash = json_decode( $sprite_hash, true );
125+
126+
if ( empty( $sprite_hash ) ) {
127+
return '';
128+
}
129+
130+
if ( ! isset( $sprite_hash[ sprintf( 'icons/%s.svg', $sprite_name ) ] ) ) {
131+
return '';
132+
}
133+
134+
return '?' . $sprite_hash[ sprintf( 'icons/%s.svg', $sprite_name ) ];
135+
}
107136
}

0 commit comments

Comments
 (0)