stoffel.io

Excluding Scripts and Parts of Templates in Statamic Live Preview

Today I found a solution to a bug one of my clients was experiencing with their new Statamic site. Others might stumble over this as well at some point, so here's how I solved it using a custom template tag.

The issue the client had: when using the live preview mode, the preview would render correctly at first, but as soon as they made a change that triggered a refresh, all they saw was a blank page instead of their preview.

All the console gave me was a fairly generic error message: Uncaught (in promise) TypeError: document.head is null

Tracking down the culprit

Looking at the LivePreview.vue component, which triggered the error, didn't get me any ideas, so instead I tried to find a different angle. This only happened on a single site, and only on the production instance, while the development site with the exact same files worked fine.

Looking at the code I noticed the only major difference being a consent management tool (CookieBot) being included only when the environment was set to production. And sure enough, manually removing that script tag fixed the issue in live preview.

/resources/views/layout.antlers.html
{{ if (environment == 'production') }} <script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="4643686c-10ea-4c32-b5b4-6aa5c153a2da" data-blockingmode="auto" type="text/javascript"></script> {{ /if }}
The code snippet breaking our live preview in production but not development.

So the obvious solution would be to not pull in the script whenever the page is loaded as a preview instead of directly. Sadly there is no is_live_preview template tag or anything along those lines.

The only difference I found in the available data was a variable live_preview, which is only defined in the case that the site is previewed. In that case it's an array which can hold additional values, but in our case it's just empty.

Sadly we can't just check for this variable since both a nonexistent variable and an empty array are considered false in php, and Antlers doesn't support is_set() or is_array().

As described in the documentation, you can create additional inputs to be shown in the live preview mode, whose values will then appear in the live_preview array. This however entails creating and pulling in a VueJS component, which seems a bit overkill for our simple problem.

If you want to show and hide different elements in live preview only on some pages, this is definitely the approach you should take, as it gives you a lot of flexibility. But for our case all I wanted was a simple solution that I can easily include into any project that might require it.

Building a Portable Solution

For our use case I only needed a simple Antlers tag that shows or hides code depending on how the page is accessed. As always there is an excellent documentation on how to create new tags.

So I decided to simply add my own tag, which can be easily done in the terminal by typing php please make:Tag IsLivePreview.

The resulting class can be found in /app/Tags:

/app/Tags/IsLivePreview.php
<?php namespace App\Tags; use Statamic\Tags\Tags; class IsLivePreview extends Tags { /** * The {{ is_live_preview }} tag. * * @return string|array */ public function index() { // } /** * The {{ is_live_preview:example }} tag. * * @return string|array */ public function example() { // } }
The boilerplate code for our freshly created tag.

We only need a super simple solution, so the example() function can go. Now whenever we add {{ is_live_preview }} to any of our Antlers views, it will print whatever the index() function returns. Feel free to try it out by simply returning a string at first.

Finding a reliable check

As discussed above we do have the live_preview variable which is only available in live preview mode. So we could in theory check for that in our tag and return true if it's set or false if it's not.

But it's not meant to be used in that way and there is no guarantee that the variable will always behave the way it does now. Luckily Statamic provides a much better way of identifying live preview requests: a request header.

Whenever a page is requested as part of a live preview, the HTTP headers include an entry x-statamic-live-preview, which isn't set on any other requests.

Checking the Headers in our Custom Tag

As usual, Laravel provides the functions and documentation you need to solve this issue in a single line. By using the request() helper, we can access the HTTP headers and check if a specific one is set or not using headers->has().

/app/Tags/IsLivePreview.php (excerpt)
public function index() { return request()->headers->has('x-statamic-live-preview'); }
The function checking for our header and returning the result

With this simple function call our index() method returns true only if the header is set, meaning we can use it in conditionals in our templates.

Using the Tag in a Template

Now that we have everything set up, it's time to actually use our tag. Since it returns a boolean, we can simply use if and else to show or hide parts of our template. The only gotcha is that our tag has to be wrapped ion {} for Antlers to handle the returned value correctly.

example.antlers.html
{{ if {is_live_preview} }} this will be shown only in live preview {{ else }} this will be hidden in live preview {{ /if }}
Using our custom tag as a conditional in an Antlers template.

Using this tag I was able to fix the live preview bug on my clients' page in seconds. Here's what my code looks like now:

/resourcs/views/layout.antlers.html (excerpt)
{{ if (environment == 'production') && !{is_live_preview} }} <script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="4643686c-10ea-4c32-b5b4-6aa5c153a2da" data-blockingmode="auto" type="text/javascript"></script> {{ /if }}
The conditional now checks both the environment and if it's a live preview request

But wait: there's an addon!

If you're facing the same issue you don't need to copy and paste what I did above. I turned the tag into a Statamic addon and made it available for free, so you can simply install it via composer and use the same tag.

More posts: