diff --git a/_includes/base.njk b/_includes/base.njk index a1186f7..d94be0d 100644 --- a/_includes/base.njk +++ b/_includes/base.njk @@ -4,8 +4,43 @@ {{ title }} | Volpe - - + + {# Critical CSS inlined for faster initial render #} + + + {# Preload full stylesheet with cache-busted filename #} + + + + {# Defer prism.css - only needed for syntax highlighting #} + + +
diff --git a/eleventy.config.js b/eleventy.config.js index f1d7345..6bf9b09 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -4,8 +4,26 @@ const markdownItFootnote = require("markdown-it-footnote"); const markdownItMermaid = require('markdown-it-mermaid').default const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight"); const fs = require("fs"); +const crypto = require("crypto"); +const path = require("path"); const { DateTime } = require("luxon"); +const cssHashCache = {}; +const getCssHash = (cssFile) => { + if (cssHashCache[cssFile]) return cssHashCache[cssFile]; + + const cssPath = path.join(__dirname, "css", cssFile); + try { + const content = fs.readFileSync(cssPath, "utf-8"); + const hash = crypto.createHash("md5").update(content).digest("hex").slice(0, 8); + cssHashCache[cssFile] = hash; + return hash; + } catch (e) { + console.warn(`Could not hash CSS file: ${cssFile}`); + return null; + } +}; + const extractTags = (content, mdInstance) => { if (!content) return []; @@ -220,7 +238,36 @@ module.exports = (eleventyConfig) => { return tagMap; }); - eleventyConfig.addPassthroughCopy("css"); + // Cache busting filter: returns hashed CSS filename + eleventyConfig.addFilter("cssHash", (cssFile) => { + const hash = getCssHash(cssFile); + const ext = path.extname(cssFile); + const base = path.basename(cssFile, ext); + return `/css/${base}.${hash}${ext}`; + }); + + eleventyConfig.on("eleventy.before", async () => { + const cssDir = path.join(__dirname, "css"); + const outputCssDir = path.join(__dirname, "_site", "css"); + + if (!fs.existsSync(outputCssDir)) { + fs.mkdirSync(outputCssDir, { recursive: true }); + } + + const cssFiles = fs.readdirSync(cssDir).filter(f => f.endsWith(".css")); + for (const cssFile of cssFiles) { + const hash = getCssHash(cssFile); + const ext = path.extname(cssFile); + const base = path.basename(cssFile, ext); + const hashedName = `${base}${hash == null ? '' : `.${hash}`}${ext}`; + + fs.copyFileSync( + path.join(cssDir, cssFile), + path.join(outputCssDir, hashedName) + ); + } + }); + eleventyConfig.addPassthroughCopy("robots.txt"); eleventyConfig.ignores.add("README.md"); diff --git a/nix/module.nix b/nix/module.nix index 2ddc24b..796dc2d 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -20,6 +20,14 @@ locations."/" = { tryFiles = "$uri $uri/ /index.html"; }; + # Cache static assets (CSS, JS, images) for 1 year with immutable + locations."~* \\.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$" = { + extraConfig = '' + expires 1y; + add_header Cache-Control "public, max-age=31536000, immutable"; + access_log off; + ''; + }; }; in { options.services.volpe = {