Deploy your Laravel app as a static site to Netlify
I'm a big fan of JAMstack. With JAMstack, your entire site is pre-built as static HTML files and hosted on CDN. Since those static files are served via CDN, it's blazing fast, scalable, and more secure as there is no backend servers and databases.
However, it might be overwhelming if you are not familiar with static site generators such as Next.js, Gatsby.js, Nuxt.js, etc. Also, you will most likely need to use a headless CMS to manage contents for your site, and consume their APIs to fetch contents on the frontend.
It may sound like an overkill for Laravel developers. In fact, you can deploy static version of your Laravel app to CDN without writing a single line of JavaScript and without consuming third-party APIs. You can directly query contents from database of your Laravel app and render them in blade templates. You don't need learn anything new.
In this post, I will walk you through how to build a static version of your Laravel app and deploy it to Netlify.
TL;DR
Here is the full source code of the sample Laravel blog application that I created.
https://github.com/avosalmon/laravel-static-blog
The sample app uses the following tech stack.
- PHP 8
- Laravel 8
- Laravel Sail for local docker environemnt
- Wink for blog admin panel
- Tailwind CSS
- Laravel Export for exporting the entire site into static HTML files
- Netlify CLI for deploying the static files to Netlify
Install Laravel Export
First, install spatie/laravel-export package to your Laravel app.
$ composer require spatie/laravel-export
Once the package is installed, publish the config file.
$ php artisan vendor:publish --provider=Spatie\\Export\\ExportServiceProvider
Now, export.php
config file has been copied to the config
folder.
By running php artisan export
, Laravel Export will scan your app and create an HTML page from every URL it crawls. The entire public
directory also gets added to the bundle so your assets are in place too. You can find the exported static files in the dist
folder in your application root.
Setup Netlify CLI
Now that your site has been exported into static HTML files, let's deploy them to Netlify. I assume you've already setup a site on Netlify. First, install netlify-cli.
$ npm install -D netlify-cli
Once it's installed, create netlify.toml
file in your application root with the following content.
[build]
publish = "dist"
This tells Netlify CLI to deploy the files in the dist
folder. See here for more details.
Next, add environment variables for the Netlify site id and auth token to your .env
file.
NETLIFY_SITE_ID=xxx
NETLIFY_AUTH_TOKEN=xxx
Since I use docker compose for my local environment, add these variables to the app container in docker-compose.yml
so that the dokcer container can reference these variables from the .env
file.
environment:
NETLIFY_SITE_ID: '${NETLIFY_SITE_ID}'
NETLIFY_AUTH_TOKEN: '${NETLIFY_AUTH_TOKEN}'
And then, add deploy
npm script in your package.json
.
{
"scripts": {
"deploy": "netlify deploy --prod"
},
}
Now, you can deploy the dist
folder to Netlify by running npm run deploy
.
Hooks
While you can run php artisan export
and npm run deploy
separately, you can add hooks for Laravel Export to do things before or after an export.
If you add the following hooks to the export.php
config file, it will build frontend assets before every export, and deploy to Netlify after an export. So, you just need to run php artisan export
.
'before' => [
'assets' => '/usr/bin/npm run prod',
],
'after' => [
'deploy' => '/usr/bin/npm run deploy',
],
Query params are not supported
When Laravel Export generates HTML files, file path of a page is determined by routes. For example, if a route path is /posts/123
, Larave Export will generate a file /posts/123/index.html
.
However, it will ignore query params when generating a file. Let's say, your app has a page that shows a list of blog posts with pagination. By default, Laravel uses query params for pagination. So, the page URL will look like /posts?page=2
. But, Laravel Export will generate a file /posts/index.html
regardless of the page
query param.
So, you need to implement a custom pagination logic that relies on a route path variable instead of a query parameter.
In this example, I added 2 routes for the blog post page. The first one is the top page of the blog and the second one is for pagination.
Route::get('/', [PostController::class, 'index']);
Route::get('/page/{page}', [PostController::class, 'index']);
In the controller, you can offset the database query using the page
parameter. As you can see, the controller passes the currentPage
, isFirstPage
, and isLastPage
attributes to the blade template to render pagination links in the template.
class PostController extends Controller
{
const PER_PAGE = 10;
public function index(int $page = 1)
{
if ($page <= 0) {
$page = 1;
}
$offset = self::PER_PAGE * ($page - 1);
$posts = Post::with('tags')
->live()
->orderByDesc('publish_date')
->offset($offset)
->limit(self::PER_PAGE)
->get();
$total = Post::with('tags')
->live()
->count();
return view('post.index', [
'posts' => $posts,
'currentPage' => $page,
'isFirstPage' => $page === 1,
'isLastPage' => ($total - $offset) <= self::PER_PAGE
]);
}
}
In the blade template, you can render pagination links like this.
<nav>
<div>
@unless ($isFirstPage)
<a href="/page/{{ $currentPage - 1 }}">< Previous</a>
@endunless
@unless ($isLastPage)
<a href="/page/{{ $currentPage + 1 }}">Next ></a>
@endunless
</div>
</nav>
Run php artisan export
again and it will generate HTML files for paginated routes as well. e.g. /posts/page/2/index.html
Wrap up
You can build the entire Laravel app as a static site and deploy it to Netlify from your local machine. You don't need to setup a full-fledged server for hosting your backend app and database.
Note that this approach works for web apps that its content doesn't change so frequently such as blog or landing pages.
If you have any questions or comments, please comment in this Twitter thread!