Blog

Latest News and Updates

How to manage static images with Symfony’s Webpack Encore

Any web project has basically 4 kind of places where images reside:

  1. In the CSS;
  2. In a font file (think at icons);
  3. In the cloud (think at AWS S3);
  4. In the project itself as static files.

Symfony’s Webpack Encore is great to manage the first two places as it is able to manage the images referenced in the CSSes and to manage fonts (as they are handled as CSSes are).

The third group of images, the ones in the cloud, have not to be managed by Webpack but by another bundle like LiipImagineBundle.

So we are left only with the fourth group: the static images.

Which is the problem with static images

When you want to use an image in your Twig templates (or any other asset) you are required to use the asset() function.

Something like this:

{# templates/stylesheets.html.twig #}
{% block stylesheets %}
     <link href="{{ asset('build/app_global.css') }}" rel="stylesheet" />
{% endblock %}

As you can see, the path references the build folder: its full path is public/build and it is created by Webpack Encore when running node_modules/.bin/encore dev (or simply yarn dev if you have the script set in your package.json).

This command does some simple things:

  1. Reads the webpack.config.js file;
  2. Combines all css and scss into one file and puts it in the public/build folder;
  3. Finds all the images referenced in the css and copies them in the folder public/build/path/to/image;
  4. Eventually minifies the generated css (if you configured the minification);
  5. Combines all js files into one file;
  6. Eventually minifies the generated js (if you configured the minification);

Then you can include the js and css files using the asset() function as shown above.

But running node_modules/.bin/encore dev doesn’t copy the images referenced in the Twig templates through the asset() function.

For instance, if you have a code like this, the images will not be copied in the build/images/privacy folder:

<div class="privacy text-center">
    <span ...>
        <img src="{{ asset('build/images/privacy/noSpam.png') }}" ... />
    </span>
</div>

And this is a problem as we practically cannot reference them through the asset() function!

What the Symfony Best Practices tell about managing static images with Webpack Encore

Nothing! The best practices don’t mention at all the static images, nor they have a section dedicated to them.

Better, they tell this:

Web assets are things like CSS, JavaScript and image files that make the frontend of your site look and work great.

So, they reference to the Symfony’s Webpack Encore documentation.

But this documentation doesn’t mention at all the handling of images: only the handling of CSS and JavaScript.

So, basically, in this moment there is no best practice in place to manage static images nor a documentation that explains how to manage them.

But static images are a big part of a web project as not all images have to be referenced in CSS nor have to be in a font or in the cloud.

We need a solution to manage static images with Symfony’s Webpack Encore!

So, here it is: lets see it!

How to manage static image files with Symfony’s Webpack Encore

Fortunately the Symfony’s community is very large and active, so, sifting both StackOverflow and the Symfony’s issues on GitHub it is possible to reconstruct a solution to manage static images with Webpack.

The two main discussion about this problem are these:

  1. Missing assets in Twig autocompletation when using manifest.json and Webpack-Encore versioning (Haehnchen/idea-php-symfony2-plugin issues page);
  2. How are static assets handled? (symfony/webpack-encore issues page);

In these discussion there is the full solution to manage static images with Symfony’s Webpack Encore.

Lets see them one by one.

The first (ugly and messy) approach: putting all images directly in the templates/images folder

The simplest way to make the asset() function is to simply put the static images directly in the public/build/images folder: this way the function is able to find them and reference them.

But this approach, although very simple, has some major drawbacks:

  1. You have your assets in two different places: in the assets folder and directly in the public folder (and also in the templates folder for twig templates: three different places!)
  2. You cannot use versioning as Webpack Encore is not aware of these images and so cannot put them in the manifest.json file (more about soon).

So, if you don’t use versioning and you are ok using a messy approach, go this way and put your static images directly in the public/build/images folder.

I don’t like messy things and I want to use versioning, so I want a more clear and organized way.

The second (tiring) approach: requiring images through require

The basic concept here is that to make Webpack aware of images.

So, basically, we have to require all the images using require in our JavaScript files: this way Webpack can move them to the public/build/images folder.

Something like this:

// assets/images.js
require('./images/privacy/noSpam.png');

Then you have to import this file in one of your already existent files so it can be processed by Webpack Encore:

// assets/js/_main.js
require('../../images');

This way you will have the file noSpam.png copied in public/build/images/noSpam.d47c971d.png .

As you can see it was versioned and it was also added to the manifest.json file:

{
  ...
  "build/images/noSpam.png": "/build/images/noSpam.d47c971d.png",
  ...
}

This approach was suggested by Ryan Weaver, the author of Webpack Encore, and is already a solution but its major drawback is that we have to create a file to require images and also require any image we want to use in our Twig templates: this means extra work and also an error prone one as we may forget to add the image, or, anyway, we have to explain to anyone who comes working on the project that (s)he has to add the images to the file, and we the file name changes we have to update it in the images.js file too, and if we decide to add 10 more images we have to require each one of them, and if…

Definitely, this is a solution, but not a good one: we need something better.

The third (ÐΞV’s way) approach: introducing require.context

What we want is to simply put all the images we need into a folder and then let Webpack Encore do the rest, without any further action on our side.

So we need a way to make Webpack Encore able to

  1. Scan a folder recursively;
  2. Find the image files;
  3. Move them to the public/images folder;
  4. Add them to the manifest.json file.

But, how can we achieve this?

The solution is require.context as suggested by Vincent Le Biannic (aka Lyrkan).

The code is this:

// assets/js/_main.js
const imagesContext = require.context('../images', true, /\.(png|jpg|jpeg|gif|ico|svg|webp)$/);
imagesContext.keys().forEach(imagesContext);

The require.context call creates a custom context in Webpack:

  1. The first argument is the folder to scan;
  2. The second argument indicates to scan also the subfolders;
  3. The third argument is a regular expression used to match the file names we want to include in the context.

Then the imagesContext.keys().forEach cycle through the result and require each found element.

Really simple!

The result is that:

  1. All found images are copied in the path public/build/images/ folder (in a flatten way, without taking care of the original subfolder: it is not included in the resulting path);
  2. All fund images are also added to the manifest.json file.

This is the resulting manifest.json file (at least a small portion of it 😅):

{
  ...
  "build/images/NoAdvertisers.png": "/build/images/NoAdvertisers.97d35611.png",
  "build/images/Unsubscribe.png": "/build/images/Unsubscribe.c63a3aac.png",
  "build/images/noSold.png": "/build/images/noSold.2b558443.png",
  "build/images/noSpam.png": "/build/images/noSpam.d47c971d.png",
  "build/images/onlyIntendedUse.png": "/build/images/onlyIntendedUse.ac6db9bf.png",
  "build/images/timeIndefinite.png": "/build/images/timeIndefinite.45f77ed9.png",
  ...
}

This solution is very close to what we need but it has a (maybe minor) drawback, too: the files are copied all into the public/images folder, without reflecting the full path: this maybe an issue if you have two or more images with the same name in different folders and you are not using the versioning (enabling the versioning, anyway, solves the problem).

But also if you can solve the “same name” problem, remains the fact that having the same folder structure maybe helpful to find on the fly the image in the assets folder: not fundamental, but certainly useful.

So, a bit of syntactic sugar to reflect the folder structure is this:

// webpack.config.js
const Encore = require('@symfony/webpack-encore');

Encore
 // directory where all compiled assets will be stored
 .setOutputPath('public/build/')

 // Relative to your project's document root dir
 .setPublicPath('/build')

 // empty the outputPath dir before each build
 .cleanupOutputBeforeBuild()

 // Here all other required configurations
 ...

 .configureFilenames({
 images: '[path][name].[hash:8].[ext]',
 })
;

Doing this will reflect in public/build the folder structure of assets producing something like public > build > assets > images > sub_folder > you_image.png.

As you can see it anyway include the folder asset but removing it requires too much work and the effort is not worth the benefit.

Conclusion

We have seen how to manage static images with Webpack in a convenient way.

We have explored three ways of doing it:

  1. hand-putting the images directly in the public/build folder (messy);
  2. Using require for each image we want to be moved in public/build and versioned (tired);
  3. Using require.context and a little bit of syntactic sugar in webpack.config.js (the ÐΞV’s way).

Which one of these solutions will you choose? 🤔

Mmm, I think I have no doubts about! 🤫 … 😂

AerendirHow to manage static images with Symfony’s Webpack Encore
Share this post

Join the conversation

Related Posts