Tag: Give

Give your Eleventy Site Superpowers with Environment Variables

Eleventy is increasing in popularity because it allows us to create nice, simple websites, but also — because it’s so developer-friendly. We can build large-scale, complex projects with it, too. In this tutorial we’re going to demonstrate that expansive capability by putting a together a powerful and human-friendly environment variable solution.

What are environment variables?

Environment variables are handy variables/configuration values that are defined within the environment that your code finds itself in.

For example, say you have a WordPress site: you’re probably going to want to connect to one database on your live site and a different one for your staging and local sites. We can hard-code these values in wp-config.php but a good way of keeping the connection details a secret and making it easier to keep your code in source control, such as Git, is defining these away from your code.

Here’s a standard-edition WordPress wp-config.php snippet with hardcoded values:

<?php   define( 'DB_NAME', 'my_cool_db' ); define( 'DB_USER', 'root' ); define( 'DB_PASSWORD', 'root' ); define( 'DB_HOST', 'localhost' );

Using the same example of a wp-config.php file, we can introduce a tool like phpdotenv and change it to something like this instead, and define the values away from the code:

<?php   $ dotenv = DotenvDotenv::createImmutable(__DIR__); $ dotenv->load();  define( 'DB_NAME', $ _ENV['DB_NAME'] ); define( 'DB_USER', $ _ENV['DB_USER'] ); define( 'DB_PASSWORD', $ _ENV['DB_PASSWORD'] ); define( 'DB_HOST', $ _ENV['DB_HOST'] );

A way to define these environment variable values is by using a .env file, which is a text file that is commonly ignored by source control.

Example of a dot env file showing variables for a node environment, port, API key and API URL.

We then scoop up those values — which might be unavailable to your code by default, using a tool such as dotenv or phpdotenv. Tools like dotenv are super useful because you could define these variables in an .env file, a Docker script or deploy script and it’ll just work — which is my favorite type of tool!

The reason we tend to ignore these in source control (via .gitignore) is because they often contain secret keys or database connection information. Ideally, you want to keep that away from any remote repository, such as GitHub, to keep details as safe as possible.

Getting started

For this tutorial, I’ve made some starter files to save us all a bit of time. It’s a base, bare-bones Eleventy site with all of the boring bits done for us.

Step one of this tutorial is to download the starter files and unzip them wherever you want to work with them. Once the files are unzipped, open up the folder in your terminal and run npm install. Once you’ve done that, run npm start. When you open your browser at http://localhost:8080, it should look like this:

A default experience of standard HTML content running in localhost with basic styling

Also, while we’re setting up: create a new, empty file called .env and add it to the root of your base files folder.

Creating a friendly interface

Environment variables are often really shouty, because we use all caps, which can get irritating. What I prefer to do is create a JavaScript interface that consumes these values and exports them as something human-friendly and namespaced, so you know just by looking at the code that you’re using environment variables.

Let’s take a value like HELLO=hi there, which might be defined in our .env file. To access this, we use process.env.HELLO, which after a few calls, gets a bit tiresome. What if that value is not defined, either? It’s handy to provide a fallback for these scenarios. Using a JavaScript setup, we can do this sort of thing:

require('dotenv').config();  module.exports = {   hello: process.env.HELLO || 'Hello not set, but hi, anyway 👋' };

What we are doing here is looking for that environment variable and setting a default value, if needed, using the OR operator (||) to return a value if it’s not defined. Then, in our templates, we can do {{ env.hello }}.

Now that we know how this technique works, let’s make it happen. In our starter files folder, there is a directory called src/_data with an empty env.js file in it. Open it up and add the following code to it:

require('dotenv').config();  module.exports = {   otherSiteUrl:     process.env.OTHER_SITE_URL || 'https://eleventy-env-vars-private.netlify.app',   hello: process.env.HELLO || 'Hello not set, but hi, anyway 👋'   };

Because our data file is called env.js, we can access it in our templates with the env prefix. If we wanted our environment variables to be prefixed with environment, we would change the name of our data file to environment.js . You can read more on the Eleventy documentation.

We’ve got our hello value here and also an otherSiteUrl value which we use to allow people to see the different versions of our site, based on their environment variable configs. This setup uses Eleventy JavaScript Data Files which allow us to run JavaScript and return the output as static data. They even support asynchronous code! These JavaScript Data Files are probably my favorite Eleventy feature.

Now that we have this JavaScript interface set up, let’s head over to our content and implement some variables. Open up src/index.md and at the bottom of the file, add the following:

Here’s an example: The environment variable, HELLO is currently: “{{ env.hello }}”. This is called with {% raw %}{{ env.hello }}{% endraw %}. 

Pretty cool, right? We can use these variables right in our content with Eleventy! Now, when you define or change the value of HELLO in your .env file and restart the npm start task, you’ll see the content update.

Your site should look like this now:

The same page as before with the addition of content which is using environment variables

You might be wondering what the heck {% raw %} is. It’s a Nunjucks tag that allows you to define areas that it should ignore. Without it, Nunjucks would try to evaluate the example {{ env.hello }} part.

Modifying image base paths

That first example we did was cool, but let’s really start exploring how this approach can be useful. Often, you will want your production images to be fronted-up with some sort of CDN, but you’ll probably also want your images running locally when you are developing your site. What this means is that to help with performance and varied image format support, we often use a CDN to serve up our images for us and these CDNs will often serve images directly from your site, such as from your /images folder. This is exactly what I do on Piccalilli with ImgIX, but these CDNs don’t have access to the local version of the site. So, being able to switch between CDN and local images is handy.

The solution to this problem is almost trivial with environment variables — especially with Eleventy and dotenv, because if the environment variables are not defined at the point of usage, no errors are thrown.

Open up src/_data/env.js and add the following properties to the object:

imageBase: process.env.IMAGE_BASE || '/images/', imageProps: process.env.IMAGE_PROPS,

We’re using a default value for imageBase of /images/ so that if IMAGE_BASE is not defined, our local images can be found. We don’t do the same for imageProps because they can be empty unless we need them.

Open up _includes/base.njk and, after the <h1>{{ title }}</h1> bit, add the following:

<img src="https://assets.codepen.io/174183/mountains.jpg?width=1275&height=805&format=auto&quality=70" alt="Some lush mountains at sunset" /> 

By default, this will load /images/mountains.jpg. Cool! Now, open up the .env file and add the following to it:

IMAGE_BASE=https://assets.codepen.io/174183/ IMAGE_PROPS=?width=1275&height=805&format=auto&quality=70

If you stop Eleventy (Ctrl+C in terminal) and then run npm start again, then view source in your browser, the rendered image should look like this:

<img src="https://assets.codepen.io/174183/mountains.jpg?width=1275&height=805&format=auto&quality=70" alt="Some lush mountains at sunset" />

This means we can leverage the CodePen asset optimizations only when we need them.

Powering private and premium content with Eleventy

We can also use environment variables to conditionally render content, based on a mode, such as private mode. This is an important capability for me, personally, because I have an Eleventy Course, and CSS book, both powered by Eleventy that only show premium content to those who have paid for it. There’s all-sorts of tech magic happening behind the scenes with Service Workers and APIs, but core to it all is that content can be conditionally rendered based on env.mode in our JavaScript interface.

Let’s add that to our example now. Open up src/_data/env.js and add the following to the object:

mode: process.env.MODE || 'public'

This setup means that by default, the mode is public. Now, open up src/index.md and add the following to the bottom of the file:

{% if env.mode === 'private' %}  ## This is secret content that only shows if we’re in private mode.  This is called with {% raw %}`{{ env.mode }}`{% endraw %}. This is great for doing special private builds of the site for people that pay for content, for example.  {% endif %}

If you refresh your local version, you won’t be able to see that content that we just added. This is working perfectly for us — especially because we want to protect it. So now, let’s show it, using environment variables. Open up .env and add the following to it:

MODE=private

Now, restart Eleventy and reload the site. You should now see something like this:

The same page, with the mountain image and now some added private content

You can run this conditional rendering within the template too. For example, you could make all of the page content private and render a paywall instead. An example of that is if you go to my course without a license, you will be presented with a call to action to buy it:

A paywall that encourages the person to buy the content while blocking it

Fun mode

This has hopefully been really useful content for you so far, so let’s expand on what we’ve learned and have some fun with it!

I want to finish by making a “fun mode” which completely alters the design to something more… fun. Open up src/_includes/base.njk, and just before the closing </head> tag, add the following:

{% if env.funMode %}   <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lobster&display=swap" />   <style>     body {       font-family: 'Comic Sans MS', cursive;       background: #fc427b;       color: #391129;     }     h1,     .fun {       font-family: 'Lobster';     }     .fun {       font-size: 2rem;       max-width: 40rem;       margin: 0 auto 3rem auto;       background: #feb7cd;       border: 2px dotted #fea47f;       padding: 2rem;       text-align: center;     }   </style> {% endif %}

This snippet is looking to see if our funMode environment variable is true and if it is, it’s adding some “fun” CSS.

Still in base.njk, just before the opening <article> tag, add the following code:

{% if env.funMode %}   <div class="fun">     <p>🎉 <strong>Fun mode enabled!</strong> 🎉</p>   </div> {% endif %}

This code is using the same logic and rendering a fun banner if funMode is true. Let’s create our environment variable interface for that now. Open up src/_data/env.js and add the following to the exported object:

funMode: process.env.FUN_MODE

If funMode is not defined, it will act as false, because undefined is a falsy value.

Next, open up your .env file and add the following to it:

FUN_MODE=true

Now, restart the Eleventy task and reload your browser. It should look like this:

The main site we’re working on but now, it’s bright pink with Lobster and Comic Sans fonts

Pretty loud, huh?! Even though this design looks pretty awful (read: rad), I hope it demonstrates how much you can change with this environment setup.

Wrapping up

We’ve created three versions of the same site, running the same code to see all the differences:

  1. Standard site
  2. Private content visible
  3. Fun mode

All of these sites are powered by identical code with the only difference between each site being some environment variables which, for this example, I have defined in my Netlify dashboard.

I hope that this technique will open up all sorts of possibilities for you, using the best static site generator, Eleventy!


The post Give your Eleventy Site Superpowers with Environment Variables appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,

Give Users Control: The Media Session API

Here’s a scenario. You start a banging Kendrick Lamar track in one of your many open browser tabs. You’re loving it, but someone walks into your space and you need to pause it. Which tab is it? Browsers try to help with that a little bit. You can probably mute the entire system audio. But wouldn’t it be nice to actually have control over the audio playback without necessarily needing to find your way back to that tab?

The Media Session API makes this possible. It gives media playback access to the user outside of the browser tab where it is playing. If implemented, it will be available in various places on the device, including:

  • the notifications area on many mobile devices,
  • on other wearables, and
  • the media hub area of many desktop devices.

In addition, the Media Session API allows us to control media playback with media keys and voice assistants like Siri, Google Assistant, Bixby, or Alexa.

The Media Session API

The Media Session API mainly consists of the two following interfaces:

  • MediaMetadata
  • MediaSession

The MediaMetadata interface is what provides data about the playing media. It is responsible for letting us know the media’s title, album, artwork and artist (which is Kendrick Lamar in this example). The MediaSession interface is what is responsible for the media playback functionality.

Before we take a deep dive into the topic, we would have to take note of feature detection. It is good practice to check if a browser supports a feature before implementing it. To check if a browser supports the Media Session API, we would have to include the following in our JavaScript file:

if ('mediaSession' in navigator) {   // Our media session api that lets us seek to the beginning of Kendrick Lamar's &quot;Alright&quot; }

The MediaMetadata interface

The constructor, MediaMetadata.MediaMetadata() creates a new MediaMetadata object. After creating it, we can add the following properties:

  • MediaMetadata.title sets or gets the title of the media playing.
  • MediaMetadata.artist sets or gets the name of the artist or group of the media playing.
  • MediaMetadata.album sets or gets the name of the album containing the media playing.
  • MediaMetadata.artwork sets or gets the array of images related with the media playing.

The value of the artwork property of the MediaMetadata object is an array of MediaImage objects. A MediaImage object contains details describing an image associated with the media. The objects have the three following properties:

  • src: the URL of the image
  • sizes: indicates the size of the image so one image does not have to be scaled
  • type: the MIME type of the image

Let’s create a MediaMetadata object for Kendrick Lamar’s “Alright” off his To Pimp a Butterfly album.

if ('mediaSession' in navigator) {   navigator.mediaSession.metadata = new MediaMetadata({     title: 'Alright',     artist: 'Kendrick Lamar',     album: 'To Pimp A Butterfly',     artwork: [       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },       // More sizes, like 192x192, 256x256, 384x384, and 512x512     ]   }); }

The MediaSession interface

As stated earlier, this is what lets the user control the playback of the media. We can perform the following actions on the playing media through this interface:

  • play: play the media
  • pause: pause the media
  • previoustrack: switch to the previous track
  • nexttrack: switch to the next track
  • seekbackward: seek backward from the current position, by a few seconds
  • seekforward: seek forward from the current position, by a few seconds
  • seekto: seek to a specified time from the current position
  • stop: stop media playback
  • skipad: skip past the advertisement playing, if any

The MediaSessionAction enumerated type makes these actions available as string types. To support any of these actions, we have to use the MediaSession’s setActionHandler() method to define a handler for that action. The method takes the action, and a callback that is called when the user invokes the action. Let us take a not-too-deep dive to understand it better.

To set handlers for the play and pause actions, we include the following in our JavaScript file:

let alright = new HTMLAudioElement();  if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('play', () => {     alright.play();   });   navigator.mediaSession.setActionHandler('pause', () => {     alright.pause();   }); }

Here we set the track to play when the user plays it and pause when the user pauses it through the media interface.

For the previoustrack and nexttrack actions, we include the following:

let u = new HTMLAudioElement(); let forSaleInterlude = new HTMLAudioElement();  if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('previoustrack', () => {     u.play();   });   navigator.mediaSession.setActionHandler('nexttrack', () => {     forSaleInterlude.play();   }); }

This might not completely be self-explanatory if you are not much of a Kendrick Lamar fan but hopefully, you get the gist. When the user wants to play the previous track, we set the previous track to play. When it is the next track, it is the next track.

To implement the seekbackward and seekforward actions, we include the following:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('seekbackward', (details) => {     alright.currentTime = alright.currentTime - (details.seekOffset || 10);   });   navigator.mediaSession.setActionHandler('seekforward', (details) => {     alright.currentTime = alright.currentTime + (details.seekOffset || 10);   }); }

Given that I don’t consider any of this self-explanatory, I would like to give a concise explanation about the seekbackward and seekforward actions. The handlers for both actions, seekbackward and seekforward, are fired, as their names imply, when the user wants to seek backward or forward by a few number of seconds. The MediaSessionActionDetails dictionary provides us the “few number of seconds” in a property, seekOffset. However, the seekOffset property is not always present because not all user agents act the same way. When it is not present, we should set the track to seek backward or forward by a “few number of seconds” that makes sense to us. Hence, we use 10 seconds because it is quite a few. In a nutshell, we set the track to seek by seekOffset seconds if it is provided. If it is not provided, we seek by 10 seconds.

To add the seekto functionality to our Media Session API, we include the following snippet:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('seekto', (details) => {     if (details.fastSeek && 'fastSeek' in alright) {       alright.fastSeek(details.seekTime);       return;     }     alright.currentTime = details.seekTime;   }); }

Here, the MediaSessionActionDetails dictionary provides the fastSeek and seekTime properties. fastSeek is basically seek performed rapidly (like fast-forwarding or rewinding) while seekTime is the time the track should seek to. While fastSeek is an optional property, the MediaSessionActionDetails dictionary always provides the seekTime property for the seekto action handler. So fundamentally, we set the track to fastSeek to the seekTime when the property is available and the user fast seeks, while we just set it to the seekTime when the user just seeks to a specified time.

Although I wouldn’t know why one would want to stop a Kendrick song, it won’t hurt to describe the stop action handler of the MediaSession interface:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('stop', () => {     alright.pause();     alright.currentTime = 0;   }); } 

The user invokes the skipad (as in, “skip ad” rather than “ski pad”) action handler when an advertisement is playing and they want to skip it so they can continue listening to Kendrick Lamar’s “Alright track. If I’m being honest, the complete details of the skipad action handler is out of the scope of my “Media Session API” understanding. Hence, you should probably look that up on your own after reading this article, if you actually want to implement it.

Wrapping up

We should take note of something. Whenever the user plays the track, seeks, or changes the playback rate, we are supposed to update the position state on the interface provided by the Media Session API. What we use to implement this is the setPositionState() method of the mediaSession object, as in the following:

if ('mediaSession' in navigator) {   navigator.mediaSession.setPositionState({     duration: alright.duration,     playbackRate: alright.playbackRate,     position: alright.currentTime   }); }

In addition, I would like to remind you that not all browsers of the users would support all the actions. Therefore, it is recommended to set the action handlers in a try...catch block, as in the following:

const actionsAndHandlers = [   ['play', () => { /*...*/ }],   ['pause', () => { /*...*/ }],   ['previoustrack', () => { /*...*/ }],   ['nexttrack', () => { /*...*/ }],   ['seekbackward', (details) => { /*...*/ }],   ['seekforward', (details) => { /*...*/ }],   ['seekto', (details) => { /*...*/ }],   ['stop', () => { /*...*/ }] ]   for (const [action, handler] of actionsAndHandlers) {   try {     navigator.mediaSession.setActionHandler(action, handler);   } catch (error) {     console.log(`The media session action, $ {action}, is not supported`);   } }

Putting everything we have done, we would have the following:

let alright = new HTMLAudioElement(); let u = new HTMLAudioElement(); let forSaleInterlude = new HTMLAudioElement();  const updatePositionState = () => {   navigator.mediaSession.setPositionState({     duration: alright.duration,     playbackRate: alright.playbackRate,     position: alright.currentTime   }); }   const actionsAndHandlers = [   ['play', () => {     alright.play();     updatePositionState();   }],   ['pause', () => { alright.pause(); }],   ['previoustrack', () => { u.play(); }],   ['nexttrack', () => { forSaleInterlude.play(); }],   ['seekbackward', (details) => {     alright.currentTime = alright.currentTime - (details.seekOffset || 10);     updatePositionState();   }],   ['seekforward', (details) => {     alright.currentTime = alright.currentTime + (details.seekOffset || 10);     updatePositionState();   }],   ['seekto', (details) => {     if (details.fastSeek && 'fastSeek' in alright) {       alright.fastSeek(details.seekTime);       updatePositionState();       return;     }     alright.currentTime = details.seekTime;     updatePositionState();   }],   ['stop', () => {     alright.pause();     alright.currentTime = 0;   }], ]   if ( 'mediaSession' in navigator ) {   navigator.mediaSession.metadata = new MediaMetadata({     title: 'Alright',     artist: 'Kendrick Lamar',     album: 'To Pimp A Butterfly',     artwork: [       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },       // More sizes, like 192x192, 256x256, 384x384, and 512x512     ]   });     for (const [action, handler] of actionsAndHandlers) {     try {       navigator.mediaSession.setActionHandler(action, handler);     } catch (error) {       console.log(`The media session action, $ {action}, is not supported`);     }   } }

Here’s a demo of the API:

I implemented six of the actions. Feel free to try the rest during your leisure.

If you view the Pen on your mobile device, notice how it appears on your notification area.

If your smart watch is paired to your device, take a sneak peek at it.

If you view the Pen on Chrome on desktop, navigate to the media hub and play with the media buttons there. The demo even has multiple tracks, so you experiment moving forward/back through tracks.

If you made it this far (or not), thanks for reading and please, on the next app you create with media functionality, implement this API.


The post Give Users Control: The Media Session API appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

WordPress Plugin Overload? Give Jetpack a Try!

The WordPress ecosystem has a plentiful supply of plugins that offer everything from AMP to Zapier integration and so, so, so many other things in between. It’s a significant contributor to what makes WordPress great because plugins can account for the needs of nearly any website.

How many plugins are installed on your WordPress site? Five? Fifteen? Fifty? We’ve been up to 31 plugins here at CSS-Tricks and we rely on them for everything from content delivery to performance. Why rebuild the wheel if you don’t have to right? It’s simply too easy and convenient to reach for something someone else has already created (and for free).

Whether you have a handful or a truckload of plugins, you know that each one adds a little bit of complexity to your site, especially when it comes to maintenance and updates. It’s not uncommon to see that little badge at the top of the WordPress admin bar displaying a number of available plugin updates that are ready to install. The problem is that it can happen a lot and staying on top of updates turns almost into a game of Whack-a-Mole that not only eats up time, but risks conflicts on your site that could break things. Plugins are great! But mashing up code from third-party authors is always a (often slight) possibility.

One way to cut down on the number of plugins: Jetpack. It’s a single plugin that provides the features and functionality of dozens. the number of plugins on CSS-Tricks would likely be much higher if we weren’t relying on it for blocking spam comments, security scans, search, social sharing, post subscriptions, and displaying related posts… just to name a few. Jetpack is literally capable of so much that’s hard to contain in a single post. We’ve outlined our favorites, though. And we can’t recommend it enough.

Try Jetpack on your WordPress site to see all the ways it can help you.

The post WordPress Plugin Overload? Give Jetpack a Try! appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

Let’s Give Grunt Tasks the Marie Kondo Organization Treatment

We live in an era of webpack and npm scripts. Good or bad, they took the lead for bundling and task running, along with bits of Rollup, JSPM and Gulp. But let’s face it. Some of your older projects are still using good ol’ Grunt. While it no longer glimmers as brightly, it does the job well so there’s little reason to touch it.

Though, from time to time, you wonder if there’s a way to make those projects better, right? Then start from “Organizing Your Grunt Tasks” article and come back. I’ll wait. That’ll set the stage for this post and then we’ll take it further together to create a solid organization of Grunt tasks.

Automatic Speed Daemon task loading

It’s no fun writing loading declarations for each task, like this:

grunt.loadNpmTasks('grunt-contrib-clean') grunt.loadNpmTasks('grunt-contrib-watch') grunt.loadNpmTasks('grunt-csso') grunt.loadNpmTasks('grunt-postcss') grunt.loadNpmTasks('grunt-sass') grunt.loadNpmTasks('grunt-uncss')  grunt.initConfig({})

It’s common to use load-grunt-tasks to load all tasks automatically instead. But what if I tell you there is a faster way?

Try jit-grunt! Similar to load-grunt-tasks, but even faster than native grunt.loadNpmTasks.

The difference can be striking, especially in projects with large codebases.

Without jit-grunt

loading tasks     5.7s  ▇▇▇▇▇▇▇▇ 84% assemble:compile  1.1s  ▇▇ 16% Total 6.8s

With jit-grunt

loading tasks     111ms  ▇ 8% loading assemble  221ms  ▇▇ 16% assemble:compile   1.1s  ▇▇▇▇▇▇▇▇ 77% Total 1.4s

1.4 seconds doesn’t really make it a Speed Daemon… so I kinda lied. But still, it’s six times faster than the traditional way! If you’re curious how that’s possible, read about the original issue which led to the creation of jit-grunt.

How is jit-grunt used? First, install:

npm install jit-grunt --save

Then replace all tasks load statements with a single line:

module.exports = function (grunt) {   // Intead of this:   // grunt.loadNpmTasks('grunt-contrib-clean')   // grunt.loadNpmTasks('grunt-contrib-watch')   // grunt.loadNpmTasks('grunt-csso')   // grunt.loadNpmTasks('grunt-postcss')   // grunt.loadNpmTasks('grunt-sass')   // grunt.loadNpmTasks('grunt-uncss')    // Or instead of this, if you've used `load-grunt-tasks`   // require('load-grunt-tasks')(grunt, {   //   scope: ['devDependencies', 'dependencies']    // })    // Use this:   require('jit-grunt')(grunt)    grunt.initConfig({}) }

Done!

Better configs loading

In the last example, we told Grunt how to load tasks itself, but we didn’t quite finish the job. As “Organizing Your Grunt Tasks” suggests, one of the most useful things we’re trying to do here is split up a monolithic Gruntfile into smaller standalone files.

If you read the mentioned article, you’ll know it’s better to move all task configuration into external files. So, instead of a large single gruntfile.js file:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({     clean: {/* task configuration goes here */},     watch: {/* task configuration goes here */},     csso: {/* task configuration goes here */},     postcss: {/* task configuration goes here */},     sass: {/* task configuration goes here */},     uncss: {/* task configuration goes here */}   }) }

We want this:

tasks   ├─ postcss.js   ├─ concat.js   ├─ cssmin.js   ├─ jshint.js   ├─ jsvalidate.js   ├─ uglify.js   ├─ watch.js   └─ sass.js gruntfile.js

But that will force us to load each external configuration into gruntfile.js manually, and that takes time! We need a way to load our configuration files automatically.

We’ll use load-grunt-configs for that purpose. It takes a path, grabs all of the configuration files there and gives us a merged config object which we use for Grunt config initialization.

Here how it works:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    const configs = require('load-grunt-configs')(grunt, {     config: { src: 'tasks/.js' }   })    grunt.initConfig(configs)   grunt.registerTask('default', ['cssmin']) }

Grunt can do the same thing natively! Take a look at grunt.task.loadTasks (or it’s alias grunt.loadTasks).

Use it like this:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({})    // Load all your external configs.   // It's important to use it _after_ Grunt config has been initialized,   // otherwise it will have nothing to work with.   grunt.loadTasks('tasks')    grunt.registerTask('default', ['cssmin']) }

Grunt will automatically load all js or coffee config files from the specified directory. Nice and clean! But, if you’ll try to use it, you’ll notice it does nothing. How is that? We still need to do one more thing.

Let’s look into our gruntfile.js code once again, this time without the comments:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({})    grunt.loadTasks('tasks')    grunt.registerTask('default', ['cssmin']) }

Notice that grunt.loadTasks loads files from tasks directory, but never assigns it to our actual Grunt config.

Compare it with a way load-grunt-configs works:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    // 1. Load configs   const configs = require('load-grunt-configs')(grunt, {     config: { src: 'tasks/.js' }   })    // 2. Assign configs   grunt.initConfig(configs)    grunt.registerTask('default', ['cssmin']) }

We initialize our Grunt config before actually loadings tasks configuration. If you are getting a strong feeling that it will make us end up with empty Grunt config — you’re totally right. You see, on contrary to the load-grunt-configs, grunt.loadTasks just imports files into gruntfile.js. It does nothing more.

Woah! So, how do we make use of it? Let’s explore!

First, create a file inside directory tasks named test.js

module.exports = function () {   console.log("Hi! I'm an external task and I'm taking precious space in your console!") }

Let’s run Grunt now:

$   grunt

We’ll see printed to the console:

> Hi! I'm an external task and I'm taking precious space in your console!

So, upon importing grunt.loadTasks, every function is executed as it loads files. That’s nice, but what’s the use of it for us? We still can’t do a thing we actually want — to configure our tasks.

Hold my beer because there is a way to command Grunt from within external configuration files! Using grunt.loadTasks upon importing provides current Grunt instance as a function first argument and also binds it to this.

So, we can update our Gruntfile:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({     // Add some value to work with     testingValue: 123   })    grunt.loadTasks('tasks')    grunt.registerTask('default', ['cssmin']) }

…and change the external config file tasks/test.js:

// Add `grunt` as first function argument module.exports = function (grunt) {   // Now, use Grunt methods on `grunt` instance   grunt.log.error('I am a Grunt error!')    // Or use them on `this` which does the same   this.log.error('I am a Grunt error too, from the same instance, but from `this`!')    const config = grunt.config.get()    grunt.log.ok('And here goes current config:')   grunt.log.ok(config) }

Now, let’s run Grunt again:

$   grunt

And what we’ll get:

> I am Grunt error! > I am Grunt error too, from the same instance, but from `this`! > And here goes current config: > {     testingValue: 123   }

See how we accessed native Grunt methods from an external file and were even able to retrieve the current Grunt config? Are you thinking about that too? Yeah, the full power of Grunt is already there, right at our fingertips in each file!

If you are wondering why methods inside external files can affect our main Grunt instance, it is because of a referencing. grunt.loadTasks passing this and grunt to our current Grunt instance — not a copy of it. By invoking methods on that reference, we’re able to read and mutate our main Grunt configuration file.

Now, we need to actually configure something! One last thing…

This time, let’s make configuration loading work for real

Alright, we’ve come a long way. Our tasks are loaded automatically and faster. We learned how to load external configs with native Grunt method. But our task configs are still not quite there because they do not end up in Grunt config.

But we’re almost there! We learned that we can use any Grunt instance methods in imported files using grunt.loadTasks. They are available on grunt and this instances.

Among many other methods, there is a precious grunt.config method. It allows us to set a value in an existing Grunt config. The main one, which we initialized in our Gruntfile… remember that one?

What’s important is the way we can define tasks configurations. Exactly what we need!

// tasks/test.js  module.exports = function (grunt) {   grunt.config('csso', {     build: {       files: { 'style.css': 'styles.css' }     }   })    // same as   // this.config('csso', {   //   build: {   //     files: { 'style.css': 'styles.css' }   //   }   // }) }

Now let’s update Gruntfile to log the current config. We need to see what we did, after all:

module.exports = function (grunt) {   require('jit-grunt')(grunt)    grunt.initConfig({     testingValue: 123   })    grunt.loadTasks('tasks')    // Log our current config   console.log(grunt.config())    grunt.registerTask('default', ['cssmin']) }

Run Grunt:

$   grunt

…and here’s what we see:

> {     testingValue: 123,     csso: {       build: {         files: {           'style.css': 'styles.css'         }       }     }   }

grunt.config sets csso value when imported, so the CSSO task is now configured and ready to run when Grunt is invoked. Perfect.

Note that if you used load-grunt-configs previously, you had a code like that, where each file exports a configuration object:

// tasks/grunt-csso.js  module.exports = {   target: {     files: { 'style.css': 'styles.css' }   } }

That needs to be changed to a function, as described above:

// tasks/grunt-csso.js  module.exports = function (grunt) {   grunt.config('csso', {     build: {       files: { 'style.css': 'styles.css' }     }   }) }

OK, one more one more last thing… this time for real!

Taking external config files to the next level

We learned a lot. Load tasks, load external configuration files, define a configuration with Grunt methods… that’s fine, but where’s the profit?

Hold my beer again!

By this time, we’ve externalized all our task configuration files. So, the our project directory looks something like this:

tasks   ├─ grunt-browser-sync.js     ├─ grunt-cache-bust.js   ├─ grunt-contrib-clean.js    ├─ grunt-contrib-copy.js     ├─ grunt-contrib-htmlmin.js      ├─ grunt-contrib-uglify.js   ├─ grunt-contrib-watch.js    ├─ grunt-csso.js     ├─ grunt-nunjucks-2-html.js      ├─ grunt-postcss.js      ├─ grunt-processhtml.js   ├─ grunt-responsive-image.js     ├─ grunt-sass.js     ├─ grunt-shell.js    ├─ grunt-sitemap-xml.js      ├─ grunt-size-report.js      ├─ grunt-spritesmith-map.mustache    ├─ grunt-spritesmith.js      ├─ grunt-standard.js     ├─ grunt-stylelint.js    ├─ grunt-tinypng.js      ├─ grunt-uncss.js    └─ grunt-webfont.js gruntfile.js

That keeps Gruntfile relatively small and things seem to be well organized. But do you get a clear picture of the project just by glancing into this cold and lifeless list of tasks? What actually do they do? What’s the flow?

Can you tell that Sass files are going through grunt-sass, then grunt-postcss:autoprefixer, then grunt-uncss, and finally through grunt-csso? Is it obvious that the clean task is cleaning the CSS or that grunt-spritesmith is generating a Sass file which should be picked up too, as grunt-watch watches over changes?

Seems like things are all over the place. We may have gone too far with externalization!

So, finally… now what if tell you that there’s yet a better way would be group configs… based on features? Instead of a not-so-helpful list of tasks, we’ll get a sensible list of features. How about that?

tasks   ├─ data.js    ├─ fonts.js    ├─ icons.js    ├─ images.js    ├─ misc.js    ├─ scripts.js    ├─ sprites.js    ├─ styles.js    └─ templates.js gruntfile.js

That tells me a story! But how could we do that?

We already learned about grunt.config. And believe it or not, you can use it multiple times in a single external file to configure multiple tasks at once! Let’s see how it works:

// tasks/styles.js  module.exports = function (grunt) {   // Configuring Sass task   grunt.config('sass', {     build: {/* options */}   })      // Configuring PostCSS task   grunt.config('postcss', {     autoprefix: {/* options */}   }) }

One file, multiple configurations. Quite flexible! But there is an issue we missed.

How should we deal with tasks such as grunt-contrib-watch? Its configuration is a whole monolithic thing with definitions for each task that are unable to be split.

// tasks/grunt-contrib-watch.js  module.exports = function (grunt) {   grunt.config('watch', {     sprites: {/* options */},     styles: {/* options */},     templates: {/* options */}   }) }

We can’t simply use grunt.config to set watch configuration in each file, as it will override the same watch configuration in already imported files. And leaving it in a standalone file sounds like a bad option too — after all, we want to keep all related things close.

Fret not! grunt.config.merge to the rescue!

While grunt.config explicitly sets and overrides any existing values in Grunt config, grunt.config.merge recursively merges values with existing values in other Grunt config files giving us a single Grunt config. A simple, but effective way to keep related things together.

An example:

// tasks/styles.js  module.exports = function (grunt) {   grunt.config.merge({     watch: {       templates: {/* options */}     }   }) }
// tasks/templates.js  module.exports = function (grunt) {   grunt.config.merge({     watch: {       styles: {/* options */}     }   }) }

This will produce a single Grunt config:

{   watch: {     styles: {/* options */},     templates: {/* options */}   } }

Just what we needed! Let’s apply this to the real issue — our styles-related configuration files. Replace our three external task files:

tasks   ├─ grunt-sass.js   ├─ grunt-postcss.js      └─ grunt-contrib-watch.js

…with a single tasks/styles.js file that combines them all:

module.exports = function (grunt) {   grunt.config('sass', {     build: {       files: [         {           expand: true,           cwd: 'source/styles',           src: '{,**/}*.scss',           dest: 'build/assets/styles',           ext: '.compiled.css'         }       ]     }   })    grunt.config('postcss', {     autoprefix: {       files: [         {           expand: true,           cwd: 'build/assets/styles',           src: '{,**/}*.compiled.css',           dest: 'build/assets/styles',           ext: '.prefixed.css'         }       ]     }   })    // Note that we need to use `grunt.config.merge` here!   grunt.config.merge({     watch: {       styles: {         files: ['source/styles/{,**/}*.scss'],         tasks: ['sass', 'postcss:autoprefix']       }     }   }) }

Now it’s much easier to tell just by glancing into tasks/styles.js that styles have three related tasks. I’m sure you can imagine extending this concept to other grouped tasks, like all the things you might want to do with scripts, images, or anything else. That gives us a reasonable configuration organization. Finding things will be much easier, trust me.

And that’s it! The whole point of what we learned.

That’s a wrap

Grunt is no longer the new darling it once was when it first hit the scene. But to date, it is a straightforward and reliable tool that does its job well. With proper handling, it gives even fewer reasons to swap it for something newer.

Let’s recap what we can do to organize our tasks efficiently:

  1. Load tasks using jit-grunt instead of load-grunt-tasks. It’s same but insanely faster.
  2. Move specific task configurations out from Gruntfile and into external config files to keep things organized.
  3. Use native grunt.task.loadTasks to load external config files. It’s simple but powerful as it exposes all Grunt capabilities.
  4. Finally, think about a better way to organize your config files! Group them by feature or domain instead of the task itself. Use grunt.config.merge to split complex tasks like watch.

And, for sure, check Grunt documentation. It’s still worth a read after all these years.

If you’d like to see a real-world example, check out Kotsu, a Grunt-based starter kit and static website generator. You’ll find even more tricks in there.

Got better ideas about how to organize Grunt configs even better? Please share them in the comments!

The post Let’s Give Grunt Tasks the Marie Kondo Organization Treatment appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , ,
[Top]