Hot Content Reload with SvelteKit & Contentful

Elad Rosenheim
5 min readApr 4, 2022

--

Photo by Mike van den Bos on Unsplash

One cool aspect of the Stackbit visual editor is how it communicates with your website’s dev server to apply all edits immediately on the page.

If you’re developing locally, you run the dev server yourself. When content editors use the cloud-based editor, we run your dev server within a container. In both cases, the technique is the same:

Whenever an edit is made, e.g. editing a field or adding a page, we instruct the dev server to let the client-side know it should refresh itself with updated content — without a full page refresh.

Doing a full refresh would be Really Bad (TM). It’ll take time, the whole page is gonna rebuild itself, and you’d lose visual context. That sucks.

Web developers are by now used to having the luxury of HMR (Hot Module Replacement/Reload) during local development. Whether you’re using React, Angular, SvelteKit, or basically anything that’s built on Webpack or Vite, we’ve come to expect that changing a piece of code would result in a near-immediate refresh.

This doesn’t work for content, though — and it’s especially tricky to do if this content isn’t stored locally (e.g. in Markdown files) but in an external CMS such as Contentful or Sanity. What we need is an HMR-like mechanism, but for content.

So, how does one implement Hot Content Reload?

Well, not simply, as the meme says;
or more exactly, it really depends on the framework you use.

For Next.js we already had an established method of doing that, whether you’re using our builtin Git-based CMS or connecting to a headless one. It used to be hidden away in Sourcebit (our data access package) but now available as a set of smallish standalone packages:

but I’m not here to dwell on how it works with Next.js; just read the README and the source to find out more — it’s fairly compact. Rather, let’s talk about SvelteKit.

HMR for content with SvelteKit

We’ve based our solution on a blog post by Daniel Imfeld. As is often the case, it’s very simple… after you’ve figured it out. So props to Daniel for figuring it out! 👏

We’ve adapted it for use with Contentful, so that on every change to content the current page will refresh. At a very high level, here’s how it works:

  1. Poll for Contentful updates, using their sync() API endpoint.
  2. When there are updates, send a signal.
  3. Using a custom Vite plugin, we capture that signal and send a custom message down the Websocket which Vite uses to communicate HMR updates to the client
  4. On the client side, receive that message and invalidate the current page, causing a clean, fast reload.

First, let’s create the Contentful client which will listen on any changes. Here’s /src/lib/contentfulClient.js:

You’ll notice that we skip the initial full sync, then set a periodic callback that will poll for subsequent delta updates. If there are no updates, the sync token remains the same and nothing happens.

When there are changes, we need to send a signal that a Vite plugin could capture. The easiest way to do it? touch a file which our plugin is gonna watch for: this is exactly what the /src/contentful/update file is used for. It may sound primitive — but it’s actually a very simple, performant way to communicate events.

Next, here’s the plugin itself:

Whenever the “signal” file has been updated, the handleHotUpdate callback is fired. Note that this code is agnostic to how the change was captured, or which system the change is coming from. All it knows is that the signal was raised.

Note that this code is running on the server side. How do we now cause the client-side to refresh itself?

Turns out we can use the existing Websocket connection which Vite establishes between the dev server and the client site. Usually, it’s used for transparently implementing HMR — but any custom message could be sent with it!

And here’s the last element: receiving this message on the client side and invalidating the page:

import.meta.hot is only available when running in development mode, and allows us to hook our own callbacks for specific messages.

The trickiest part here, actually? it’s invalidating the correct resource.
Note: we’re not calling invalidate(currentUrl) but rather $invalidate(`${currentUrl}.json`).

To explain why, let’s look at how our SvelteKit example renders content. In this specific example, we’re rendering blog posts stored in Contentful. Each post has a dedicated page rendered by /src/routes/[slug].svelte :

In a pretty standard affair for SvelteKit, the actual data for this page is fetched inside the load function, by calling an accompanying JSON resource: /src/routes/[slug].json.js (to learn more about data loading in SvelteKit, see here).

It is that JSON resource which actually calls Contentful for the post’s data:

One of the auto-magical things that SvelteKit does for us is tracking any fetch() call made within a load() function — so whenever any specific page rendered by [slug].svelte is rendered, SvelteKit keeps a note that this page depends on data from its little helper [slug].json.js.

When we call invalidate() we need to specify the resource which has changed and needs to be re-fetched — and since it’s the Contentful data that has changed, we tell SvelteKit: “hey, the JSON data for the current page? it’s changed now, so any page that fetched it needs to be reloaded”. When that happens, any page using that specific JSON payload would re-run its load() function, fetching the up-to-date data and rendering itself.

In other words: we’re not telling SvelteKit to refresh the current page; we’re telling it which data resource has actually changed, and it will know to hot-reload any page which uses it.

Pretty cool, huh? (though sometimes confusing… 🤷‍♀️)

Now, the loop is complete:
Our Contentful listener raised a “signal” by touching a file,
…which was captured by our Vite plugin on the server,
…which sent a message to the client in the Websocket,
…which invalidated the correct resource.

Photo by Sergio Capuzzimati on Unsplash

The Next Step: Adding Stackbit

Now that we have Hot Content Reload, we can use the same mechanism in any SvelteKit project to make it Stackbit-compatible. Now, how do I make Stackbit know exactly which content and content model I have, and where’s that content on the page?

Head over to our tutorial to learn more.
If you got this far, I think you’d like it.

--

--