diff --git a/README.md b/README.md index 4d21057..ddafc16 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,47 @@ -# Longcache +# Smartcache -Longcache will set Cache-Control headers for CDNs / reverse proxies to cache WordPress pages for a long period of times (days+). As such this plugin also handles invalidations on content updates, to FLUSH the CDN / reverse proxy on page changes. +Smartcache integrates with Batcache to smartly tune the cache lifetime for your content. -This is a work-in-progress. -## Todo +## Behaviour -- Delete post / transition should trigger invalidate. +By default, caching plugins like Batcache will cache for a static 5 minutes for all content. + +Smartcache dynamically tunes this cache lifetime to match the type of content. It breaks content into three buckets: frequently updated, regular content, and infrequently updated content. + +Frequently updated content is cached for 5 minutes. This content is: + +* The "home" page - this is the page showing the list of posts, not necessarily the "front page" for sites using a static home page. +* Content (posts, pages, etc) published in the past 24 hours + +Regular content is cached for 6 hours. This content is: + +* Content published in the past 30 days (except last 24 hours as above) +* Most archive pages (including categories, tags, author pages) +* Date archive pages, except the current one (today/current month/current year) +* Search pages + +Infrequently updated content is cached for 14 days. This content is: + +* Content published more than 30 days ago +* Pages (except those published recently) +* Date archive pages which aren't the current one +* 404 pages + +Smartcache forces cache invalidation on the CDN in the following cases: + +* Publishing a new piece of content (to invalidate the 404) +* Updating an infrequently updated piece of content + + +## Overriding behaviour + +Smartcache has a variety of filters available. These include: + +* `smartcache.old_threshold` - Filter how long a post must be published before it's considered old (infrequently updated). Default is 7 days. +* `smartcache.is_old_post` - Filter whether a specific post is considered old (infrequently updated). (Default true for posts older than the old threshold.) +* `smartcache.is_new_post` - Filter whether a specific post is considered new (frequently updated). (Default true if published in previous 24 hours.) +* `smartcache.max-age` - Filter the maximum lifetime for the current page directly. +* `smartcache.should_cache` - Filter whether a page should be cached at all. (Only affects whether Smartcache generates a header, but may be overridden by other behaviour or by the cache itself.) +* `smartcache.urls_to_invalidate_for_post` - Filter which URLs to invalidate for a given post. +* `smartcache.should_invalidate` - Should we invalidate URLs for this post? (Default true, false for new content as it will expire naturally quickly.) diff --git a/inc/admin/namespace.php b/inc/admin/namespace.php index bb22251..545e63e 100644 --- a/inc/admin/namespace.php +++ b/inc/admin/namespace.php @@ -14,16 +14,16 @@ */ function bootstrap() : void { add_action( 'admin_menu', __NAMESPACE__ . '\\register_admin_page' ); + add_action( 'admin_init', __NAMESPACE__ . '\\register_settings' ); add_action( 'admin_init', __NAMESPACE__ . '\\check_on_invalidate_urls_submit' ); require_once ABSPATH . '/wp-admin/includes/class-wp-list-table.php'; require_once __DIR__ . '/class-log-list-table.php'; } - function register_admin_page() : void { add_submenu_page( - 'options-general.php', + 'tools.php', _x( 'Smartcache', 'settings page title', 'smartcache' ), _x( 'Smartcache', 'settings menu title', 'smartcache' ), 'manage_options', @@ -32,11 +32,67 @@ function register_admin_page() : void { ); } +function register_settings() { + add_settings_section( + 'smartcache-quota', + __( 'Quota', 'smartcache' ), + __NAMESPACE__ . '\\render_quota_section', + 'smartcache' + ); +} + +function render_quota_section() { + $usage = Smartcache\get_invalidation_quota_usage(); + $quota = Smartcache\get_invalidation_quota(); + + $is_warning = $usage >= ( 0.8 * $quota ); + $is_full = $usage >= $quota; + $class = $is_warning ? 'warning' : ( $is_full ? 'error' : '' ); + ?> + + + + + +
+ Monthly quota usage + + + + + +

+ +

+
+ = $quota; + if ( $exceeded_quota ) { + add_settings_error( 'smartcache', 'quota_exceeded', __( 'You have exceeded your invalidation quota for this month. Contact support.'), 'warning' ); + } + $submit_attr = $exceeded_quota ? 'disabled' : ''; + ?>

+
-

-
- -

- Use * as a wildcard, wildcards can only be at the end of a URL. A maximum of absolute URLs or wildcard URLs can be issued per request. -

- -
+ + + + + + + + + +
+ Invalidate all URLs + +
+ + +
+ +

+ +

+
+ + +
+ +

+ Specify URLs to invalidate, one per line. +

+

+ Use * as a wildcard at the end of a URL. A maximum of absolute URLs or wildcard URLs can be issued per request. +

+

+ If you need to invalidate a lot of URLs, use a single broad wildcard instead of listing each URL. +

+ +
+

Log

post_date_gmt < ( time() - $old_threshold ) ); +} + +function is_new_content( WP_Post $post ) { + return apply_filters( 'smartcache.is_new_post', $post->post_date_gmt > ( time() - DAY_IN_SECONDS ) ); +} + +/** + * Get the default lifetime for the current page. + * + * Determines an appropriate lifetime based on the age and type of the content. + * + * This can be overridden by setting a different max age manually. + * + * @return int One of LIFETIME_SHORT, LIFETIME_MEDIUM, or LIFETIME_LONG. + */ +function get_default_lifetime() : int { + // The home (i.e. post list page) is likely to change more frequently, + // and feed readers should always receive fresh content. + if ( is_home() || is_feed() ) { + return LIFETIME_SHORT; + } + + // Single content depends on how old the content is. + if ( is_singular() ) { + $post = get_queried_object(); + + // If the post was published today, cache it for a shorter time. + // This accounts for fixes to the content, new comments, etc. + if ( is_new_content( $post ) ) { + return LIFETIME_SHORT; + } + + // If the post is older than 7 days, cache it for longer. + // Also, pages are likely to change less frequently. + if ( is_old_content( $post ) || is_page() ) { + return LIFETIME_LONG; + } + + return LIFETIME_MEDIUM; + } + + // Date-based archives won't change after the period is over. + if ( is_date() ) { + $is_current = $is_current = get_query_var( 'year' ) === date( 'Y' ); + if ( is_month() || is_day() ) { + $is_current = $is_current && get_query_var( 'monthnum' ) === date( 'm' ); + } + if ( is_day() ) { + $is_current = $is_current && get_query_var( 'day' ) === date( 'd' ); + } + + return $is_current ? LIFETIME_MEDIUM : LIFETIME_LONG; + } + + // 404 pages never change, except on publication. + if ( is_404() ) { + return LIFETIME_LONG; + } + + // Other archive pages are likely to change less frequently. + if ( is_archive() || is_search() ) { + return LIFETIME_MEDIUM; + } + + // Default to medium lifetime for other pages. + return LIFETIME_MEDIUM; +} + /** * Set the cache TTL depending on the curernt global scope. * @@ -57,7 +201,7 @@ function set_cache_ttl() : void { } global $batcache; - $max_age = absint( apply_filters( 'smartcache.max-age', DAY_IN_SECONDS * 14 ) ); // 14 days by default. + $max_age = absint( apply_filters( 'smartcache.max-age', get_default_lifetime() ) ); if ( ! $batcache || ! is_object( $batcache ) ) { header( 'Cache-Control: s-maxage=' . $max_age . ', must-revalidate' ); } else { @@ -91,6 +235,7 @@ function invalidate_urls( array $urls ) : bool { }, $urls ); try { + increment_invalidation_quota_usage( count( $urls ) ); $result = Cloud\purge_cdn_paths( $urls ); } catch ( Exception $e ) { foreach ( $urls as $url ) { @@ -177,6 +322,21 @@ function on_transition_post_status( string $new_status, string $old_status, WP_P return; } + // If we're *just* publishing the post, ensure it invalidates. + // + // For other new content changes, skip invalidation to avoid rush of traffic during + // high-traffic events. + // + // For old content, invalidate it. + if ( is_new_content( $post ) ) { + $should_invalidate = ( $new_status !== $old_status ); + } else { + $should_invalidate = true; + } + $should_invalidate = apply_filters( 'smartcache.should_invalidate', $should_invalidate, $post ); + if ( ! $should_invalidate ) { + return; + } queue_invalidate_urls( get_urls_to_invalidate_for_post( $post->ID ) ); }