Cache Revalidation
Translations
./lib/get-i18n.js
const data = await drupal.getTranslations({ withCache: true, cacheKey: "i18n config_translation_info_plugins locale",})
When the Next.js app initially retrieves translations from Drupal backend_url/_translations
, it stores them in the Redis cache
using the key specified as i18n config_translation_info_plugins locale
with no cache expiration time.
By following the Internationalization section, you have the flexibility to add an unlimited number of keywords that require translation.
What actually occurs when we translate a new keyword (in Drupal side), considering that Next.js app stores translations without an expiration time?
This API endpoint, /api/cache/clear
, is precisely what Drupal utilizes to invalidate the translation cache whenever
new keywords are added through the Back-Office.
./profiles/contrib/vactory_starter_kit/modules/vactory_decoupled/vactory_decoupled.module
/** * Implements hook_form_alter(). */function vactory_decoupled_form_alter(&$form, FormStateInterface $form_state, $form_id) { if ($form_id === 'locale_translate_edit_form') { $form['#submit'][] = 'clear_next_cache_translation'; } ...}
/** * Clear frontend cache. */function clear_next_cache_translation(&$form, FormStateInterface $form_state) { $query = [ 'invalidate' => 'translation', ]; clear_next_cache($query);}
Menu
./lib/get-menus.js
const fetchMenu = async ({ name, locale, auth }) => { const uid = auth?.auth?.user?.id || 0 const cacheKey = `config:system.menu.${name} user:${uid} language:${locale}` let options = { withCache: true, cacheKey: cacheKey, }
if (auth?.auth?.user?.id > 0) { options["withAuth"] = () => `Bearer ${auth?.auth.accessToken}` options["headers"] = { "X-Auth-Provider": auth?.auth.provider, } } const data = await drupal.getMenu(name, locale, options) return data}
When the Next.js app initially retrieves menus from Drupal backend_url/_menus?menu_name=main
, it stores them in
the Redis cache using the key specified as config:system.menu.${name} user:${uid} language:${locale}
with no cache expiration time.
- name: Menu name
- locale: Current language
- uid: User id
But, what occurs when we add, update or delete items from/to one of our menus?
Similar to the translation process, Drupal also use the endpoint /api/cache/clear to invalidate the front cache.
./profiles/contrib/vactory_starter_kit/modules/vactory_decoupled/vactory_decoupled.module
/** * Implements hook_entity_update(). */function vactory_decoupled_entity_update(EntityInterface $entity) { static $loaded;
if ($entity instanceof Node && !$entity->isNew()) { $query = get_node_info($entity); clear_next_cache($query); }
if ($entity instanceof MenuItemExtrasMenuLinkContent && !isset($loaded)) { $loaded = 1; clear_next_cache([ 'menu' => $entity->bundle(), 'invalidate' => 'menu', ]); } else { if ($entity instanceof Menu) { clear_next_cache([ 'menu' => $entity->id(), 'invalidate' => 'menu', ]); } }}
/** * Implements hook_entity_delete(). */function vactory_decoupled_entity_delete(EntityInterface $entity) { if ($entity instanceof MenuItemExtrasMenuLinkContent) { $query = [ 'menu' => $entity->bundle(), 'invalidate' => 'menu', ]; clear_next_cache($query); }}
Router
./pages/index.jsx
const cacheKey = `router:${joinedSlug} language:${locale} ${uid}`const cached = await redis.get(cacheKey)if (cached) { router = JSON.parse(cached)} else { routerResponse = await drupal.getRoute(joinedSlug, locale, routerOptions) router = await routerResponse.json() await redis.set( cacheKey, JSON.stringify(router), "EX", process.env.REDIS_ROUTER_EXPIRE )}
When a user requests a page, Next.js sends a request to Drupal to get the necessary route information for the
requested page. Once the information is received, the Next.js app stores it in the Redis cache. The cache entry
is identified using a key that follows the format router:${joinedSlug} language:${locale} ${uid}
.
Additionally, the cache entry is configured with an expiration time based on the value set in the environment
variable REDIS_ROUTER_EXPIRE
.
- joinedSlug: Current route slug url
- locale: Current language
- uid: User id
Node
./pages/index.jsx
const cacheKey = `node:${router.entity.id} bundle:${router.entity.bundle} language:${locale} user:${uid} slug:${joinedSlugKey}`const cached = await redis.get(cacheKey)if (cached) { node = JSON.parse(cached)} else { node = await drupal.getNode(router, nodeParams, locale, joinedSlug, nodeOptions) if (!node.internal_extra.cache_exclude) { await redis.set( cacheKey, JSON.stringify(node), "EX", process.env.REDIS_NODE_EXPIRE ) }}
After storing the route information, Next.js needs to send an additional request to Drupal in order to retrieve
the necessary data related to the specific node being accessed. Once the information is received, the Next.js
app stores it in the Redis cache. The cache entry is identified using a key that follows the format
node:${router.entity.id} bundle:${router.entity.bundle} language:${locale} user:${uid} slug:${joinedSlugKey}
.
Additionally, the cache entry is configured with an expiration time based on the value set in the environment
variable REDIS_NODE_EXPIRE
.
- router.entity.id: Node id
- router.entity.bundle: Node bundle
- locale: Current langague
- uid: User id
- joinedSlugKey: Current route slug url
But, what occurs when we add, update or delete the node?
Similar to the translation, Menus process, Drupal also use the endpoint /api/cache/clear
to invalidate the front
cache.
./profiles/contrib/vactory_starter_kit/modules/vactory_decoupled/vactory_decoupled.module
/** * Implements hook_entity_update(). */function vactory_decoupled_entity_update(EntityInterface $entity) { static $loaded;
if ($entity instanceof Node && !$entity->isNew()) { $query = get_node_info($entity); clear_next_cache($query); }}
/** * Implements hook_entity_predelete(). */function vactory_decoupled_entity_predelete(EntityInterface $entity) { if ($entity instanceof Node) { $query = get_node_info($entity); clear_next_cache($query); }}
Advanced uses cases
Front Cache Revalidation Module
In the scenario where we need to invalidate the cache of specific pages based on changes in another entity, what options are available to address this situation?
To handle such situations, the Vactory Starter Kit offers a custom module called vactory_decoupled_revalidator
.
This module provides a solution by allowing the creation of rules that enable on-demand revalidation.
With the vactory_decoupled_revalidator
module, you can define a set of rules that specify the conditions for cache
invalidation, thereby providing a flexible mechanism to trigger revalidation when necessary.
Configuration
Visit
/admin/structure/revalidator-entity-type
Click Configure entity
Configure the plugin
And save changes.
Configure new invalidation redis cache rule
Edit : ./pages/api/cache/clear.js
./pages/api/cache/clear.js
import { redis, isHttpMethod } from "@vactorynext/core/server"
export default async function handler(req, res) { const secret = req.headers["x-cache-secret"] || "" isHttpMethod(req, res, ["GET"])
if (process.env.CACHE_SECRET === undefined) { res.status(500).json({ status: "CACHE_SECRET environment variable not specified!" }) return }
if (process.env.CACHE_SECRET !== secret) { res.status(500).json({ status: "secret key doesn't match" }) return }
if (req?.query?.invalidate) { console.log( `[Cache]: received command to clear ${req.query.invalidate} cache`, req.query )
const pipeline = redis.pipeline()
if (req.query.invalidate === "node" && req?.query?.id) { ... }
... if (req.query.invalidate === "custom_key") { // Your custom code here } } else { console.log(`[Cache]: received command to clear all caches`) await redis.flushall() }
res.status(200).json({ status: "Cache cleared" })}