Deploy your Laravel app as a static site to Netlify

Aug 8, 2021 Laravel

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!

avatar

About Me

I am a software engineer from Japan and currently based in Singapore. Over the past 5 years, I've been creating e-commerce and FinTech web applications using Laravel, Angular, and Vue.js. Making clean, maintainable, and scalable software with agile methodology is one of my biggest passions. Im my spare time, I play the bass and enjoy DIY.