11ty / eleventy

A simpler site generator. Transforms a directory of templates (of varying types) into HTML.
https://www.11ty.dev/
MIT License
17.09k stars 494 forks source link

Function to transform asset file names for cache busting #2927

Open rightaway opened 1 year ago

rightaway commented 1 year ago

There's no way to do cache busting based on file content hash without hacks or using a separate process or external dependency. Some of those options are in https://github.com/11ty/eleventy/issues/272.

It's actually quite simple to do like in https://github.com/keithamus/postcss-hash, but it's not possible to hook into eleventy's file outputting. Can we have a function like this?

eleventyConfig.transformAssetFileNames({
  globs: ["**/*.png", "asset/data.json", "**/*.jpg"],
  hashAlgorithm: "sha256",
  hashLength: 10,
  name: (dirs, name, hash, ext) => `${path.join(...dirs)}/${name}-${hash}.${ext}`,
  output: "_data/manifest.json",
})

That would output _data/manifest.json

{
  "dir1/dir/file.png": "dir1/dir/file-1234567890.png",
  "asset/data.json": "asset/asset-1234567890.json",
  "dir2/dir/file.jpg": "dir2/dir/file-1234567890.jpg"
}

It would make cache busting very simple to do with an asset filter which would get the transformed file name from the file in output (_data/manifest.json). Then <img src={{ "dir1/dir/file.png" | asset }}> will become <img src=dir1/dir/file-1234567890.png>.

rightaway commented 1 year ago

Eleventy doesn't even need to output a physical manifest.json file for this. output key in the function argument isn't needed.

puleddu commented 1 year ago

I stumbled upon many conversations and blog posts regarding styles and javascript (i.e. global assets) cache-busting, but not very much is being said about the issue you raise here: content assets, like images in a blog posts or an article.

Cache-busting for content assets is critical for things like documentation websites. I'm surprised nothing close to a best practice exists in that regard. Am I missing something, or is 11ty simply not meant to be used like that? Is there any solid resource I can refer to, in order to setup my configuration to provide cache-busting for content assets?

davekerber commented 1 year ago

I was able to do this with built in eleventy events using this.

My contents are in src/site and my output is to _site

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

module.exports = function(eleventyConfig) {

  // A cache to store the hashed file names
  const hashCache = {};

  // A cache buster if a file changes
  const prefixLength ="./src/site".length
  eleventyConfig.on('eleventy.beforeWatch', async (changedFiles) => {
    for(const file of changedFiles) {
      const relativePath = file.slice(prefixLength)
      delete hashCache[relativePath]
    }
  });

  // A filter to dynamically hash asset file contents
  eleventyConfig.addFilter("digest", async (filePath)  => {
    // If we've already hashed this file, return the hash
    if(hashCache[filePath]) {
      return hashCache[filePath];
    }

    // Get the absolute path to the file inside of src/site
    const absolutePath = path.join(__dirname, 'src/site', filePath);

    // Digest the file
    const fileBuffer = fs.readFileSync(absolutePath);
    const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
    const relativePath = filePath.slice(0, path.basename(filePath).length * -1)
    const digestFileName = `${relativePath}${hash}-${path.basename(filePath)}`;

    // See if the digest file exists in the output folder _site
    const digestFilePath = path.join(__dirname, '_site', digestFileName);
    hashCache[filePath] = digestFileName;
    if(!fs.existsSync(digestFilePath)) {
      if(!fs.existsSync(path.dirname(digestFilePath))) {
        fs.mkdirSync(path.dirname(digestFilePath), { recursive: true });
      }
      fs.copyFileSync(absolutePath, digestFilePath);
    }
    // Return the digest file name
    return digestFileName;
  })

  // other stuff

  return {
    dir: {
      input: "src/site", // source files
      output: "_site", // destination folder
      includes: "_includes", // folder for layouts and includes
      data: "_data" // folder for data files
    }
  };
};

Then I digest assets like this:

<link href="{{"/css/style.css" | digest}}" rel="stylesheet">