DEV Community

Cover image for Setting up Laravel with Inertia.js + Vue.js + Tailwind CSS
George Tudor
George Tudor

Posted on • Updated on

Setting up Laravel with Inertia.js + Vue.js + Tailwind CSS

NOTICE: This guide was written using Laravel 8. It also works with Laravel 9 that's using Laravel Mix. If you're using Laravel 9 with Vite, follow this tutorial.

Laravel is by far the most popular open source PHP framework out there. I've been using it since version 4 and today we celebrate the launch of the 9th version. What an achievement!

The beauty of this php framework is not only the ease of writing code but the community behind it which always find new ways to improve the code, develops new packages and also pushes the integration with other awesome frameworks.

For example, if it wasn't for Laravel's creator, Taylor Otwell, I think Vue wouldn't have been so popular today. He stated in a tweet many years ago that Vue was actually easier to learn compared to React... and I couldn't agree more. So even today, even if Laravel has scaffoldings for both JS frameworks, I'd always pick Vue over React, just because it is easier.

So what I'm trying to point out is that Laravel will always try to adopt and support new cool JS frameworks or any other tool that's really a gamechanger. Inertia.js and Tailwind CSS are just two more tools that were added to the book that are really mind blowing.

Before we dive in deep, we just want to make sure we have all the tools we need. We'll be using PHP 8, so make sure you have that installed, Composer and NPM. I'll briefly go over how to install Composer and NPM.

Installing Composer

If you already have Composer installed, you can skip the following chunk of commands:

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "unlink('composer-setup.php');"
php composer-setup.php --install-dir=/usr/local/bin
Enter fullscreen mode Exit fullscreen mode

We've just installed Composer and moved it into our /usr/local/bin directory, so now the composer command will be available globally in our terminal.

One more thing we need to do is to add our Composer's vendor bin directory in $PATH. You can find more info about this in the official Laravel documentation here.

For me, on WSL2 Ubuntu on Windows 11, the easiest way to do it is to add the path to .bashrc. So nano ~/.config/.bashrc and add the path to the Composer's vendor bin directory at the end of the file:

export PATH=~/.config/composer/vendor/bin:$PATH
Enter fullscreen mode Exit fullscreen mode

Now save and run source ~/.config/.bashrc to refresh the configuration.

Installing NodeJs

We will also need NodeJS to use the node package manager aka npm. To install it on Ubuntu, we just use the following lines:

sudo apt update
sudo apt install nodejs
Enter fullscreen mode Exit fullscreen mode

Now npm should be available globally:

npm -v #retuns the current version
Enter fullscreen mode Exit fullscreen mode

Installing Laravel

To install Laravel you can use Laravel Sail, which will boot up a Docker container, or you can use the old fashioned Laravel installer. I'm using Windows 11 + WSL2 running Ubuntu and I prefer the Laravel installer, so I need to run the following commands one by one. Please note that I'm using Laravel 8 and PHP 8.0.

Making sure we are in the desired folder we're going to require the Laravel's installer globally and then use it to create a new app called "awesome-app" (this will automatically create the folder with the same name).

composer global require laravel/installer
laravel new awesome-app
cd awesome-app
npm install #installs all the dependencies
Enter fullscreen mode Exit fullscreen mode

If the laravel new awesome-app returns laravel: command not found make sure that you have you're Composer's vendor bin directory in $PATH (see above).

Now that we have our fresh install, we can go ahead and add Inertia.js, Vue.js and Tailwind CSS.

Installing Tailwind CSS

Tailwind requires the least amount of effort. We just need to install postcss and autoprefixer too.

npm install -D tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode

Let's create the tailwind config file...

npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

...and add our template files so the Tailwind's JIT will know exactly what classes we use in our templates and generate them. So open tailwind.config.js and add the following line ./resources/js/**/*.{vue,js} to the content so the file will look like this:

module.exports = {
    content: ["./resources/js/**/*.{vue,js}"],
    theme: {
        extend: {},
    },
    plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

We also have to add the Tailwind's directives to resources/css/app.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

The last thing to do is to require Tailwind into webpack.mix.js which uses Laravel Mix to build our assets. We'll get back to our webpack config file later, but for now it will have to look like this:

const mix = require("laravel-mix");

mix.js("resources/js/app.js", "public/js").postCss(
    "resources/css/app.css",
    "public/css",
    [require("tailwindcss")]
);
Enter fullscreen mode Exit fullscreen mode

Installing Vue.js

We'll be using version 3 of Vue. Please note that as of 7th of February 2022, the version 3 has become the default version.

Vue 3 has two different API styles. It still supports the Options API from Vue 2, but it makes available the Composition API. If you don't understand what that is, you can read this short introduction. For the sake of simplicity, we'll be using the Options API since most of the developers are already used to it and have been using it since forever.

So let's add Vue 3:

npm install vue@next
Enter fullscreen mode Exit fullscreen mode

Installing Inertia.js

First we'll need to install Inertia's server side package:

composer require inertiajs/inertia-laravel
Enter fullscreen mode Exit fullscreen mode

Next we'll need to create the Inertia middleware which handles the requests and also helps us to share data with all our Vue views, similar to View::share().

php artisan inertia:middleware
Enter fullscreen mode Exit fullscreen mode

HandleInertiaRequests.php will be created inside app/Http/Middleware. We'll just need to add this middleware to the web middleware group inside app/Http/Kernel.php:

'web' => [
    // ...
    \App\Http\Middleware\HandleInertiaRequests::class,
],
Enter fullscreen mode Exit fullscreen mode

Coming next is the Inertia's client side. We're using Vue 3, so we'll install Inertia alongside with the Vue 3 adapter:

npm install @inertiajs/inertia @inertiajs/inertia-vue3
Enter fullscreen mode Exit fullscreen mode

Let's throw in the Inertia's progress bar. This will be used as a loading indicator between page navigation.

npm install @inertiajs/progress
Enter fullscreen mode Exit fullscreen mode

Inertia uses Laravel's routes, so we won't need to use a client side router, but to make use of Laravel's web.php routes, we have to pass them to the DOM somehow. The easiest way to do it to use Ziggy.
Let's install Ziggy:

composer require tightenco/ziggy
Enter fullscreen mode Exit fullscreen mode

Now we can use the @routes blade directive inside our blade template to expose the web.php routes to the client side.

Gluing everything together

Now we have everything installed and ready to be used. We have installed Vue 3, Inertia and Tailwind CSS.

Let's start by setting up our one and only blade template. We're going to rename the welcome.blade.php to app.blade.php inside resources/views. We're also going to remove all its content and replace it with the following:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    @routes
    <link href="{{ asset(mix('css/app.css')) }}" rel="stylesheet">
    <script src="{{ asset(mix('js/manifest.js')) }}" defer></script>
    <script src="{{ asset(mix('js/vendor.js')) }}" defer></script>
    <script src="{{ asset(mix('js/app.js')) }}" defer></script>
    @inertiaHead
</head>

<body>
    @inertia
</body>

</html>

Enter fullscreen mode Exit fullscreen mode

So first of all you will notice we don't have any <title>. This is because we need it to be dynamic and we can set that using Inertia's <Head> component. That's why you can see that we've also added the @inertiaHead directive.

We have added the @routes directive to pass the Laravel's routes in the document's <head>.

We are importing our app.css and also a bunch of .js we are going to take care shortly.

In the <body> we only use the @inertia directive which renders a div element with a bunch of data passed to it using a data-page attribute.

Ziggy Setup

Let's get back to Ziggy and generate the .js file that contains all of our routes. We'll gonna import this into our app.js a bit later.

php artisan ziggy:generate resources/js/ziggy.js
Enter fullscreen mode Exit fullscreen mode

To resolve ziggy in Vue, we'll have to add an alias to the Vue driver in webpack.mix.js:

const path = require("path");

// Rezolve Ziggy
mix.alias({
    ziggy: path.resolve("vendor/tightenco/ziggy/dist/vue"),
});
Enter fullscreen mode Exit fullscreen mode

Setting up app.js

Let's move on by setting up our app.js file. This is our main main file we're going to load in our blade template.

You can delete bootstrap.js from resources/js. That's no longer needed.

Now open resources/js/app.js and delete everything from it and add the following chunk of code:

import { createApp, h } from "vue";
import { createInertiaApp, Link, Head } from "@inertiajs/inertia-vue3";
import { InertiaProgress } from "@inertiajs/progress";

import { ZiggyVue } from "ziggy";
import { Ziggy } from "./ziggy";

InertiaProgress.init();

createInertiaApp({
    resolve: async (name) => {
        return (await import(`./Pages/${name}`)).default;
    },
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue, Ziggy)
            .component("Link", Link)
            .component("Head", Head)
            .mixin({ methods: { route } })
            .mount(el);
    },
});
Enter fullscreen mode Exit fullscreen mode

What does this is to import Vue, Inertia, Inertia Progress and Ziggy and then create the Inertia App. We're also passing the Link and Head components as globals because we're going to use them a lot.

Inertia will load our pages from the Pages directory so I'm gonna create 3 demo pages in that folder. Like so:

demo-inertia-pages

Each page will container the following template. The Homepage text will be replaced based on the file's name:

<template>
    <h1>Homepage</h1>
</template>
Enter fullscreen mode Exit fullscreen mode

The next step is to add the missing pieces to the webpack.mix.js file. Everything needs to look like this:

const path = require("path");
const mix = require("laravel-mix");

// Rezolve Ziggy
mix.alias({
    ziggy: path.resolve("vendor/tightenco/ziggy/dist/vue"),
});

// Build files
mix.js("resources/js/app.js", "public/js")
    .vue({ version: 3 })
    .webpackConfig({
        resolve: {
            alias: {
                "@": path.resolve(__dirname, "resources/js"),
            },
        },
    })
    .extract()
    .postCss("resources/css/app.css", "public/css", [require("tailwindcss")])
    .version();
Enter fullscreen mode Exit fullscreen mode

You can see that we're specifying the Vue version that we're using, we're also setting and alias (@) for our root js path and we're also using .extract() to split our code into smaller chunks (optional, but better for production in some use cases).

Setting up our Laravel routes

We've taken care of almost everything. Not we just need to create routes for each of the Vue pages we have created.

Let's open the routes/web.php file and replace everything there with the following:

<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get(
    '/',
    function () {
        return Inertia::render(
            'Home',
            [
                'title' => 'Homepage',
            ]
        );
    }
)->name( 'homepage' );

Route::get(
    '/about',
    function () {
        return Inertia::render(
            'About',
            [
                'title' => 'About',
            ]
        );
    }
)->name( 'about' );

Route::get(
    '/contact',
    function () {
        return Inertia::render(
            'Contact',
            [
                'title' => 'Contact',
            ]
        );
    }
)->name( 'contact' );
Enter fullscreen mode Exit fullscreen mode

You can notice right away that we're not returning any traditional blade view. Instead we return an Inertia::render() response which takes 2 parameters. The first parameter is the name of our Vue page and the 2nd is an array of properties that will be passed to the Vue page using $page.props.

Modifying the Vue pages

Knowing this we can modify our pages to the following template and also add a navigation to them:

<template>
    <Head>
        <title>{{ $page.props.title }} - My awesome app</title>
    </Head>

    <div class="p-6">
        <div class="flex space-x-4 mb-4">
            <Link
                :href="route('homepage')"
                class="text-gray-700 bg-gray-200 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
                >Homepage</Link
            >
            <Link
                :href="route('about')"
                class="text-gray-700 bg-gray-200 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
                >About</Link
            >
            <Link
                :href="route('contact')"
                class="text-gray-700 bg-gray-200 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
                >Contact</Link
            >
        </div>

        <h1>This is: {{ $page.props.title }}</h1>
    </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Now we have a simple navigation on each page and also a dynamic page <title>. The only thing left now is to compile everything and start the server:

npm run dev
php artisan serve
Enter fullscreen mode Exit fullscreen mode

The last command will start a server on your localhost using the 8000 port http://127.0.0.1:8000/ so navigating to it you will be able to see the final result.

Testing

It should look similar to this:
inertia-demo.gif

That's pretty much everything you need to know. Of course there's more like using Laravel's lang files, Vue layouts, server side rendering... but maybe in a part 2.

Enjoy!

Support & follow me

Buy me a coffee Twitter GitHub Linkedin

Oldest comments (2)

Collapse
 
melaniecarr23 profile image
Melanie

This is awesome!!! I just upgraded to Laravel 9 the other day and am taking a course in Vue. Just finished a tailwind course and followed this. How amazing the way the routing works. Can't wait to learn more and develop from there. THANK YOU!

Collapse
 
shang profile image
Shang • Edited

Completely not working!
I am confusing.
I followed your guidance fully without any missing, but when I run "npm run dev", I got this message.

The following dependencies are imported but could not be resolved:
ziggy (imported by E:/xampp/htdocs/glenborn-job-offer/resources/js/app.js)
Are they installed?

prnt.sc/H3LhtVhjJFC6

Of course, I install ziggy package.
And what I am confusing is , do I have to run "npm install" command in root folder or ./resource folder?
And what I am not sure is , when I installed Laravel, vue3 etc using command, webpack.mix.js file was not generated automatically, so I made this file manually.

And in this code, in where route variable is defined? (.mixin({ methods: { route } }))

import { createApp, h } from "vue";
import { createInertiaApp, Link, Head } from "@inertiajs/inertia-vue3";
import { InertiaProgress } from "@inertiajs/progress";

import { ZiggyVue } from "ziggy";
import { Ziggy } from "./ziggy";

InertiaProgress.init();

createInertiaApp({
resolve: async (name) => {
return (await import(./Pages/${name})).default;
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue, Ziggy)
.component("Link", Link)
.component("Head", Head)
.mixin({ methods: { route } })
.mount(el);
},
});

Please let me know what's wrong.
Thanks!