I'm building out a Shedquarters in my backyard! Check out the ever-evolving post + pictures here →

Separate Marketing + App sites with Laravel Jetstream and Inertia.js

April 15, 2021

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>
Code highlighting powered by torchlight.dev (A service I created!)

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.

Thanks for reading! My name is Aaron and I'm currently working at small property tax firm in Texas, where I serve in dual roles as COO & CTO.

My main project focus is Hammerstone, where we build components for your Laravel and Rails applications. I currently do a podcast.

If you ever have any questions or want to chat, I'm always on Twitter
Copyright 2013 - 2021, Aaron Francis.