chesio/bc-cache

Simple disk cache plugin inspired by Cachify.

Installs: 3

Dependents: 0

Suggesters: 0

Security: 0

Stars: 9

Watchers: 2

Forks: 0

Open Issues: 1

Type:wordpress-plugin

3.3.2 2024-04-22 11:53 UTC

README

GitHub Actions Packagist

Modern and simple full page cache plugin for WordPress inspired by Cachify.

BC Cache has no settings page - it is intended for webmasters who are familiar with .htaccess files and WordPress actions and filters.

BC Cache can cache not only HTML pages, but core XML sitemaps as well. Technically, any content type generated by WordPress for front-end requests routed via root index.php file can be handled provided that output generation triggers send_headers hook.

Requirements

Limitations

  • BC Cache has not been tested on WordPress multisite installation.
  • BC Cache has not been tested on Windows servers.
  • BC Cache does not support content negotiation via Accept header.

Installation

BC Cache is not available at WordPress Plugins Directory, but there are several other ways you can get it.

Using WP-CLI

If you have WP-CLI installed, you can install (and optionally activate) BC Cache with a single command:

wp plugin install [--activate] https://github.com/chesio/bc-cache/archive/master.zip

Using Composer

Composer is a great tool for managing PHP project dependencies. Although WordPress itself does not make it easy to use Composer to manage WordPress installation as a whole, there are multiple ways how to do it.

BC Cache is available at Packagist, so just run composer require chesio/bc-cache as usual.

Using Git

Master branch always contains latest stable version, so you can install BC Cache by cloning it from within your plugins directory:

cd [your-project]/wp-content/plugins
git clone --single-branch --branch master https://github.com/chesio/bc-cache.git

Updating is as easy as:

cd [your-project]/wp-content/plugins/bc-cache
git pull

Using Git Updater plugin

Once installed, BC Cache can be kept up to date via Git Updater plugin. To install it either use the direct download method described below or use Git Updater Pro.

Direct download

This method is the least recommended, but it works without any other tool. You can download BC Cache directly from GitHub. Make sure to unpack the plugin into correct directory and drop the version number from folder name.

Setup

You have to configure your Apache webserver to serve cached files. Most common way to do it is to add the lines below to the root .htaccess file. This is the same file to which WordPress automatically writes pretty permalinks configuration - you must put the lines below before the pretty permalinks configuration.

Note: the configuration below assumes that you have WordPress installed in wordpress subdirectory - if it is not your case, simply drop the /wordpress part from the following rule: RewriteRule .* - [E=BC_CACHE_ROOT:%{DOCUMENT_ROOT}/wordpress]. In general, you may need to make some tweaks to the example configuration below to fit your server environment.

# BEGIN BC Cache
AddDefaultCharset utf-8

<IfModule mod_rewrite.c>
  RewriteEngine on

  # Configure document root.
  RewriteRule .* - [E=BC_CACHE_ROOT:%{DOCUMENT_ROOT}/wordpress]

  # Get request scheme (either http or https).
  RewriteRule .* - [E=BC_CACHE_SCHEME:http]
  RewriteCond %{ENV:HTTPS} =on [OR]
  RewriteCond %{HTTP:X-Forwarded-Proto} https
  RewriteRule .* - [E=BC_CACHE_SCHEME:https]

  # Clean up hostname (drop optional port number).
  RewriteCond %{HTTP_HOST} ^([^:]+)(:[0-9]+)?$
  RewriteRule .* - [E=BC_CACHE_HOST:%1]

  # Set path subdirectory - its suffix depends on whether URI ends with slash or not.
  RewriteRule .* - [E=BC_CACHE_PATH:%{REQUEST_URI}/@file/]
  RewriteCond %{REQUEST_URI} /$
  RewriteRule .* - [E=BC_CACHE_PATH:%{REQUEST_URI}@dir/]

  # Set request variant (by default there is only empty one).
  RewriteRule .* - [E=BC_CACHE_REQUEST_VARIANT:]

  # Optionally, serve gzipped version of the file.
  RewriteRule .* - [E=BC_CACHE_FILE:index%{ENV:BC_CACHE_REQUEST_VARIANT}]
  <IfModule mod_mime.c>
    RewriteCond %{HTTP:Accept-Encoding} gzip
    RewriteRule .* - [E=BC_CACHE_FILE:index%{ENV:BC_CACHE_REQUEST_VARIANT}.gz]
  </IfModule>

  # Main rules: serve only GET requests with whitelisted query string fields coming from anonymous users.
  RewriteCond %{REQUEST_METHOD} GET
  RewriteCond %{QUERY_STRING} ^(?:(?:_gl|gclid|gclsrc|fbclid|msclkid|utm_(?:source|medium|campaign|term|content))=[\w\-]*(?:&|$))*$
  RewriteCond %{HTTP_COOKIE} !(wp-postpass|wordpress_logged_in|comment_author)_
  RewriteCond %{ENV:BC_CACHE_ROOT}/wp-content/cache/bc-cache/%{ENV:BC_CACHE_SCHEME}_%{ENV:BC_CACHE_HOST}%{ENV:BC_CACHE_PATH}%{ENV:BC_CACHE_FILE} -f
  RewriteRule .* %{ENV:BC_CACHE_ROOT}/wp-content/cache/bc-cache/%{ENV:BC_CACHE_SCHEME}_%{ENV:BC_CACHE_HOST}%{ENV:BC_CACHE_PATH}%{ENV:BC_CACHE_FILE} [L,NS]

  # Do not allow direct access to cache entries.
  RewriteCond %{REQUEST_URI} /wp-content/cache/bc-cache/
  RewriteCond %{ENV:REDIRECT_STATUS} ^$
  RewriteRule .* - [F,L]
</IfModule>
# END BC Cache

Configuration

BC Cache has no settings. You can modify plugin behavior with PHP constants, WordPress filters and theme features.

Constants

Two advanced features of the plugin can be optionally disabled with a constant.

The cache warm up feature can be disabled by setting BC_CACHE_WARM_UP_ENABLED constant with a false value:

define('BC_CACHE_WARM_UP_ENABLED', false);

File locking is used to ensure atomicity of operations that manipulate the cache. If your webserver has issues with flock() you may want to disable use of file locking by setting BC_CACHE_FILE_LOCKING_ENABLED constant with a false value:

define('BC_CACHE_FILE_LOCKING_ENABLED', false);

Both constant must be defined at the time the plugin boots - typically the best place to define them is wp-config.php file. It is recommended to set the constants before activating the plugin.

Filters

If there was a settings page, following filters would likely become plugin settings as they alter basic functionality:

  • bc-cache/filter:can-user-flush-cache - filters whether current user can clear the cache. By default, any user with manage_options capability can clear the cache.
  • bc-cache/filter:html-signature - filters HTML signature appended to HTML files stored in cache. You can use this filter to get rid of the signature: add_filter('bc-cache/filter:html-signature', '__return_empty_string');
  • bc-cache/filter:cached-response-headers - filters HTTP response headers generated by WordPress/PHP that are kept when cache entry is created. These headers are then sent as additional response headers with cached content. By default following headers are kept: Content-Type, Link, X-Pingback, X-Robots-Tag, X-BC-Cache-Generated. Note that Content-Type header is crucial for proper delivery of cached content and thus is always preserved.
  • bc-cache/filter:cache-generation-timestamp-format - filters format of cache generation timestamp. This timestamp is included in HTML signature and in X-BC-Cache-Generated HTTP response header. The value must be a valid $format argument for wp_date().

Filters for advanced functions

Following filters can be used to tweak automatic cache flushing:

  • bc-cache/filter:flush-hooks - filters list of actions that trigger cache flushing. Filter is executed in a hook registered to init action with priority 10, so make sure to register your hook earlier (for example within plugins_loaded or after_setup_theme actions).
  • bc-cache/filter:is-public-post-type - filters whether given post type should be deemed as public or not. Publishing or trashing of public post type items triggers automatic cache flushing, but related action hooks cannot be adjusted with the bc-cache/filter:flush-hooks filter, you have to use this filter.
  • bc-cache/filter:is-public-taxonomy - filters whether given taxonomy should be deemed as public or not. Creating, deleting or editing terms from public taxonomy triggers automatic cache flushing, but related action hooks cannot be adjusted with the bc-cache/filter:flush-hooks filter, you have to use this filter.

Following filters can be used to extend list of cache exclusions or whitelist some query string parameters:

  • bc-cache/filter:skip-cache - filters whether response to current HTTP(S) request should be cached. Filter is only executed, when none from built-in skip rules is matched - this means that you cannot override built-in skip rules with this filter, only add your own rules.
  • bc-cache/filter:query-string-fields-whitelist - filters list of query string fields that do not prevent cache write.

Following filters are necessary to set up request variants:

  • bc-cache/filter:request-variant - filters name of request variant of current HTTP request.
  • bc-cache/filter:request-variants - filters list of all available request variants. You should use this filter, if you use variants and want to have complete and proper information about cache entries listed in Cache Viewer.

Following filters can be used to tweak warming up of cache:

  • bc-cache/filter:cache-warm-up-initial-url-list - filters list of initial URLs to be included in warm up. This filter is used to shortcut default processing: if it returns an array (even empty), no URLs are read from XML sitemap(s).
  • bc-cache/filter:cache-warm-up-final-url-list - filters the final list of URLs to be included in warm up.
  • bc-cache/filter:cache-warm-up-invocation-delay - filters the time (in seconds) between cache flush and warm up invocation.
  • bc-cache/filter:cache-warm-up-run-timeout - sets the time (in seconds) warm up crawler is allowed to run within single WP-Cron invocation. The value cannot be larger than value of WP_CRON_LOCK_TIMEOUT constant. Note that crawler stops only after this limit is reached. This means for example that even if the timeout is set to 0, there is one HTTP request sent.
  • bc-cache/filter:cache-warm-up-request-arguments - filters list of arguments of HTTP request run during warm up.

Following filters are only useful if your theme declares support for caching for front-end users:

  • bc-cache/filter:frontend-user-capabilities - filters list of capabilities of front-end users.
  • bc-cache/filter:is-frontend-user - filters whether current user is a front-end user.
  • bc-cache/filter:frontend-user-cookie-name - filters name of front-end user cookie.
  • bc-cache/filter:frontend-user-cookie-value - filters contents of front-end user cookie.

Theme features

Some advanced features must be supported by your theme and are active only if the theme explicitly declares its support for particular feature:

Automatic cache flushing

The cache is flushed automatically on core actions listed below. The list of actions can be filtered with bc-cache/filter:flush-hooks filter.

Special handling of posts and terms

In WordPress, posts can be used to hold various types of data - including data that is not presented on frontend in any way. To make cache flushing as sensible as possible, when a post is published or trashed the cache is flushed only when post type is public. You may use bc-cache/filter:is-public-post-type filter to override whether a particular post type is deemed as public for cache flushing purposes or not.

Note: Changing post status to draft, future or pending always triggers cache flush (regardless of the post type).

Terms (taxonomies) are handled in a similar manner - cache is automatically flushed when a term is created, deleted or edited, but only in case of terms from a public taxonomy. You may use bc-cache/filter:is-public-taxonomy filter to override whether a particular taxonomy should be deemed as public or not.

Flushing the cache programmatically

If you want to flush BC Cache cache from within your code, just call do_action('bc-cache/action:flush-cache'). Note that the action is available after the init hook with priority 10 is executed.

Scheduled cache flushing

Flushing of BC Cache cache on given schedule can be easily achieved with WP-Cron - you only have to hook the bc-cache/action:flush-cache action to a scheduled event. Following WP-CLI command sets WP-Cron event that triggers cache flush every midnight:

wp cron event schedule 'bc-cache/action:flush-cache' midnight daily

Cache exclusions

A response to HTTP(S) request is not cached by BC Cache if any of the conditions below evaluates as true:

  1. Request is a POST request.
  2. Request is a GET request with one or more query string fields that are not whitelisted. By default, the whitelist consists of Google click IDs, Facebook Click Identifier, Microsoft Click ID and standard UTM parameters, but it can be filtered.
  3. Request is not for a front-end page (ie. wp_using_themes returns false). Output of AJAX, WP-CLI or WP-Cron calls is never cached.
  4. Request comes from a non-anonymous user (ie. user that is logged in, left a comment or accessed password protected page/post). The rule can be tweaked to ignore front-end users if your theme supports it.
  5. Request/response type is one of the following: search, 404, feed, trackback, robots.txt, preview or password protected post.
  6. Fatal error recovery mode is active.
  7. DONOTCACHEPAGE constant is set and evaluates to true. This constant is for example automatically set by WooCommerce on certain pages.
  8. Return value of bc-cache/filter:skip-cache filter evaluates to true.

Important! Cache exclusion rules are essentially defined in two places:

  1. In PHP code (including bc-cache/filter:skip-cache filter), the rules are used to determine whether current HTTP(S) request should be written to cache.
  2. In .htaccess file, the rules are used to determine whether current HTTP(S) request should be served from cache.

When you add new rule for cache writing via bc-cache/filter:skip-cache filter, you should always consider whether the rule should be also enforced for cache reading via .htaccess file. In general, if your rule has no relation to request URI (for example you check cookies or User-Agent string), you probably want to have the rule in both places. The same applies to bc-cache/filter:query-string-fields-whitelist filter - any extra whitelisted fields will not prevent cache writing anymore, but will still prevent cache reading unless they are integrated into respective rule in .htaccess file.

Cache viewer

Contents of cache can be inspected (by any user with manage_options capability) via Cache Viewer management page (under Tools). Users who can flush the cache are able to delete individual cache entries.

You may encounter a warning in Cache Viewer about total size of cache files being different from total size of files in cache folder - this usually means that you failed to correctly provide list of all available request variants via bc-cache/filter:request-variants filter.

Front-end users and caching

Note: front-end user is any user that has no business accessing /wp-admin area despite being able to log in via wp-login.php. Although the implementation details do not presume any particular plugin, following text is written with WooCommerce (and registered customers as front-end users) in mind.

Depending on your theme, the HTML served to front-end users can be identical to the HTML served to anonymous users. Such themes most often fetch any personalized content (like items added to cart) via a JavaScript call. In such case there is no reason to exclude front-end users from full page caching.

There is a catch though...

Unlike some other content management systems, WordPress does not distinguish between back-end and front-end users. The same authentication mechanism is used to authenticate back-end users (like shop managers) and front-end users (like shop customers). As a fact, you cannot use the same email address to create a test customer account as you had used for shop manager account.

BC Cache by default does not read from or write to cache when HTTP request comes from any logged-in user:

  1. When call to is_user_logged_in function returns true, response to HTTP request is not written to cache.
  2. When HTTP request has a cookie with wordpress_logged_in in its name, response to HTTP request is not read from cache - this check must be configured in .htaccess file.

When your theme declares support for front-end user caching:

The first check is relaxed automatically with some reasonable defaults: any user that has read and customer capabilities only is considered to be front-end user and any pages he/she visits are written to cache normally. You may filter the capabilities list or the output of the check if you wish so.

To make it possible to relax the second check, BC Cache sets an additional session cookie whenever front-end user logs in. The rule in .htaccess file that deals with login cookie has to be extended as follows:

# The legacy rule is replaced by 3 rules below:
# RewriteCond %{HTTP_COOKIE} !(wp-postpass|wordpress_logged_in|comment_author)_
RewriteCond %{HTTP_COOKIE} !(wp-postpass|comment_author)_
RewriteCond %{HTTP_COOKIE} !wordpress_logged_in_ [OR]
RewriteCond %{HTTP_COOKIE} bc_cache_is_fe_user=true

This way cached pages can be served to front-end users too. Cookie name and content can be adjusted by designated filters - make sure to adapt respective .htaccess rule if you change them.

Request variants

Sometimes a different response body is served to request to the same URL, typically when particular cookie is set or request is made by particular browser/bot. In such cases, BC Cache allows to define request variants and cache/serve different response body based on configured conditions. A typical example is the situation in which privacy policy notice is displayed until site visitor accepts it. The state (cookie policy accepted or not) is often determined based on presence of particular cookie. Using request variants, BC Cache can serve visitors regardless if they have or have not accepted the cookie policy.

Request variant configuration example

A website has two variants: one with cookie notice (cookie_notice_accepted cookie is not set) and one without (cookie_notice_accepted cookie is already set).

Request variant name should be set whenever cookie notice is accepted (example uses API of Cookie Notice plugin):

add_filter('bc-cache/filter:request-variant', function (string $default_variant): string {
    return cn_cookies_accepted() ? '_cna' : $default_variant;
}, 10, 1);

add_filter('bc-cache/filter:request-variants', function (array $variants): array {
    $variants['_cna'] = 'Cookie notice accepted';
    return $variants;
}, 10, 1);

The default configuration needs to be extended as well and set the new variant accordingly:

  # Set request variants (default and "cookie notice accepted"):
  RewriteRule .* - [E=BC_CACHE_REQUEST_VARIANT:]
  RewriteCond %{HTTP_COOKIE} cookie_notice_accepted=true
  RewriteRule .* - [E=BC_CACHE_REQUEST_VARIANT:_cna]

Important: Variant names are appended to basename part of cache file names, so index becomes index_cna and index.gz becomes index_cna.gz in the example above. To make sure your setup will work, use only letters from [a-z0-9_-] range as variant names.

Cache warm up

Since version 2, the plugin performs cache warm up, ie. stores all pages in cache automatically without the need of front-end users to visit them. The obvious advantage is that even the first visitors of particular pages are served from cache (= fast).

Internally, the warm up process is hooked to WP-Cron and the website is crawling itself in the background. This automatic crawling is kicked up every time cache is flushed (with a 10 minutes delay by default, but this can be configured).

Since version 2.2, cache warm up can be triggered immediately from Cache Viewer. Also, the cache can be warmed up from command line via following WP-CLI command: wp bc-cache warm-up

In order for the warm up to function properly:

  • Website has to have XML sitemap(s) available. URL of the XML sitemap has to be either advertised in robots.txt file or has to be (default) <home-url>/sitemap.xml. XML sitemap index is supported, but not recursively.
  • In case request variants are used, the bc-cache/filter:cache-warm-up-request-arguments filter should be used to modify arguments of HTTP request to any non-default URL variant, so the website generates correct response to such request.
  • It is highly recommended to hook WP-Cron into system task scheduler for increased performance.

Cache warm up configuration examples

Invoke cache warm up just 5 minutes after last cache flush:

add_filter('bc-cache/filter:cache-warm-up-invocation-delay', function (): int { return 5 * MINUTE_IN_SECONDS; }, 10, 0);

Allow only single warm up HTTP request per WP-Cron invocation:

add_filter('bc-cache/filter:cache-warm-up-run-timeout', '__return_zero', 10, 0);

Modify arguments of HTTP request to get page variant with cookie notice accepted (see request variant configuration example for context):

add_filter('bc-cache/filter:cache-warm-up-request-arguments', function (array $args, string $url, string $request_variant): array {
    if ($request_variant === '_cna') {
        $args['cookies'] = [
            'cookie_notice_accepted' => 'true',
        ];
    }
    return $args;
}, 10, 3);

WP-CLI commands

You might use WP-CLI to delete specific posts/pages form cache, flush entire cache, run cache warm up, get size information or even list all cache entries. BC Cache registers bc-cache command with following subcommands:

  • delete <post-id> - deletes cache data (all request variants) of post/page with given ID
  • remove <url> - deletes cache data (all request variants) of given URL
  • flush - flushes entire cache
  • warm-up - runs cache warm up
  • size [--human-readable] - retrieves cache directory apparent size, optionally in human readable format
  • list [<column>...] [--format=<format>] [--plain] [--sort-by=<column>] - list cache entries, optionally in specified format or sorted

Integration with 3rd-party plugins and tools

Autoptimize

Autoptimize is a very popular plugin to optimize script and styles by aggregation, minification, caching etc. BC Cache automatically flushes its cache whenever Autoptimize cache is purged.

Cookie Notice

Cookie Notice is a popular plugin to display a simple, customizable website banner helpful achieving compliance with certain cookie consent requirements. BC Cache automatically flushes its cache whenever Cookie Notice banner configuration is changed.

7G firewall

If you happen to have 7G firewall by Jeff Starr installed on your website, you may have to alter the rule in 7G:[REQUEST URI] section that prevents access to .gz files (note that the code snippet below has been shortened with ... for better readability):

RewriteCond %{REQUEST_URI} (\.)(7z|...|git|gz|hg|...|zlib)$ [NC,OR]

If you see 403 errors instead of cached pages, you have to either remove the |gz part from the RewriteCond line above or remove the line completely.

Credits

  • Sergej Müller & Plugin Kollektiv for inspiration in form of Cachify plugin.
  • Font Awesome for HDD icon
  • Tim Lochmüller for inspirational tweaks to .htaccess configuration taken from his Static File Cache extension