Full page caching can be a useful technique for improving the performance of your WooCommerce website, but it can be challenging to implement it in a way that works for different user roles. This is because different user roles may have different permissions and access to different content, and the cached version of a page may not accurately reflect the content that is visible to a particular user.
To cache WooCommerce product pages with Varnish, you will need to install and configure Varnish on your server and configure it to cache your WooCommerce product pages. This typically involves creating a Varnish configuration file that defines the rules for caching your pages.
Woocommerce product pages can be fully cached by Varnish with the following config:
if (req.method == "GET" && req.url ~ "^/(shop|product)" && req.url !~ "\?add-to-cart=") {
unset req.http.cookie;
}
However, when using plugins, such as woocommerce-wholesale-pricing
, a different price must be shown to different WordPress user roles.
To have a full page in such a case, we need to vary page cache based on the logged-in user’s roles.
This requires adding some WordPress code to emit a cookie with user’s roles, as well as Varnish configuration changes, that create cache variations.
WordPress code changes
Our PHP code is pretty simple and can be added to your theme’s functions.php
:
/*
add_action('wp_login', function ( $user_login, $user ) {
if ( ! headers_sent() ) {
$roles = implode(',', $user->roles);
setcookie( 'user_roles', $roles, time() + (30 * DAY_IN_SECONDS), COOKIE_PATH, COOKIE_DOMAIN );
}
}, 10, 2);
*/
add_action('set_logged_in_cookie', function ( $logged_in_cookie, $expire, $expiration, $user_id ) {
$user = get_user_by( 'id', $user_id );
$roles = implode( ',', $user->roles );
setcookie( 'user_roles', $roles, $expire, COOKIEPATH, COOKIE_DOMAIN, true, true );
}, 10, 4);
We also need to clear the user roles cookie upon logout:
add_action( 'wp_logout', function ( $user_id ) {
if ( ! headers_sent() ) {
setcookie( 'user_roles', time() - (30 * DAY_IN_SECONDS) );
}
} );
It plugs into wp_login
hook which happens right after successful user authentication. It then takes the user’s roles, and instructs the browser to create user_roles
cookie.
The value of the cookie is a comma-delimited list of the user’s roles, e.g. vat
, or administrator
.
Now, any time after authentication the user browses the website, its browser sends the user_roles
cookie and we can teach Varnish to vary cache based on its value.
Varnish VCL code
The vcl_recv
routine
sub vcl_recv {
if (req.method == "GET" && req.url ~ "^/shop" && req.url !~ "\?add-to-cart=") {
# set special header for varying cache by roles
if (req.http.cookie ~ "user_roles=") {
set req.http.x-user-roles = regsub(req.http.cookie, "^.*?user_roles=([^;]+);*.*$", "\1");
} else {
set req.http.x-user-roles = "guest";
}
# we must pass through cookies for role-specific content variation
# save the cookies before the built-in vcl_recv and restore later for backend to see while caching
# see: https://info.varnish-software.com/blog/yet-another-post-on-caching-vs-cookies
set req.http.Cookie-Backup = req.http.Cookie;
unset req.http.Cookie;
}
}
In the sub vcl_recv
routine, we specify product pages via regular expression ~/shop
. Take note that we should only act on GET
requests, because upon POST
requests some versions of Woocomerce
have add-to-cart functionality.
If user_roles
cookie is present in the Cookie:
header, we extract its value and create X-User-Roles
header.
Next up, we backup the whole Cookie
header in order to be able to cache the request. This is important due to “coding with built-in VCL in mind” pattern.
The vcl_backend_response
routine
sub vcl_backend_response {
# ensure Varnish varies objects by X-User-Roles header value
# this ensures that upon change, a PURGE will evict all variations
if (bereq.http.x-user-roles) {
if (!beresp.http.Vary) { # no Vary at all
set beresp.http.Vary = "x-user-roles";
} elseif (beresp.http.Vary !~ "x-user-roles") { # add to existing Vary
set beresp.http.Vary = beresp.http.Vary + ", x-user-roles";
}
}
# make sure that shop pages are cacheable no matter if we accessed with a session
# or a bad plugin that emits Set-Cookie for no good reason
if (bereq.method == "GET" && bereq.url ~ "^/shop" && bereq.url !~ "\?add-to-cart=") {
unset beresp.http.Set-Cookie;
unset beresp.http.Cache-Control;
# while doing Set-Cookie, PHP sends anti-caching header in Cache-Control, so we must explicitly set Varnish TTL to keep caching in Varnish
set beresp.ttl = 2w;
}
}
In vcl_backend_response
, the first block of code ensures that Varnish treats different values of the X-User-Roles
header as variations of the same object.
This is crucial to allow us easily PURGE
all variations of the same product page, that look differently (e.g. has different prices) for each WordPress role.
In the next block of VCL, we ensure the cacheability of the product page even when we’re caching it for different user roles. (note that in our case, Varnish sees request with Cookie:
but still does caching).
The to vcl_hash
routine
sub vcl_hash {
if (req.http.Cookie-Backup) {
# restore the cookies before the lookup if any
set req.http.Cookie = req.http.Cookie-Backup;
unset req.http.Cookie-Backup;
}
}
Our vcl_hash
routine restores the backed up Cookie:
header so that the backend can see it and generate appropriate pages with the correct prices for each user role.
The vcl_deliver
routine
sub vcl_deliver {
set resp.http.x-user-roles = req.http.x-user-roles;
}
Finally, in the vcl_deliver
routine, we emit X-User-Roles
header, for debugging and making sure our value extraction from user_roles
cookie is correct.
Cache invalidation
When a product is changed, its cache should be invalidated. We throw in the supplementary plugin to send PURGE
request for us.
Thanks to the logic we added to our VCL, those purge request will clear up caches for all variations at once.
The plugin can be installed and configured via WP-CLI:
wp plugin install varnish-http-purge --activate --force
wp option add vhp_varnish_ip 127.0.0.1