Cashbusting files to template

Hello there,
I have been coding WordPress themes for the last years and now I am building my first website with Kirby CMS. Looks very promising and I am looking forward to doing a lot more Kirby projects in the future. I have added the yarn/webpack pipeline from the Sage WP theme (GitHub - roots/sage: WordPress starter theme with Laravel Blade components and templates, Tailwind CSS, and block editor support), which has integrated cachebusting. Now I am facing the question how I can get the generated hashed versions of JS or CSS files to the website code.

Can anyone give me some support here?

Thanks and best regards from Austria,
Matthias

1 Like

In Kirby we tend to do the cache busting for CSS/JS in a tooling agnostic manner via plugins who automate this for your via PHP. Then you can safely remove the cache busting part from your development pipeline.

A good start would be to have a look at https://github.com/bnomei/kirby3-fingerprint .

Thanks for your answer - I will try with the plugin!

A more lightweight method would be to add a css and js component in a plugin.
You can do something like this and use the default css() and js() helper functions.

This solution will “only” add a query string with the current timestamp of the file. But in most cases this is sufficient. And if you do not user the @auto feature you can ditch the complete if statement.

<?php

Kirby::plugin('lukaskleinschmidt/cachebuster', [
    'components' => [
        'css' => function ($kirby, $url, $options) {
            if ($url === '@auto') {
                if (!$url = Url::toTemplateAsset('styles/templates', 'css')) {
                    return null;
                }
            }

            return $url . '?v=' . F::modified($url);
        },
        'js' => function ($kirby, $url, $options) {
            if ($url === '@auto') {
                if (!$url = Url::toTemplateAsset('scripts/templates', 'js')) {
                    return null;
                }
            }

            return $url . '?v=' . F::modified($url);
        }
    ],
]);
2 Likes

Thanks for your input, Lukas. That solution looks very clean and nice to me.

you could also use laravel-mix to generate the assets and use the included manifest file to get proper hashes.

Query strings for cache busting is an anti-pattern as it confuses proxies etc. Adding it to the file name should do the trick.

Imho this should be in core, as it works this way for the assets from the plugins. I’ve added this as enhancement request in github: https://github.com/getkirby/kirby/issues/1474

Let’s see what the devs say.

Thanks everybody for your support!

Hey Lukas,

what do you think about something like this:

<?php

/**
 * Get cache-busting hashed filename from assets.json.
 *
 * @param  string $filename Original name of the file.
 * @return string Current cache-busting hashed name of the file.
 */
function get_hashed_asset( $filename ) {

  // Cache the decoded manifest so that we only read it in once.
  static $manifest = null;
  if ( null === $manifest ) {
    $manifest_path = kirby::instance()->roots()->assets() . '/assets.json';
    $manifest = file_exists( $manifest_path )
      ? json_decode( file_get_contents( $manifest_path ), true )
      : [];
  }

  // Get rid of asset folder in path to match filename in manifest
  $filename = str_replace('assets/', '', $filename);

  // If the manifest contains the requested file, return the hashed name.
  if ( array_key_exists( $filename, $manifest ) ) {
    $hashed_filename = $manifest[ $filename ];
    // Add asset folder path to hashed file again
    return 'assets/' . $hashed_filename;
  }

  // Assume the file has not been hashed, when it was not found within the manifest and add asset folder path
  return 'assets/' . $filename;
}

Kirby::plugin('test/cachebusting', [
  'components' => [
    'css' => function ($kirby, $url, $options) {
      return get_hashed_asset($url);
    },
    'js' => function ($kirby, $url, $options) {
      return get_hashed_asset($url);
    }
  ],
]);

It works, but for sure it can be improved. It checks, if an json-manifest file exists + simply compares the filenames. If there is a hashed version the filename is adjusted. I appreciate your suggestions or feedback. Thanks!

maybe something like this (untested)

/////////////////////////////////////////
// src/Cachebusting.php
namespace Test;

class Cachebusting {

    static private $manifest = null;

    private static function manifest(): array {
        if(static::$manifest) {
            return static::manifest;
        }

        $m = option('test.cachebusting.manifest');
        if(is_callable($m)) {
            $m = $m();
        }
        $m = array_map(function($a) {
            if(Kirby\Toolkit\F::exists(kirby()->root('assets') . '/' . $a)) {
                return kirby()->url('assets') . '/' . $a;
            }
            return $a;
        }, $m);

        static::$manifest = $m;
        return static::$manifest;
    }

    public static function fingerprint(string $url) {
        return \Kirby\Toolkit\A::get(static::manifest(), $url, $url);
    }
}

/////////////////////////////////////////
// index.php
load([
    'test\\cachebusting' => 'src/Cachebusting.php'
], __DIR__);


Kirby::plugin('test/cachebusting', [
    'options' => [
        'manifest.file' => 'assets.json',

        // this is an option so you could also just hardcode the array in the config file
        'manifest' => function(): array {
            $path = kirby()->root('assets') . '/' . option('test.cachebusting.manifest.file');
            return \Kirby\Toolkit\F::exists($path) ? \Kirby\Data\Json::read($path) : [];
        }
    ],
    'components' => [
        'css' => function ($kirby, string $url, $options) {
            return \Test\Cachebusting::fingerprint($url);
        },
        'js' => function ($kirby, string $url, $options) {
            return \Test\Cachebusting::fingerprint($url);
        }
    ],
]);

Great, looks a lot better. I will test it soon, thanks a lot!