getgrav / grav

Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS powered by PHP, Markdown, Twig, and Symfony
https://getgrav.org
MIT License
14.46k stars 1.4k forks source link

Feature request: Ability to use versioning/hashing/cache busting on file change #1330

Open Sogl opened 7 years ago

Sogl commented 7 years ago

Hello!

Grav has option enable_asset_timestamp. It provide assets links like this:

`js/main.js?3939200292`

But this timestamp based on config, pages changes or cache clear. Is it possible to add changes based on file content changes (or file date modified/changed)?

There is also another way. We can use gulp extension to output filenames like:

unicorn-d41d8cd98f.css

But Grav assets has no option to add files with wildcard (regex maybe). How it's done in Laravel framework: https://scotch.io/tutorials/run-gulp-tasks-in-laravel-easily-with-elixir#versioning-or-hashing

mahagr commented 7 years ago

Elixir is able to change filenames because of it compiles the css and js files on demand. This is not as easy task if you just include existing file as the file would need to be either copied or you need PHP script and/or .htaccess hacks to support re-routing the filename to the proper file.

Sogl commented 7 years ago

@rhukster Why this issue closed? I think Grav could accept wildcard filenames or something and it might be useful not only for the hashing.

rhukster commented 7 years ago

It's just not something we want to do at this time. This is something that is very specific to your use case, and would require logic that slowed things down in Grav for little benefit to 99.9% of people. We have other much more pressing things to work on.

If you want to write a PR for this feature I will consider it, but its just not going to benefit enough people for us to put our limited time into.

NicoHood commented 3 years ago

Is there a chance to add this feature to the asset manager? I know that this already works when enabling the pipeline. I did not use the pipeline yet, as each site has its own css depending on what plugins are active on the site, which will slow things down.

NicoHood commented 3 years ago

I've written a simple hash approach that also makes use of the file timestamp and the grav cache (it is fast!). It should be added to BaseAssets.php line 140:

if ($this->modified) {
    $cache = Grav::instance()['cache'];
    $cache_id = hash('sha256', 'asset-pipeline' . $cache->getKey() . '-' . $asset . '-' . $this->modified);

    // Cache file hash result for 7 days
    if (!($cache_busting_hash = $cache->fetch($cache_id))) {
        $crc = implode(unpack('C*', hash_file('crc32b', $file->getPathname(), true)));
        $cache_busting_hash = substr(str_pad($crc, 10, '0', STR_PAD_LEFT), 0, 10);
        $cache->save($cache_id, $cache_busting_hash, 604800);
    }

    // Add hash to file
    $path_parts = pathinfo(trim($asset));
    $asset = $path_parts['filename'] . '.' . $cache_busting_hash . '.' . $path_parts['extension'];
}

This would be the rewrite rule (Make sure to add them at the TOP of the other grav rules!):

# Rewrites asset versioning, ie styles.1399647655.css to styles.css.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)\.(\d{10})\.(js|css)$ $1.$3 [L]

I've taken some of the ideas from https://github.com/aelvan/Stamp-Craft/tree/craft3 An additional config option is yet missing.

It would be nice, if you could give me some feedback about that.

NicoHood commented 3 years ago

Another hack is to process the asset pipeline output via a custom twig filter. The filter can be added in the theme.

    public function onTwigInitialized()
    {
        // Custom twig filter for cache busting
        $this->grav['twig']->twig()->addFilter(
            new \Twig_SimpleFilter('chache_busting', [$this, 'chache_busting'])
        );
    }

    /**
     * Add a cache busting string to all css and js files
     * TODO workaround until implemented by upstream:
     * https://github.com/getgrav/grav/issues/1330#issuecomment-752031229
     */
    public function chache_busting(string $input, bool $enable = true): string
    {
        if (!$enable) {
            return $input;
        }
        $cache = Grav::instance()['cache'];

        $output = preg_replace_callback(
          '#((href|src)=")((\/.*)?(\/.+)(\.(css|js)))(")#',
          function ($match) use ($cache) {
            // Extract filename and path
            $filename = $match[3];
            $path = GRAV_ROOT . $filename;

            // Get modified time
            $file = new \SplFileInfo($path);
            $modified = $file->isFile() ? $file->getMTime() : false;

            // Cache file hash result for 7 days
            $cache_id = hash('sha256', 'asset-pipeline' . $cache->getKey() . '-' . $filename . '-' . $modified);
            if (!($cache_busting_hash = $cache->fetch($cache_id))) {
                $crc = implode(unpack('C*', hash_file('crc32b', $file->getPathname(), true)));
                $cache_busting_hash = substr(str_pad($crc, 10, '0', STR_PAD_LEFT), 0, 10);
                $cache->save($cache_id, $cache_busting_hash, 604800);
            }
            return $match[1] . $match[4] . $match[5] . '.' . $cache_busting_hash . $match[6] . $match[8];
          },
          $input
        );

        return $output;
    }
{% block assets deferred %}
    {# TODO cache busting workaround until implemented by upstream:
    https://github.com/getgrav/grav/issues/1330#issuecomment-752031229 #}
    {{ assets.css()|chache_busting(theme_var('production-mode'))|raw }}
    {{ assets.js()|chache_busting(theme_var('production-mode'))|raw }}
{% endblock %}
mahagr commented 3 years ago

It's beter not to do this in a closed issue as nobody looks into closed issues. I wouldn't use cache at all though, it's enough just to take the file modification time and put append that into the filename with this method.

NicoHood commented 3 years ago

I thought about this too, but the downside is, that the client will reload the css, even if it did not change. We want to reduce client loading time as best as possible, so the hash would be the better option from my understanding.

Edit: You can reopen the issue, I cannot.

mahagr commented 3 years ago

FYI: Usually I don't reopen any issues which are years old because of the code usually changes in that time and most of the time even if people say it's the same issue, it really isn't. If there are commits in the issue, then I will never reopen the issue unless there was regression and the release isn't out yet.