Cache Drupal responses on the Nuxt server
Cache Drupal's JSON:API responses on the Nuxt server for as long as Drupal allows, and clear them from Drupal when content changes.
Before you start: the cache is held in the Nuxt server, so it needs
nuxt start or another server-rendered deployment. A site built with
nuxt generate has no server to cache in, and nuxt dev never caches.
Two caches
Druxt keeps Drupal's responses in two places:
| Cache | Where | Holds | Lasts |
|---|---|---|---|
| Vuex store | Each server render, and each browser tab | Every resource, collection, route, Views result and menu item a page fetched | Until the render ends or the tab closes |
| Process cache | The Nuxt server | The JSON:API index and menus, and with cache.content every other response |
As long as Drupal allows |
The store is the Vuex state every Druxt module shares. On the server it starts empty for each render. Its state is sent to the browser with the page, so the browser reuses what the server loaded. In the browser it lasts for the visit: moving to another page reads the entities it already holds instead of asking Drupal again. The Druxt store explains how its entries are keyed.
The process cache works below the store, on the server only. When a render needs a response the process cache holds, it answers before Drupal is asked. Drupal only sees that request again after a restart, a clear, or once the entry expires.
The process cache
In production, the Nuxt server keeps these responses in memory between page renders:
- The JSON:API index.
- Each menu, keyed on the query its settings build.
- With
cache.contenton, entities, collections, Views results and route lookups. - A regenerated schema, when
druxt.schema.refreshis on. See Schemas after a display change.
cache.content is off by default. With it on, a second visitor to a page
costs Drupal nothing until the page's lifetime runs out. Content changes
far more often than menus, so pair it with a
clear from Drupal. Without one, an edit reaches
anonymous visitors only once the page cache maximum age has passed. Only
successful responses are kept, so a page published after a visitor saw a
404 appears straight away.
With cache.content off, each server render fetches a page's entities
from Drupal, and the store answers repeats within that render.
How much it holds
cache.maxSize sets how much JSON each cache area keeps, 50 MB by
default, for each consumer. When an area is full, the least recently used
responses go first, and a response larger than the whole limit is not
kept. A full cache can take about three times that in server memory, so
size it against the memory each Nuxt process has.
How long it lasts
A response is kept for as long as its Cache-Control header allows.
Drupal's default page cache maximum age is 0, so out of the box every
response says no-cache and nothing is kept. The cache does nothing
until you turn it on.
Druxt never keeps a response when:
| The response or request | Example |
|---|---|
| Is marked private or uncacheable | Cache-Control: private, no-store or no-cache |
| Varies on a header the browser sends | Vary: Accept-Language, Origin or User-Agent |
| Comes from a signed-in request | An Authorization header, basic auth, or a session cookie |
A Vary on Cookie, X-Consumer-ID or Accept-Encoding does not stop
a response being kept.
Drupal's CORS settings decide whether it sends Vary: Origin. Allow one
origin, or * without credentials, or the cache keeps nothing. Several
origins, a pattern, or * with credentials add the header even to the
Nuxt server's own requests.
The content cache also leaves out any request that sets its own headers,
auth, params or base URL, and any request whose validateStatus refuses
a 200.
Signed-in requests
Signed-in requests never use the cache. Drupal marks every response to a
session cookie or a bearer token no-cache, and Druxt skips the cache
for any request that carries one. A session cookie is any cookie whose
name matches druxt.cache.sessionCookie, by default S?SESS[0-9a-f]+.
Druxt cannot see every kind of credential:
- A token an Axios interceptor adds is only seen once the response comes back, so that client's first request can still read a cached entry.
- An API key in a header of its own is not recognised.
- Other cookies share one entry, as they do in Drupal's page cache. If a
module varies responses on a consent or region cookie, add that cookie
to
sessionCookie, or turn the cache off.
Each Nuxt process has its own cache. Restarting the server empties it, and so does an idle environment waking up.
Turn it on
Set a page cache maximum age in Drupal, either at Configuration > Development > Performance or with Drush. Then rebuild the cache, so Drupal starts sending the new header:
drush config:set system.performance cache.page.max_age 300 -y
drush cache:rebuildCheck the header on an anonymous JSON:API request:
curl -sI https://cms.example.com/jsonapi | grep -i cache-control
# cache-control: max-age=300, publicA CDN or Varnish in front of Drupal caches JSON:API responses for the same time, and does not clear on publish unless it is set up to. Check what sits there before you raise the value.
These options set what is kept and how much. None of them keeps a response longer than Drupal allows:
// nuxt.config.js
export default {
druxt: {
cache: {
// Keep entities, collections, Views results and route lookups too.
// Pair it with a clear from Drupal.
content: true,
// Bytes of JSON kept per cache area, 50 MB by default. The least
// recently used responses go first.
maxSize: 20 * 1024 * 1024,
// Seconds an entry may live at most. Shortens Drupal's lifetime,
// never extends it; 0 keeps nothing.
ttl: 120,
// The session cookie's name, for a proxy that renames Drupal's.
sessionCookie: 'MY_SESSION[0-9a-f]+',
},
},
}To turn the cache off, set druxt: { cache: false }.
Clear it from Drupal
Give the cache a secret, and the Nuxt server accepts
POST /_druxt/cache/clear, which empties the whole cache. The endpoint is
server middleware, so list Druxt, or druxt-site, under modules. Nuxt
skips buildModules under nuxt start, and the endpoint then answers
404:
// nuxt.config.js
export default {
// modules, not buildModules: the clear endpoint registers under nuxt start.
modules: ['druxt-site'],
druxt: {
cache: {
secret: process.env.DRUXT_CACHE_SECRET,
},
},
}Send the secret in an X-Druxt-Secret header, never in the URL:
| Status | Meaning |
|---|---|
204 |
The cache is empty. |
401 |
The secret is wrong or missing. |
405 |
The request was not a POST. |
A response still in flight when the cache clears is not stored.
Clear on save with Purge
The Purge module can send that
request each time content is saved. Enable purge, purge_purger_http,
purge_queuer_coretags and a processor such as
purge_processor_lateruntime:
purge_queuer_coretagsqueues the cache tags a save invalidates.- The bundled HTTP purger from
purge_purger_httpsends them as onePOST, with the secret in its header, however many tags there are. purge_processor_lateruntimesends it after the page response, so an editor never waits for it.
Install the modules and add the purger:
composer require drupal/purge drupal/purge_purger_http
drush pm:install purge purge_purger_http purge_queuer_coretags purge_processor_lateruntime purge_drush -y
drush p:purger-add httpbundledThe purger is saved as config, purge_purger_http.settings.<id>, where
<id> is what p:purger-add prints. Export it and point it at the Nuxt
server by the host and port Drupal reaches it on. The example below uses
a nuxt service on the same internal network:
# purge_purger_http.settings.<id>.yml
name: 'Druxt server cache'
invalidationtype: tag
hostname: nuxt
port: 3000
path: /_druxt/cache/clear
request_method: POST
scheme: http
headers:
- field: X-Druxt-Secret
value: ''
body: ''
timeout: 3.0
connect_timeout: 1.0
max_requests: 100
http_errors: trueLeave the secret empty in the exported config, and set it in
settings.php from the environment. Use the id from the exported file's
name:
// settings.php
$purger = 'purge_purger_http.settings.705a5122da';
$config[$purger]['headers'][0]['value'] = getenv('DRUXT_CACHE_SECRET');Keep http_errors on. With it, a 401 fails the queue item, and Purge
tries again with the next batch once the secret is right.
Each Nuxt process has its own cache, so each needs its own clear. A site that runs several processes adds a purger for each, with its own hostname.
Clear it in code
Each of these runs in the process that calls it. In the browser, that is the visitor's own store:
| Call | Clears |
|---|---|
this.$store.dispatch('druxt/clearCache') |
The client and the whole store. On the server it also empties the process cache. |
this.$druxt.clearCache() |
The client's JSON:API index, and the process cache when it runs on the server. |
this.$store.commit('druxt/flushResource', { type, id }) |
Stored resources. Pass {} for all of them. |
this.$store.commit('druxt/flushCollection', { type, query }) |
Stored collections. Pass {} for all of them. |
this.$store.commit('druxt/views/flushResults', { viewId, displayId }) |
Stored Views results. Pass {} for all of them. |
this.$store.commit('druxtMenu/flushEntities', {}) |
Stored menu items. |
this.$store.commit('druxtRouter/flushRoutes', { path: '/about' }) |
Stored routes. Pass {} for all of them. |
A dispatch with bypassCache, including a Views query, skips the
server cache and stores the fresh response.
Code in the browser cannot reach the server's cache. Use the clear endpoint for that.
Schemas after a display change
Druxt builds its schemas with the site, so a display changed in Drupal
does not show up after a cache clear. With druxt.schema.refresh on,
the Nuxt server regenerates a schema after each clear, the first time a
page needs it, and keeps the built one as the fallback. Under nuxt dev
every request regenerates. A static site cannot refresh. See
Display schemas for its limits.
druxt.schema.refresh is off by default:
// nuxt.config.js
export default {
druxt: {
schema: { refresh: true },
},
}