I'm building a Vue.js site with Craft as the backend, which works pretty well so far. The only thing I can't get to work yet is live preview.
Vue-Router relies on the window.location which is flawed in live preview (https://example.com/admin/entries/pages/1-homepage instead of https://example.com/homepage). Is there any way to get this right?

Update: A new issue popped up. About to test things, I noticed the content does not change because - obviously - I pull it from the Element API, which pulls it from the database. I'll update my answer below when I've found a way around this.

2 Answers 2


Craft 3.2 introduces Headless Preview which works a treat with SPA's. Essentially Craft exposes tokens which you can forward on to subsequent Element API requests and Craft will do the rest. The interesting thing to note with this release is that Live Preview isn't actually "live" rather draft entries that are saved and accessible by tokens generated to identify the entry version.

Your example outlined above is adequate in configuring Vue Router for these previews to work. But we also need to pass on these preview tokens to Element API. These tokens allow Craft to do away with obfuscating URL's loaded in the Live Preview iframe and now load the actual entry URL as you would access it on the actual website.

For example, previously Live Preview would load /admin/entries/282-project-slug in an iframe, it now loads the entry target URL /projects/project-slug?x-craft-preview=XXX&token=XXX. Fantastic!

Detecting Preview Mode

Live Preview loads the entry URL in an iframe with tokens exposed via a query string. These are x-craft-preview and token. We can extract these from the URL either client-side or server-side. The choice is entirely up to you but we just need to grant easy access to these tokens.

Client-side detection example:

let hashes = window.location.search.slice(window.location.search.indexOf('?') + 1).split('&')
let params = {}
hashes.map(hash => {
  let [key, val] = hash.split('=')
  params[key] = decodeURIComponent(val)

// x-craft-preview = params['x-craft-preview']
// token = params['token']

Server-side detection example (Twig):

{% if craft.app.request.isPreview %}
   token = {{ craft.app.request.getQueryParam('token') }}
   x-craft-preview = {{ craft.app.request.getQueryParam('x-craft-preview') }}
{% endif %}

It's worth noting that Craft 3.2 also deprecates isLivePreview in favour of isPreview since Live Preview is no longer programatically live rather drafts saved on-the-go and tokenized.

Configuring Vue

For demonstration purposes we will declare the following variables and assume we now have access to the tokens.

const craftPreview = // value of 'x-craft-preview',
const craftToken = // value of 'token'

Set the router mode to mode: 'abstract', for example:

const router = new VueRouter({
  mode: craftPreview ? 'abstract' : 'history',

The router will resolve the preview URL so there's no need to replace the path. If you're still having trouble with this then you can try setting it to see if it resolves your problem...

router.replace({ path: window.location.pathname })

Below is an example of forwarding these params with Axios as our HTTP client.

axios.get('/api/v1/project.json', {
  params: {
    slug: 'project-slug',
    token: craftToken,
    'x-craft-preview': craftPreview

Alternatively, you could utilise Axios interceptors and configure params before each request.

  (config) => {
    // Forward Craft Preview tokens to API
    if (craftPreview) {
      const params = {
        token: craftToken,
        'x-craft-preview': craftPreview
      if (!config.params) {
        config.params = params
      else {
        Object.keys(params).map(key => config.params[key] = params[key])
    return config
  (err) => {
    return Promise.reject(err)

Hopefully this steers people in the right direction. As always, huge shout out to the Craft team for making this possible.

  • 2
    I wrote the Headless Preview in Craft CMS article that goes a bit deeper under the hood in terms of how it works in Craft-land. I linked to your post here. Commented Aug 13, 2019 at 4:33

As of Craft 3.2 there is native support for headless previews. Please see Cue's extensive answer for a detailed explanation.

Original answer

I managed to do this, although I'm still not quite happy with it.
First part of the solution is figuring out whether this is a live preview. To do so, I decided to append data attributes to the HTML element. You're free to use global variables or whatever else suits you, though.

{% if craft.request.isLivePreview %}
  <script type="text/javascript">
    window.document.documentElement.dataset.livePreview = true;
    window.document.documentElement.dataset.path        = '{{ '/' ~ craft.request.path }}';
{% endif %}

Next up, getting Vue-Router to not use window.location for route matching and navigating straight to the desired URI:

const isLivePreview  = window.document.documentElement.dataset.livePreview,
      actualPath     = window.document.documentElement.dataset.path;

if (isLivePreview) {
  routerConfig.mode = 'abstract';

const router = new VueRouter(routerConfig);

if (isLivePreview) {
  router.replace({ path: actualPath });

This makes live preview work like a charm so far. What I don't like, though, is a) the doubled if (isLivePreview) and b) the need to check this for every visitor and therefore 99% of all requests.
If anyone can come up with something better it will be greatly appreciated. I'm leaving the question open for a few days.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.