Any web project has basically 4 kind of places where images reside:
- In the CSS;
- In a font file (think at icons);
- In the cloud (think at AWS S3);
- 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:
- Reads the
webpack.config.js
file; - Combines all
css
andscss
into one file and puts it in thepublic/build
folder; - Finds all the images referenced in the
css
and copies them in the folderpublic/build/path/to/image
; - Eventually minifies the generated
css
(if you configured the minification); - Combines all
js
files into one file; - 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:
- Missing assets in Twig autocompletation when using manifest.json and Webpack-Encore versioning (Haehnchen/idea-php-symfony2-plugin issues page);
- 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:
- You have your assets in two different places: in the
assets
folder and directly in thepublic
folder (and also in thetemplates
folder for twig templates: three different places!) - 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
- Scan a folder recursively;
- Find the image files;
- Move them to the
public/images
folder; - 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:
- The first argument is the folder to scan;
- The second argument indicates to scan also the subfolders;
- 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:
- 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); - 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:
- hand-putting the images directly in the
public/build
folder (messy); - Using
require
for each image we want to be moved inpublic/build
and versioned (tired); - Using
require.context
and a little bit of syntactic sugar inwebpack.config.js
(the ÐΞV’s way).
Which one of these solutions will you choose? ?
Mmm, I think I have no doubts about! ? … ?
Remember to “Make. Ideas. Happen.”.
I wish you flocking users, see you soon!
tvitas says
Great way. Maybe You know how to tell to webpack encore, that we are in the aliased web server’s directory – something like setPublicPath(‘/alias/build’)?
tvitas says
Sorry, found on symfony web 🙂
+ // this is your *true* public path
+ .setPublicPath(‘/myAppSubdir/build’)
+ // this is now needed so that your manifest.json keys are still `build/foo.js`
+ // i.e. you won’t need to change anything in your Symfony app
+ .setManifestKeyPrefix(‘build’)
Aerendir says
Great! 🙂
David says
Super!!! one question, how can add to file loader webm, mp4 and pdf extensions??
Aerendir says
You have to add the relevant file extensions to the `request.context` regex.
Seyed Kalantarian says
hi dear
in last solution how should we define our images in html
tnx
Babak Bandpey says
Perfect. Thanks
Jonathan Nieto says
To remove the “assets/” use [folder]:
.configureFilenames({
images: ‘images/[folder]/[name]-[hash:8].[ext]’,
})
Aerendir says
I’ll try this solution ASAP: thank you for sharing it!
Bogdan says
Thanks for this sharing mate. ! 🙂
Aerendir says
Glad to be helpful 🙂
Kilian says
Thanks for this, very clean and very efficient 😉