DEV Community

loading...

The trouble with implementing SSR into a Laravel/Vue app

Michael Z
Software writer
Originally published at michaelzanggl.com Updated on ・5 min read

Originally posted at michaelzanggl.com. Subscribe to my newsletter 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 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 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. 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 would look like this

step 1. blade

<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>
Enter fullscreen mode Exit fullscreen mode

step 2. Vue

The root component also gets the id app assigned.

<template
  <div id="app">
    <!-- ... --!>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

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>
    <head>
        <script defer src="{{ mix('app-client.js') }}">
    </head>
    <body>
        <div id="app">
            {!! ssr('js/app-server.js')->render() !!}
        </div>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

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

NODE_PATH=node
Enter fullscreen mode Exit fullscreen mode

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

php artisan vendor:publish --provider="Spatie\Ssr\SsrServiceProvider" --tag="config"
Enter fullscreen mode Exit fullscreen mode

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.

mix.options({
    extractVueStyles: 'public/css/app.css',
})
Enter fullscreen mode Exit fullscreen mode

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:

mix.options({
    extractVueStyles: 'public/css/[name].css',
})
Enter fullscreen mode Exit fullscreen mode

This would create the following files for example

> /public/css/js/login.css
> /public/css/js/registration.css
> /public/css/js/search.css
Enter fullscreen mode Exit fullscreen mode

Apart from extracting Vue styles you also have to remove importing CSS files in JavaScript.

import "some-library/some-style.css"
Enter fullscreen mode Exit fullscreen mode

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.

mix.extract(['vue']);
Enter fullscreen mode Exit fullscreen mode

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 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')
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  1. Move the logic to the mounted method.
data() {
    name: null
},
mounted() {
    // client only
    this.name = localStorage.getItem('name')
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (14)

Collapse
kp profile image
KP

@mzanggl thanks for this article. I am interested in making my Laravel app quick to render (client-side), but SEO is super-important so I need SSR.
What are your thoughts on inertiajs vs nuxt vs github.com/spatie/laravel-server-s...?

inertiajs, in case you are unfamiliar:
github.com/inertiajs/inertia-laravel
github.com/inertiajs/inertia-vue
I just havent gone deep enough to see if SSR works with inertia.

Collapse
michi profile image
Michael Z Author

I really like the idea of inertiajs, have been playing around with a while ago as well. But not sure if it is production ready yet, it's rather new. As far as I know though, the DOM will still be rendered through JS. With that in mind I would go with nuxtjs. Nuxt also provides more benefits than just SSR, you will get a proper battery included vue framework.

The solution I used for this article was only because I already had an app and couldn't afford to rewrite it with Nuxt.

Collapse
kp profile image
KP

Makes a lot of sense @michael
I'm biting the bullet and going with Nuxt (As painful as it is).

Cheers and you have a new github follower.

Collapse
bertugkorucu profile image
Bertug Korucu • Edited

@mzanggl Thanks for the detailed post. I have a question. Did you try Lazy Loading with this approach? For me static loading works, however when I try lazy loading:

export const routes = [{
     path: '/', 
     name: "Home", 
     component: () => import('../views/Home')
}]

It throws:

Error: Cannot find module './js/chunks/server/0.js?id=c3384f174123f0848451'

The command "/usr/bin/node /home/vagrant/Code/project/storage/app/ssr/717358e60bfd52035a1e58256cdfbba0.js" failed. Exit Code: 1(General error) Working directory: /home/vagrant/Code/project/public Output: ================ Error Output: ================ internal/modules/cjs/loader.js:628 throw err; ^ Error: Cannot find module './js/chunks/server/0.js?id=c3384f174123f0848451'

Update: This is how I solved it - stackoverflow.com/questions/582169...

Collapse
wherejuly profile image
wj • Edited

Hi Michael, thansk for sharing. That is all some piece of work. Question: what is the reason to SSR with Vue/Node while you have PHP/Laravel for the same SSR at hand? What issue did you resolved with SSR-ing Vue while having SSR capabilities of PHP?

Collapse
michi profile image
Michael Z Author

Hi wj,

You only get SSR when using blade / raw HTML. With vue, react etc. all you get is <div id="app"></div> in the initial render. Then everything gets processed through JavaScript. This is enough for the user, but not for SEO.

We are basically following this approach.

And here is an indepth video.

Long story short, without SSR indexing takes more time and crawlers that are stuck on old JavaScript engines might not be able to read the content. (Luckily Google Bot was updated to Chrome 74 just recently).

Collapse
matus0011 profile image
matus0011

Why, for example: on-click, v-model cannot detect vue ?

Collapse
michi profile image
Michael Z Author

on-click and v-model should work just fine. Maybe it is something else with your setup. Try replicating the issue on a barebones Laravel application.

Collapse
matus0011 profile image
matus0011

Thanks for the reply, vue already can detect events.
I have next question,how to correctly implement external dependencies like vue-select ? because now when i try implement external dependencies i get error " window is not defined"

Thread Thread
michi profile image
Michael Z Author

This seems to be an issue with the vue-select library itself: github.com/sagalbot/vue-select/iss.... (There are a couple of closed issues related to this, hopefully you can find a solution in one of them)

Collapse
hipertracker profile image
Jaroslaw Zabiello

You are wrong. PHP can understand JavaScript thanks to V8js. There is no need to spawn an external Node server.

Collapse
microdreamit profile image
Shahanur Sharif

setting up V8js is a hell process. I have been trying to implement it but its far more complicated.

Collapse
michi profile image
Michael Z Author

In my research regarding v8js, the setup looked more complicated.

Actually the PHP dependency supports both Node and v8js.

Collapse
tayambamwanza profile image
Tayamba Mwanza

Have you looked into rendertron?