Essay

Tutorial · April 15, 2021

Separate marketing + app sites with Laravel Jetstream and Inertia.js

By separating your Laravel Jetstream application into an Inertia-powered dashboard and a standard Blade marketing site, you can leverage the best of both worlds for a flexible, high-performance developer experience.

If you're building an application with Laravel Jetstream, you may find yourself in the situation where you want

  1. the entire app part to be owned by Intertia.js
  2. all of the marketing website as standard Blade views

I find this to be a great developer experience. All the app stuff is totally Vue + Inertia, which is great for the more complicated, interactive bits that your app will likely have, while the marketing site is plain ol' Blade. Easy to update, change, throw away, and rework.

Going down this route, there are just a few things you'll want to look out for.

Layouts

It's best to set up two different Blade layouts, one for the application and one for the marketing views. This way you're not including Inertia on the pages that don't have it, and you're not including your marketing CSS and JavaScript in the Inertia part of your site.

The app layout is just the basic one from Jetstream.

layouts/app.blade.php

<!DOCTYPE html>
<head>
    <meta charset="utf-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>Dashboard for My Great App</title>
    <link rel="stylesheet" href="{{ mix('css/app.css') }}">

    <script src="{{ mix('js/app.js') }}" defer></script>
</head>
<body class="font-sans antialiased">
    @inertia
</body>
</html>

And then your marketing layout can be whatever you want, but here's a basic example:

layouts/marketing.blade.php

<!DOCTYPE html>
<head>
    <meta charset="utf-8">

    <title>Marketing for My Great App</title>
    <link rel="stylesheet" href="{{ mix('css/marketing.css') }}">

    <!-- Just include Alpine, no bundle needed -->
    <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>
</head>
<body class="font-sans antialiased">
    @yield('body')
</body>
</html>

App and Marketing on Different Domains

Even if your entire app is in a single Laravel project, it's still a good idea to separate your app and marketing domains.

This makes it easier in the future if you want to move your marketing site off to a third party application, it avoids any route collisions, and lets you scope your cookies and sessions to just the app.

You can add the domain configuration in the app.php

config/app.php

return [
    // ...

    'marketing_domain' => env('MARKETING_DOMAIN', 'torchlight.dev'),
    
    'app_domain' => env('APP_DOMAIN', 'app.torchlight.dev'),
];

And then use the domain method to scope your routes:

routes/web.php

Route::domain(config('app.marketing_domain'))->group(function () {
    // All of your marketing routes...
});

Route::domain(config('app.app_domain'))->group(function () {
    // All of your app routes...
});

The Logout Action

The last thing you'll need to customize is the LogoutResponse that Fortify sends. By default, the response is this:

vendor/laravel/fortify/src/Http/Responses/LogoutResponse.php

class LogoutResponse implements \Laravel\Fortify\Contracts\LogoutResponse
{
    public function toResponse($request)
    {
        return $request->wantsJson()
            ? new JsonResponse('', 204)
            : redirect('/');
    }
}

But now because we're trying to redirect to 1) another domain and 2) a non-inertia endpoint, we'll need to update this.

From the (very good) Inertia docs:

Sometimes it's necessary to redirect to an external website, or even another non-Inertia endpoint in your app, within an Inertia request. This is possible using a server-side initiated window.location visit.

That's exactly our situation!

Fortunately, Fortify makes it very easy to customize all of this stuff without having to totally eject.

We're going to create a LogoutResponse of our own and then register it.

app/Http/Responses/LogoutResponse.php

class LogoutResponse implements \Laravel\Fortify\Contracts\LogoutResponse
{
    public function toResponse($request)
    {
        // Use Intertia::location to redirect to an
        // "external" URL, i.e. our marketing site.
        return Inertia::location(route('marketing.welcome'));
    }
}

Now that we have the response that we need, the last thing to do is to register it in the service container. We'll bind it to the LogoutResponseContract class.

app/Providers/FortifyServiceProvider.php

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(LogoutResponseContract::class, LogoutResponse::class);
    }
}

Inertia::location("That's it!")

That's all you need to do to get your Jetstream and marketing sites all set up on different domains! It's only a couple of steps, but it can be a little bit of futzing if you've never done it before. Hopefully this has made it easier.

Details
Published
April 15, 2021
Reading
3 min
Words
629
Newsletter

Every week: the best things I've found and what I've been thinking about.

 

Related
  1. Apr 2025A cookieless, cache-friendly image proxy in Laravel (inspired by Cloudflare)
  2. Nov 2024Reservable models in Laravel
  3. Nov 2024Make 5x faster outbound requests in Laravel
Colophon

Set in Tiempos and IBM Plex Mono on a 22px baseline.

Thanks for spending a little of your day here.