Cache-busting page-data.json files in Gatsby

Viewed 4836

I have a gatsby generated website on which I have replaced the contents of the homepage.

Unfortunately the previous version was serving up /page-data/index/page-data.json with the incorrect cache-control headers, resulting in /page-data/index/page-data.json being cached on client browsers (and stale data being shown unless force-refreshed). I have also discovered that page-data.json files are not hashed (see https://github.com/gatsbyjs/gatsby/issues/15080).

I've updated the cache-control headers so that versions from now on will not be cached but this does not help with clients that have the cached version now.

What can I do to force clients to request the latest version of this file?

3 Answers

I got there in the end... This is in my gatsby-node.js

const hash = md5(`${new Date().getTime()}`)

const addPageDataVersion = async file => {
  const stats = await util.promisify(fs.stat)(file)
  if (stats.isFile()) {
    console.log(`Adding version to page-data.json in ${file}..`)
    let content = await util.promisify(fs.readFile)(file, 'utf8')
    const result = content.replace(
      /page-data.json(\?v=[a-f0-9]{32})?/g,
      `page-data.json?v=${hash}`
    )
    await util.promisify(fs.writeFile)(file, result, 'utf8')
  }
}

exports.onPostBootstrap = async () => {
  const loader = path.join(__dirname, 'node_modules/gatsby/cache-dir/loader.js')
  await addPageDataVersion(loader)
}

exports.onPostBuild = async () => {
  const publicPath = path.join(__dirname, 'public')
  const htmlAndJSFiles = glob.sync(`${publicPath}/**/*.{html,js}`)
  for (let file of htmlAndJSFiles) {
    await addPageDataVersion(file)
  }
}

Check out this tutorial, this is the solution I've been using.

https://examsworld.co.in/programming/javascript/how-to-cache-bust-a-react-app/

It's basically a wrapper component that checks to see if the browser's cached version matches the build's version number in package.json. If it doesn't, it clears the cache and reloads the page.

This is how I'm using it.

gatsby-browser.js

    export const wrapRootElement = ({ element }) => (
     <CacheBuster>
        {({ loading, isLatestVersion, refreshCacheAndReload }) => {
          if (loading) return null
          if (!loading && !isLatestVersion) {
            // You can decide how and when you want to force reload
            refreshCacheAndReload()
          }
          return <AppProvider>{element}</AppProvider>
        }}
      </CacheBuster>
    )

CacheBuster.js

import React from 'react'
import packageJson from '../../package.json'

global.appVersion = packageJson.version

// version from response - first param, local version second param
const semverGreaterThan = (versionA, versionB) => {
  const versionsA = versionA.split(/\./g)

  const versionsB = versionB.split(/\./g)
  while (versionsA.length || versionsB.length) {
    const a = Number(versionsA.shift())

    const b = Number(versionsB.shift())
    // eslint-disable-next-line no-continue
    if (a === b) continue
    // eslint-disable-next-line no-restricted-globals
    return a > b || isNaN(b)
  }
  return false
}

class CacheBuster extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      loading: true,
      isLatestVersion: false,
      refreshCacheAndReload: () => {
        console.info('Clearing cache and hard reloading...')
        if (caches) {
          // Service worker cache should be cleared with caches.delete()
          caches.keys().then(function(names) {
            for (const name of names) caches.delete(name)
          })
        }

        // delete browser cache and hard reload
        window.location.reload(true)
      },
    }
  }

  componentDidMount() {
    fetch('/meta.json')
      .then(response => response.json())
      .then(meta => {
        const latestVersion = meta.version
        const currentVersion = global.appVersion

        const shouldForceRefresh = semverGreaterThan(
          latestVersion,
          currentVersion
        )
        if (shouldForceRefresh) {
          console.info(
            `We have a new version - ${latestVersion}. Should force refresh`
          )
          this.setState({ loading: false, isLatestVersion: false })
        } else {
          console.info(
            `You already have the latest version - ${latestVersion}. No cache refresh needed.`
          )
          this.setState({ loading: false, isLatestVersion: true })
        }
      })
  }

  render() {
    const { loading, isLatestVersion, refreshCacheAndReload } = this.state
    const { children } = this.props
    return children({ loading, isLatestVersion, refreshCacheAndReload })
  }
}

export default CacheBuster

generate-build-version.js

const fs = require('fs')
const packageJson = require('./package.json')

const appVersion = packageJson.version

const jsonData = {
  version: appVersion,
}

const jsonContent = JSON.stringify(jsonData)

fs.writeFile('./static/meta.json', jsonContent, 'utf8', function(err) {
  if (err) {
    console.log('An error occured while writing JSON Object to meta.json')
    return console.log(err)
  }

  console.log('meta.json file has been saved with latest version number')
})

and in your package.json add these scripts

 "generate-build-version": "node generate-build-version",
 "prebuild": "npm run generate-build-version"

Outside of going to each client browser individually and clearing their cache there isn't any other means of invalidating all of your client's caches. If your webpage is behind a CDN you can control, you may be able to force invalidation at the CDN-level so new clients will always be routed to the up to date webpage even if the CDN had a pre-existing, outdated copy cached.

Related