From b2474a595cbd982cc37b485cc2164e23c8c4f36c Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sun, 20 Jul 2025 14:13:39 +0200 Subject: [PATCH] feat: add InlineCssPlugin to inline CSS assets in production builds --- webpack.config.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/webpack.config.js b/webpack.config.js index 4c4c5ea..d8d6bef 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -102,6 +102,74 @@ const CopyAssetsPlugin = { }, }; +// Custom plugin to inline CSS instead of loading externally +class InlineCssPlugin { + apply(compiler) { + compiler.hooks.compilation.tap('InlineCssPlugin', (compilation) => { + HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync( + 'InlineCssPlugin', + (data, cb) => { + // Only inline CSS in production mode + if (isDevelopment) { + cb(null, data); + return; + } + + // Find CSS assets and inline them + let inlinedCss = ''; + const cssAssets = []; + + // Get CSS assets from compilation + Object.keys(compilation.assets).forEach(assetName => { + if (assetName.endsWith('.css')) { + const cssContent = compilation.assets[assetName].source(); + inlinedCss += cssContent + '\n'; + cssAssets.push(assetName); + + // Remove CSS asset from compilation to prevent external file generation + delete compilation.assets[assetName]; + } + }); + + if (inlinedCss.trim()) { + // Remove existing CSS link tags from HTML + data.html = data.html.replace(/]*href="[^"]*\.css"[^>]*>/g, ''); + + // Extract font URLs from CSS for preloading + const fontUrls = []; + const fontMatches = inlinedCss.match(/url\(([^)]+\.ttf)\)/g); + if (fontMatches) { + fontMatches.forEach(match => { + const fontUrl = match.replace(/url\(([^)]+)\)/, '$1').replace(/['"]/g, ''); + if (!fontUrls.includes(fontUrl)) { + fontUrls.push(fontUrl); + } + }); + } + + // Add font preload links + let fontPreloads = ''; + fontUrls.forEach(fontUrl => { + fontPreloads += `\n`; + }); + + // Add inlined CSS to head + const styleTag = ``; + data.html = data.html.replace('', `${fontPreloads}${styleTag}\n`); + + console.log(`✅ Inlined CSS assets: ${cssAssets.join(', ')} (${Math.round(inlinedCss.length / 1024)}KB)`); + if (fontUrls.length > 0) { + console.log(`✅ Added font preloads: ${fontUrls.length} fonts`); + } + } + + cb(null, data); + } + ); + }); + } +} + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const isDevelopment = process.env.NODE_ENV !== 'production'; @@ -263,6 +331,7 @@ export default { generateStatsFile: true, statsFilename: 'bundle-stats.json', }), + new InlineCssPlugin(), ].filter(Boolean), devServer: { allowedHosts: 'all',