Hot reloading content in Craft CMS's Live Preview
When I saw Jack Sleight's post about hot reloading in Statamic's live preview I was pretty blown away. Live preview is already incredibly useful in a CMS, but inserting content changes without a refresh would be a huge upgrade to that experience.
A typical live preview experience works like this:
- A companion
<iframe>
is loaded, showing you the draft of your current entry - When a content change is made in the CMS the
<iframe>
is refreshed, showing the latest changes
Statamic 3.4.8 introduced the ability to disable the auto-refresh in favor of sending a postMessage()
event via JavaScript. That means, the live preview experience changes a bit:
- A companion
<iframe>
is loaded, showing you the draft of your current entry - When a content change is made the CMS doesn't refresh the
<iframe>
, but instead broadcasts apostMessage()
event your site can listen to and act upon.
Once I saw this, I wanted to see if I could acheive a similar experience in Craft.
Setting it up in Craft
- We need to disable auto-refresh on any Preview Targets within our entry types. We no longer want the
<iframe>
to get a full-page reload. - Now need to register some JavaScript in the control panel that will fire our
postMessage()
event when the content has changed. I'm not going to walk through this process, but this is where you would likely create a Craft CMS module and, on control panel requests, insert thepostMessage()
JavaScript code:
// Use Garnish to hook into Craft's beforeUpdateIframe's event
Garnish.on(Craft.Preview, 'beforeUpdateIframe', function (event) {
if (!event.refresh) {
// Once the content has been changed, fire a postMessage event with a live preview key,
// but scope the broadcast to our site for security purposes
event.target.$iframe[0].contentWindow.postMessage(
'entry:live-preview:updated',
event.previewTarget.url
)
}
})
- In your front-end template—likely your
_layout.twig
file—we need to add the following to act upon the broadcastedpostMessage()
{# Execute this JavaScript in preview mode only #}
{% if craft.app.request.isPreview %}
{# Include Alpine.js and the Morph plugin #}
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/[email protected]/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
<script>
{# Listen for postMessage() calls #}
window.addEventListener('message', async (event) => {
{# Ignore messages that don't use our live preview key #}
if (event.data !== 'entry:live-preview:updated') {
return
}
{# Grab the URL we need to process from the event, send it to a fetch() call, and then parse the text #}
const text = await fetch(event.target.location.href).then((res) => res.text())
{# Process the text as HTML #}
const updated = new DOMParser().parseFromString(text, 'text/html')
{# Use Alpine.js's morph plugin to diff the existing DOM to the new HTML and update the page #}
Alpine.morph(document.body, updated.body)
})
</script>
{% endif %}
And now, once you've opened your Live Preview panel in Craft CMS you'll be able to make changes without incurring a full-page refresh!
After a bit of setup it's an impressive experience! And, like hot module reloading, it's hard to go back once you've used it.