A Laravel middleware to optimise images with imgproxy on arbitrary markup

Author: Ally
Published:

Summary:

Create a Laravel middleware to parse the response DOM and update a set of img’s src to be routed through imgproxy.

Table of Contents

  1. imgproxy
  2. Middleware

Recently I had to add imgproxy to a section of a Laravel which hosts CMS content through a WYSIWYG and is persisted as html.

I decided to use a middleware to rewrite the content, I used the following (well rated) packages to help:

imgproxy

For local development, I obviously use docker.

The following variables will need to be added to .env file for your Laravel project (and they are the same that we will pass to our imgproxy service later):

IMGPROXY_KEY=
IMGPROXY_SALT=
IMGPROXY_SIGNATURE_SIZE=32

For IMGPROXY_KEY and IMGPROXY_SALT I use the following to generate values:

echo $(xxd -g 2 -l 64 -p /dev/random | tr -d '\n')

I don’t need to run it all the time, so this will suffice:

docker run --rm --env-file /path/to/laravel-site/.env -p 8888:8080 darthsim/imgproxy:latest

Since I use valet, I need to proxy since serving http content on https is tricky.

valet proxy --secure imgproxy.ac93 http://localhost:8888

This will mean that https://imgproxy.ac93.test will be what will used in the Laravel app to host our images through imgproxy. Add it to the .env.

IMGPROXY_URL=https://imgproxy.ac93.test

Add the following to a relevant config file, e.g. config/services.php:

<?php

return [

    'imgproxy' => [
        'url'  => env('IMGPROXY_URL'),
        'salt' => env('IMGPROXY_SALT'),
        'key'  => env('IMGPROXY_KEY'),
    ],

];

And finally, a little bit of dependency injection:

app/Providers/AppServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Imgproxy\UrlBuilder;
use Illuminate\Foundation\Application;

class AppServiceProvider extends ServiceProvider
{

    public function boot()
    {
        $this->app->bind(UrlBuilder::class, function (Application $app) {
            return new UrlBuilder(
                config('services.imgproxy.url'),
                config('services.imgproxy.key'),
                config('services.imgproxy.salt')
            );
        });
    }
}

Middleware

The middleware is relatively simple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Imgproxy\UrlBuilder;
use PHPHtmlParser\Dom;
use Throwable;

class ImgproxyCmsContent
{
    /**
     * @param Request $request
     * @param Closure $next
     * @return Response
     */
    public function handle($request, Closure $next)
    {

You can add ?no_imgproxy to url to skip the middleware for testing, etc.

23
24
25
        if ($request->has('no_imgproxy')) {
            return $next($request);
        }

Get the response content and load it into the DOM parser.

26
27
28
29
30
31
        /** @var Response $response */
        $response = $next($request);

        $dom = new Dom();
        try {
            $dom->loadStr($response->getContent());

For each img in the DOM, don’t change svg and only rewrite images which are hosted on our site, i.e. in public; or hosted on our bucket.

imgproxy can be configured to host s3 content directly, but out of scope here, and serving public files is advantageous in this scenario.

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
            collect($dom->find('img'))
                ->reject(function (Dom\Node\HtmlNode $node) {
                    return Str::of($node->getAttribute('src'))
                        ->endsWith('.svg');
                })
                ->filter(function (Dom\Node\HtmlNode $node) {
                    $src = Str::of($node->getAttribute('src'));

                    return $src->startsWith(config('app.url'))
                        || $src->startsWith(Storage::disk('s3')->url('.'));
                })
                ->each(function (Dom\Node\HtmlNode $node) {
                    $node->setAttribute(
                        'src',
                        app(UrlBuilder::class)
                            ->build(
                                $node->getAttribute('src'),

Since this middleware is only applied to one section of the site which has a known max-width.

49
50
51
52
53
54
55
56
57
58
59
60
61
62
                                896,
                                0
                            )
                            ->useAdvancedMode()
                            ->toString()
                    );
                });

            return $response->setContent((string) $dom);
        } catch (Throwable $e) {
            return $response;
        }
    }
}

You can see the middleware gist.

Using Laravel's pipeline to replace CMS content placeholders with values
Using whiptail to show the active git branch for a set of git repositories
To bottom
To top