The trouble with implementing SSR into a Laravel/Vue app
Tags: laravel, ssr, vue, nuxt
Published: 2019-04-06 21:56:44

> Originally posted at [michaelzanggl.com](https://michaelzanggl.com/articles/implementing-ssr-in-laravel-vue-app). Subscribe to [my newsletter](https://michaelzanggl.com/) to never miss out on new content.

TLDR: It is possible!

This is meant for those who want to integrate server-side rendering into an existing Laravel Vue application. If you are planning to create a new application, consider using [Nuxt.js](https://nuxtjs.org/) for a server-side rendered Vue application, with Laravel only serving as an API. If you want to go full Node.js, also consider using [Adonis.js](https://adonisjs.com/) instead of Laravel.

---

PHP does not understand JavaScript. So in order to achieve SSR, we have to spawn a Node.js instance, render our Vue app there and return the output to the client. 

Actually, there is already a composer dependency to achieve the task: [https://github.com/spatie/laravel-server-side-rendering](https://github.com/spatie/laravel-server-side-rendering). You can follow the steps there to implement it. This post will merely deal with the problems I ran into. I will also give some tips along the way.

I am using Laravel 5.5 and Node 8.11. Let's first go over some simple things.

---

### The blade view

The documentation is a little incomplete in the repository. I was confused with `app.$mount('#app')` since in the blade files of the readme, there was no element matching the selector `#app`.

Actually, the complete blade view according to the [examples](https://github.com/spatie/laravel-server-side-rendering-examples) would look like this

step 1. blade

```html
<html>
    <head>
        <script defer src="{{ mix('app-client.js') }}">
    </head>
    <body>
        {!! ssr('js/app-server.js')->fallback('<div id="app"></div>')->render() !!}
        <script defer src="{{ mix('app-client.js') }}">
    </body>
</html>
```

step 2. Vue

The root component also gets the id `app` assigned.

```vue
<template
  <div id="app">
    <!-- ... --!>
  </div>
</template>
```

---

So when SSR fails for some reason it would fall back to `<div id="app"></div>` and the client-side render would take care of everything.

Otherwise, after the app has been rendered on the server, the client side mount `app.$mount('#app')` would still work properly because of step `2`.

So this works, but having the same ID in multiple places is a little confusing. An easier solution would be to put `#app` in a wrapper class only in the blade view.

```html
<html>
    <head>
        <script defer src="{{ mix('app-client.js') }}">
    </head>
    <body>
        <div id="app">
            {!! ssr('js/app-server.js')->render() !!}
        </div>
    </body>
</html>
```

Yes, even with SSR in place, we still need a client-side mount to let Vue add event listeners, deal with all the reactivity and lifecycle hooks. One example would be the `mounted` method which will only be executed on the client. SSR will only execute what is needed for the initial render.

### What's my Node path in .env

In many cases, this might simply be

```env
NODE_PATH=node
```

You know, the same way you can globally access Node for things like `node some-file.js` or `node -v`.

### It doesn't perform SSR at all

By default, it is only activated for production. You can change this by first publishing the config file

```bash
php artisan vendor:publish --provider="Spatie\Ssr\SsrServiceProvider" --tag="config"
```

and then changing `'enabled' => env('APP_ENV') === 'production'` to `'enabled' => true`.

---

By now it should at least try to perform SSR. That means you are one step closer to finishing it. But now you might encounter problems like the following when Node tries to render the Vue app.

### async await is crashing


We are talking about integrating this into an existing application. So be sure to check whether your version of Laravel-mix is not too outdated. In my case, it was not even on 2.0. An update to `laravel-mix@2.1.14` was enough to fix these issues. You might want to consider updating even higher, but then be sure to check the release notes regarding the breaking changes.

### All props are undefined in child component

Another error that turned out to be a version error. An update from 2.5 to the latest `vue@2.6.10` fixed the error. In hindsight, the problem might have also occurred due to having different versions for Vue and `vue-server-renderer`.

### Window is not defined in `return window && document && document.all && !window.atob`

Now it becomes a little more interesting. You will encounter this error as soon as you have styles in a Vue component. The reason for this is because `vue-loder` uses `style-loader` under the hood, which is responsible for dynamically adding the styles to the head during runtime. But there is one problem, **it only works in the browser**. Since SSR is rendered in Node, there is neither `window` nor `document` available. So this got me thinking, how is Nuxt.js doing it? They are also using vue-loader after all. The solution is quite easy: Extract the styles before they are rendered by Node. This is actually a good practice to do so, so let's set it up in laravel-mix.

The only thing we have to do is add the following to the `options` in `webpack-mix.js`.

```javascript
mix.options({
    extractVueStyles: 'public/css/app.css',
})
```

All styles are being extracted into a single file `app.css`. If you have individual pages that use Vue and you would like to have a separate CSS file for each page, go with the following:

```javascript
mix.options({
    extractVueStyles: 'public/css/[name].css',
})
```

This would create the following files for example
```
> /public/css/js/login.css
> /public/css/js/registration.css
> /public/css/js/search.css
```

---

Apart from extracting Vue styles you also have to remove importing CSS files in JavaScript.
```javascript
import "some-library/some-style.css"
```

Instead, move these to some global style sheet. You might already have some merging technique in place for that. Again, it's a good practice to do so anyway ;)


### webpackJsonp is not defined

If this happens, you are likely extracting Node modules out into a vendor file. This has various performance benefits.

```javascript
mix.extract(['vue']);
```

Why is it crashing? If you look at the output of `manifest.js` it creates a global variable `webpackJsonp` and every JavaScript file will access this global variable to resolve the dependencies. Node.js, however, would not get `manifest.js` as well as `vendor.js` and therefore would be missing global variables and crash when trying to render your app.

One way to still make use of this feature is to have one webpack.mix.js file for only the server side scripts and another one for the client side scripts. [This comment](https://github.com/spatie/laravel-server-side-rendering/issues/26#issuecomment-452348326) shows how to do exactly that. Unfortunately, that is the only way I know of now how to keep extracting your dependencies.

### window / document / $ / localStorage / etc. is not defined

By now, your page might already render correctly, but there are a couple more traps to run into.

Imagine the following

```
data() {
    name: localStorage.getItem('name')
}
```

and... crash! 

This has nothing to do with the plugin or Laravel at this point, but simply something you have to be aware of when using SSR. window/document/localStorage and much more only exist on the client, not within Node.

There are two workarounds to fix the crash.

1. check the existence of variables before accessing these kinds of objects

```
data() {
    name: typeof localStorage !== 'undefined' ? localStorage.getItem('name') : null
}
```

2. Move the logic to the `mounted` method.

```
data() {
    name: null
},
mounted() {
    // client only
    this.name = localStorage.getItem('name')
}
```

In Nuxt.js you could also make use of the global `process.client` boolean to check whether the code is being executed on the server or on the client.

### Conclusion

Having to more or less manually set up SSR really makes one appreciate frameworks like Nuxt.js. But the good news is that SSR in Laravel is definitely possible. 

If there is any other problem, leave a comment below, but first think: How is Nuxt.js doing it? Because there is certainly a way to do it.

---

If this article helped you, I have a lot more tips on simplifying writing software [here](https://michaelzanggl.gumroad.com/l/intent-driven-development).