Skip to content

Instantly share code, notes, and snippets.

@Nooshu
Last active November 2, 2025 16:17
Show Gist options
  • Save Nooshu/edfc2e382bc249e92ab238779357c93e to your computer and use it in GitHub Desktop.
Save Nooshu/edfc2e382bc249e92ab238779357c93e to your computer and use it in GitHub Desktop.
ESM manipulation file I use on my 11ty v3.0.0 blog for manipulating my CSS.
// CSS Manipulation Module for Eleventy
// Processes, minifies, and compresses CSS with cache busting and Brotli compression
import crypto from "crypto";
import fs from "fs";
import path from "path";
import { brotliCompressSync } from "zlib";
import CleanCSS from "clean-css";
import env from "../_data/env.js";
// CleanCSS configuration for minification
const cleanCSS = new CleanCSS({
level: {
1: {
cleanupCharsets: true,
normalizeUrls: true,
optimizeBackground: true,
optimizeBorderRadius: true,
optimizeFilter: true,
optimizeFont: true,
optimizeFontWeight: true,
optimizeOutline: true,
removeEmpty: true,
removeNegativePaddings: true,
removeQuotes: true,
removeWhitespace: true,
replaceMultipleZeros: true,
replaceTimeUnits: true,
replaceZeroUnits: true,
roundingPrecision: 2,
selectorsSortingMethod: "standard",
specialComments: "none",
tidyAtRules: true,
tidyBlockScopes: true,
tidySelectors: true,
transform: function () {},
},
2: {
mergeAdjacentRules: true,
mergeIntoShorthands: true,
mergeMedia: true,
mergeNonAdjacentRules: true,
mergeSemantically: false,
overrideProperties: true,
removeEmpty: true,
reducePadding: true,
reducePositions: true,
reduceTimingFunctions: true,
reduceTransforms: true,
restructureRules: false,
skipProperties: [],
},
},
format: {
breaks: {
afterAtRule: false,
afterBlockBegins: false,
afterBlockEnds: false,
afterComment: false,
afterProperty: false,
afterRuleBegins: false,
afterRuleEnds: false,
beforeBlockEnds: false,
betweenSelectors: false,
},
indentBy: 0,
indentWith: "space",
spaces: {
aroundSelectorRelation: false,
beforeBlockBegins: false,
beforeValue: false,
},
wrapAt: false,
},
inline: ["none"],
rebase: false,
returnPromise: false,
});
const DEFAULT_BROTLI_COMPRESSION_LEVEL = 11;
const cssBuildCache = new Map();
const directoriesCreated = new Set();
export function manipulateCSS(eleventyConfig) {
eleventyConfig.addShortcode("customCSS", async function (cssPath) {
// Skip processing in local development
if (env.isLocal) {
return `<link rel="stylesheet" href="${cssPath}">`;
}
// Check in-memory cache
if (cssBuildCache.has(cssPath)) {
const cached = cssBuildCache.get(cssPath);
if (cached instanceof Promise) {
return await cached;
}
return cached.htmlOutput;
}
const fileStartTime = Date.now();
const inputFile = path.join("./public", cssPath);
const outputDirectory = path.join("./_site", "css");
const cacheDirectory = path.join("./.cache", "css");
const brotliCompressionLevel = parseInt(
process.env.BROTLI_COMPRESSION_LEVEL || DEFAULT_BROTLI_COMPRESSION_LEVEL,
10,
);
const processingPromise = (async () => {
try {
if (!fs.existsSync(inputFile)) {
console.error(`Input CSS file not found: ${inputFile}`);
return "";
}
const dirKey = `${cacheDirectory}:${outputDirectory}`;
if (!directoriesCreated.has(dirKey)) {
for (const dir of [cacheDirectory, outputDirectory]) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
directoriesCreated.add(dirKey);
}
let stepStartTime = Date.now();
const inputCSS = await fs.promises.readFile(inputFile, "utf8");
const inputSize = inputCSS.length;
const hash = crypto
.createHash("sha256")
.update(inputCSS)
.digest("hex")
.slice(0, 10);
console.log(
`⏱️ [${Date.now() - stepStartTime}ms] CSS Read & Hash: ${cssPath} (${(inputSize / 1024).toFixed(2)} KB)`,
);
const cacheKey = `${hash}-${cssPath.replace(/[/\\]/g, "-")}`;
const cachePath = path.join(cacheDirectory, cacheKey);
let processedCSS;
stepStartTime = Date.now();
if (fs.existsSync(cachePath)) {
processedCSS = await fs.promises.readFile(cachePath, "utf8");
console.log(
`⏱️ [${Date.now() - stepStartTime}ms] CSS Cache Hit: ${cssPath}`,
);
} else {
processedCSS = cleanCSS.minify(inputCSS).styles;
await fs.promises.writeFile(cachePath, processedCSS);
const processedSize = processedCSS.length;
console.log(
`⏱️ [${Date.now() - stepStartTime}ms] CSS Minify: ${cssPath} (${(inputSize / 1024).toFixed(2)} KB → ${(processedSize / 1024).toFixed(2)} KB)`,
);
}
stepStartTime = Date.now();
const parsedPath = path.parse(inputFile);
const finalFilename = path.join(
outputDirectory,
`${parsedPath.name}-${hash}${parsedPath.ext}`,
);
if (!fs.existsSync(finalFilename)) {
await fs.promises.writeFile(finalFilename, processedCSS);
console.log(
`⏱️ [${Date.now() - stepStartTime}ms] CSS Write: ${cssPath}`,
);
} else {
console.log(
`⏱️ [${Date.now() - stepStartTime}ms] CSS Write Skip: ${cssPath} (already exists)`,
);
}
const brotliFilename = `${finalFilename}.br`;
stepStartTime = Date.now();
if (!fs.existsSync(brotliFilename)) {
const brotliOptions = { level: brotliCompressionLevel };
const brotliBuffer = brotliCompressSync(
Buffer.from(processedCSS),
brotliOptions,
);
const brotliSize = brotliBuffer.length;
const processedSize = processedCSS.length;
await fs.promises.writeFile(brotliFilename, brotliBuffer);
console.log(
`⏱️ [${Date.now() - stepStartTime}ms] CSS Brotli: ${cssPath} (level ${brotliCompressionLevel}, ${(processedSize / 1024).toFixed(2)} KB → ${(brotliSize / 1024).toFixed(2)} KB)`,
);
} else {
console.log(
`⏱️ [${Date.now() - stepStartTime}ms] CSS Brotli Skip: ${cssPath} (already exists)`,
);
}
const totalTime = Date.now() - fileStartTime;
const hashedPath = brotliFilename
.replace(path.join("./_site"), "")
.replace(/\\/g, "/");
console.log(
`✅ CSS Complete: ${cssPath} in ${totalTime}ms (${(totalTime / 1000).toFixed(2)}s)`,
);
const result = `<link rel="stylesheet" href="${hashedPath}">`;
cssBuildCache.set(cssPath, { hash, processedCSS, htmlOutput: result });
return result;
} catch (err) {
const totalTime =
typeof fileStartTime !== "undefined" ? Date.now() - fileStartTime : 0;
console.error(`❌ CSS Error: ${cssPath} after ${totalTime}ms:`, err);
return "";
}
})();
cssBuildCache.set(cssPath, processingPromise);
return await processingPromise;
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment