With the recent release of Jekyll 4.0, it was a good time to revisit how I manage my blog’s assets. The web and its tools continue to move in a break-neck speed and new versions like to break things so finding up to date best practices and code samples was a bit difficult. This is one reason why I’m writing this post.
My previous pipeline was built on Ruby’s Rake, so the tasks were defined in a Rakefile
.
- I had previously used Bootstrap as the framework for styling so one task compiled the LESS files.
- Jekyll’s own build process would resize and transform images using bespoke plugins1.
- Another task ran
imageoptim
andpngcrush
on the destination (_site/images
) to optimize the transformations. - Finally, one task deployed the built site using
rsync
.
The tools were varied and scattered all around the place. I wasn’t looking forward to update my plugins to take advantage of the new version of Jekyll2. However, there’s no real disadvantage in using Rake if you’re using Jekyll - both are Ruby so there is not that much new to learn.
So, one option was to build everything in Jekyll and continue use Rake tasks. I had already started to move to use SCSS, which was supported in Jekyll so this was at least in the realm of possibility.
However, why maintain your own tools when there are much better options available3.
Enter Gulp
The other option was to move fully to use a Node based build flow, of which there are many4. For no particular reason I went with Gulp. In fact, I first made sure that Gulp worked with the tools I wanted to use: Sass5, PostCSS and Imagemin.
The general idea of the architecture I was bulding was that
- Tasks are defined in Gulp’s
gulpfile.js
- Gulp compiles and minifes Sass to
_site/css
- Gulp resizes and optimizes from
_images
to_site/images
- Jekyll builds only the HTML content
- Other Rake tasks are ported to a Gulp tasks (like
rsync
)
First thing to note that normally Jekyll destroys the destination folder before building, so here Jekyll’s configuration needs to be updated so that it leaves _site/images
and _site/css
untouched in _config.yml
.
You might also want to exclude build related files from your site.
The whole Gulpfile
Here is the full Gulpfile I’m using to build this site in its Gulp 4.0 style syntax (exported normal functions instead of task()s
).
One could use this in development by calling gulp watch
and gulp serve
(or better yet, make a parallel task of these two). Building for deployment can be achived with gulp build && gulp deploy
(or again, by making an additional task that runs these as a series).
As for the file structure, it’s possible to split tasks to different files, and many examples do this. One neat approach to this is to create a folder called gulpfile.js
and have a main file index.js
and tasks as their own .js
files. I don’t think my file is large or complex enough to warrant this. However, the above file will work identically if saved as gulpfile.js/index.js
.
Another improvement would be to add variables for sources and destinations.
The other tasks are quite self-explanatory but CSS and image handling have a bit more complex flows that I will explain next.
CSS management
In my case, the source files are .css
and .scss
files in /_sass
folder6.
Above, I have added an example (commented out) how to add gulp-sourcemaps
for development purposes.
In my case, my main stylsheet imports from sanitize.css which I bring in as an npm package.
For Sass compiler to find this packaged file, I need to tell it to look for it in node_modules
using includePaths
. For reference, if you are using Jekyll’s built-in Sass compiler, you can achieve similar thing with configuration option:
The above is irrelevant if you are using gulp-sass
to compile your CSS.
Another way to import sanitize.css
(or normalize.css
) file would be to bring it in using Gulp and as the PostCSS plugin postcss-normalize
.
After compilation, the scripts runs the stylesheet through PostCSS plugins. Here through CSS nano for minification7 and Autoprefixer for automatic vendor prefixing of CSS rules8. There are various other PostCSS plugins that you can add to the array, for example the beforementioned postcss-normalize
or stylelint
.
Another aside is that if you’re using Kramdown and Rouge for syntax highlighting, you can get a .css
of Rouge’s themes with a shell command like
Note the scope
parameter, Kramdown’s default HTML converter adds .highlighter-rouge
class which is different from Rouge’s default .highlight
.
Image management
My images in _images
are in various sizes and formats. The above piece of code makes resized versions of the images that fit the layout of my site and optimizes the resulting files with mozjpeg
and pngquant
. Note that both of these are lossy, imagemin
defaults to lossless algorithms.
The resizing and variants are accomplished with gulp-scale-images
. To inject variants to the stream, I use flatMap
(as instructed in gulp-scale-images
documentation). If you only need one version of your images, you can remove the .pipe(flatMap(retinaVersions))
row and the retinaVersions
function.
Note that you will in any case need a function that adds the file.scale
map for gulp-image-scale
to instruct how to process the file. This can be achieved for example using through2
as shown in gulp-scale-images
documentation and clarifying issue.9
Conversely, you can also add further variants for thumbnails and such in the function called by flatMap
. Note that you would need to add support in your Jekyll to actually use the retina or thumbnail variant (through a Liquid filter or other plugin). In my case, my Kramdown converter scans for a retina version and adds a srcset
to the <img>
tag if one is found10.
Conclusion
In the end, what did I actually get from bringing in npm and it’s thousands of packages into my project? Most importantly, I get access to the best practices tools like Sass, PostCSS and Imagemin. I also get a clean separation of duties where I can more easily change from Jekyll to something else if need be.
Everyone’s workflow is a bit different to scratch their own personal itch. The same applies to this. I would recommend to use it as starting point for your own tasks.
It is also possible that by the time you read this, a breaking update has made the syntax or parameters invalid. I recommend checking each plugin’s and tool’s up to date documentation for reference. That is how I ended up with the above, I found outdated snippets from various blogs and attempted to bring them up to date by referencing the source documentation.