Cache static pages in Laravel by Cloudflare CDN

Published 02 December 2019 12:00 (6 minute read)

Some of the websites I made don't need to be updated very often, but they receive in some periods a lot of traffic within minutes. To make the website faster and reduce the amount of database queries executed I want my CDN (in this case, Cloudflare) to cache those static pages for 5 minutes.

When you use Cloudflare CDN you can enable caching for specific DNS records within the Dashboard. But what does Cloudflare caching?

Caching is the process of storing copies of files in a cache, or temporary storage location, so that they can be accessed more quickly. Technically, a cache is any temporary storage location for copies of files or data, but usually the term is used in reference to Internet technologies. DNS servers cache DNS records for faster lookups, CDN servers cache content to reduce latency, and web browsers cache HTML files, JavaScript, and images in order to load websites more quickly.

More information can be found at https://www.cloudflare.com/learning/cdn/what-is-caching/.

Enable caching in Laravel

I've made a middleware (CachePagesMiddleware) that set the correct headers for Cloudflare to cache the response and let them serve the next requests. This means the servers of Cloudflare will serve the page before it hits my application.

<?php

namespace App\Http\Middleware;

use Closure;
use Symfony\Component\HttpFoundation\Response;

class CachePagesMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        if ($this->shouldCacheResponse($request, $response)) {
            $response->headers->add([
                'Cache-Control' => 'max-age=1800, public'
            ]);
        }

        return $response;
    }

    public function shouldCacheResponse($request, Response $response)
    {
        if (!config('app.env') == 'production') {
            return false;
        }

        if (auth()->check()) {
            return false;
        }

        if ($request->method() !== 'GET') {
            return false;
        }

        if (! $response->isSuccessful()) {
            return false;
        }

        return true;
    }
}

To use this, I've added the middleware to all the routes I wanted to be cached. I have not enabled this by default because I want to have the control about which routes are being cached and if I forget to disabled the caching for the specific route it can possibly leak information (of course, you never want this to happen!).

In the HTTP kernel (app/Http/Kernel.php) I added them as a route middleware:

protected $routeMiddleware = [
    'cache' => \App\Http\Middleware\CachePagesMiddleware::class,
    ...
];

And added them to the routes that I wanted to be cached:

Route::get('/my-pages', '[email protected]')
    ->middleware('cache');

Make your static resources "stateless"

Stateless responses are without a session. When you don't sent a session id with your response Cloudflare will cache it (if it meets the cacheable file extensions or custom page rule).

For a stateless response I've created a custom route in my Laravel project.

// file: app/Providers/RouteServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    //...

    /**
     * Define the routes for the application.
     *
     * @return void
     */
    public function map()
    {
        $this->mapApiRoutes();

        $this->mapWebRoutes();

        $this->mapStatelessRoutes(); // <-- added
    }

    //...

    /**
     * Define the "stateless" routes for the application.
     * 
     * @return void
     */
    protected function mapStatelessRoutes()
    {
        Route::middleware('stateless')
             ->namespace($this->namespace)
             ->group(base_path('routes/stateless.php'));
    }

    //...
}

And in the Kernel.php I've created a middleware group:

// file: app/Http/Kernel.php
<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    //...

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        //...

        'stateless' => [
            // at this moment no middlewares for this group
        ],

        //...

    ];

    //...
}

And the stateless routes:

// file: routes/stateless.php
<?php

Route::middleware('cache.headers:public;max_age='.(60 * 60 * 24 * 365))->group(function () {
    Route::group(['prefix' => '/public-s3/'], function () {
        Route::fallback('PublicS3Controller');
    });
});

And the controller for handling the s3 files to be cached by Cloudflare CDN.

// file: app/Http/Controllers/PublicS3Controller.php
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Storage;
use League\Flysystem\FileNotFoundException;

class PublicS3Controller extends Controller
{
    public function __invoke($item = '')
    {
        $disk = Storage::disk('s3');

        try {
            $file = $disk->get($item);
        } catch (FileNotFoundException $exception) {
            abort(404);
        }

        $mime = $disk->mimeType($item);

        return response($file, 200, [
            'Content-Type' => $mime,
        ]);
    }
}

The cache headers are set in the routes/stateless.php route group to a year in seconds, so (60 * 60 * 24 * 365) = 31.536.000 seconds that Cloudflare & end-browser will cache that file.

To refresh the cache for that file you can add a version parameter to the url that will trigger the browser & Cloudflare CDN cache busting.

Ps, even when you have a stateless url, keep in mind your url need to have a cacheable file extension or a custom page rule.

Cacheable file extensions

By default Cloudflare caches the following file extensions:

  • bmp
  • ejs
  • jpeg
  • pdf
  • ps
  • ttf
  • class
  • eot
  • jpg
  • pict
  • svg
  • webp
  • css
  • eps
  • js
  • pls
  • svgz
  • woff
  • csv
  • gif
  • mid
  • png
  • swf
  • woff2
  • doc
  • ico
  • midi
  • ppt
  • tif
  • xls
  • docx
  • jar
  • otf
  • pptx
  • tiff
  • xlsx

Take a look at the up-to-date list on Cloudflare.com.

Help! Cloudflare is not caching my HTML pages

That can be possible, by default Cloudflare doesn't cache HTML pages. If Cloudflare didn't cache the page you have to take a look at the "CF-Cache-Status" header in the response. If this is "DYNAMIC" you have to create a page rule to cache the page. By default Cloudflare isn't caching html content.

When do you not need to cache the response?

Some pages doesn't need to be cached, this can be a form request, when you are authenticated or when the request isn't successful.

How to test if Cloudflare caches the response?

There is a site for this that checks how Cloudflare. It looks at the "CF-Cache-Status" header send by the response to verify if the page is cached by Cloudflare. More information can be found here: https://cf-cache-status.net/

Convert images to webp

It's possible to automatically convert your images to webp when you have Cloudflare Pro plan, then Cloudflare will convert the requested image to webp.

It will only convert the image to webp when it has a smaller file size than the original, this way it will give your users the fastest loading experience.

You can if the image is converted to webp by taking a look at the response headers. The "content-disposition" header contains the following information:

inline; filename="xnxjxnjx-200x200.webp"
Cloudflare "check if webp" by @sdayman

Cloudflare "check if webp" by @sdayman

As said by sdayman:

  1. If the original format is more efficient, Cloudflare won’t convert it to WebP
  2. WebP is only delivered to browsers that advertise they accept WebP
  3. The image retrains the .jpg extension, but it’s really WebP

More information can be found on Cloudflare.com.

Robin Dirksen

Robin Dirksen

On my blog, you can find articles that I've found useful or wanted to share with anyone else.

If you want to know more about this article or just want to talk to me, don't hesitate to reach out.