Mario Hernandez: Managing image embeds with Drupal media

Allowing your content creators to embed images in text fields is a big risk if you don't have the right measures in place to get properly rendered images without jeopardizing your site's performance. We faced this issue first-hand with embedded images due to not using the right configuration and this lead to extremely large images being rendered. In this post I'll go over the techniques I took for addressing those issues and set up a system for image embeds that is solid and performant.

I started by writing a seven-part guide on how to setup responsive images. In this post I'll focus on image embeds. If you followed or read the responsive images guide, you should be able to take advantage of some of the work we did there in this post. The guidelines covered here include:

  • Defining requirements
  • Image styles
  • Media view modes
  • Text format configuration

Defining requirements

Before you write the first line of code or set the first drupal configuration for this issue, you need to have a clear understanding of your requirements. Here is a summary of my requirements:

  • Only certain user roles can embed images

    This means we need to figure out if the text formats used in our site will allow us to set the restrictions we need. Otherwise we may need to create or edit a text format for our target user roles.

  • Users need to be able to choose the image size and aspect ratio when embedding images

    We defined the image sizes and aspect ratios and assigned names that were user-friendly for non-technical users. We came up with name options we think our users will find easy to work with such as:

    • Small square, Small portrait, Small rectangular
    • Medium square, Medium portrait, Medium rectangular, Medium rectangular wide
    • Large square, Large rectangular, Large rectangular wide
    • Extra large square, Extra large rectangular, Extra large rectangular wide
  • If no option is selected by users, set a default image size

    For the default option when no selection is made by the user, we decided to use the Medium rectangular option. This has an aspect ratio of 3:2 and it measures about 720x480.

  • Existing Media items need to be available for embedding

    This was a tricky one because my original inclination was to create a new Media type so we can isolate all configuration for its view modes and not overpopulate our default Media type. However, this ended up not working for us because when you limit your image embeds to only use a new Media type, you don't get access to any of the media items (images), that have already been uploaded to the Media library using other media types. Ultimately we ended up using Drupal core's Media type, Image, and our dev team had to compromise on having a very busy list of view modes for this media type.

  • Images need the ability to be cropped wihin the Media page

    Since most of our images already provide the ability to be cropped at different aspect ratios, using the core Media type in the previous bullet point made this an easy solution.

Image styles

It all starts with image styles. I'm not going to go over how to create image styles, you can read my post Image styles in Drupal. The one thing I am going to repeat however is the importance of creating reusable image styles. Reusable image styles can help you reduce the number of image styles you create while providing the flexibility you need with each use case.

Image styles are key as each of the size options we defined above translate into image styles. So Small square for example, is an image style that is defined as 1:1 (250px). Medium rectangular would be something like 3:2 (720x480), etc. You may be wondering, how do you plan on using fiendly names for your content editors when your image styles names are not very friendly? Great question. Since we are using Drupal's Media, content editors do not interact directly with image styles, they do with Media view modes and this is where we will use more friendly names.

Media view modes

View modes are one of Drupal's powerful features. Being able to display content is different ways with little effort can turn a simple website into a dynamic content hub. The example I always give when someone asks me what view modes are or how do they work is the Amazon website. When you are viewing a product in amazon.com, or most retail websites for that matter, you will notice that the same product or similar ones appear all over the page but in slightly different ways, with different fields or styles. See the page below for an example.

Image removed.

The image above shows many ways in which a product can be displayed. I've numbered each display.

In Drupal, every entity such as content types, media types, blocks, etc., offer the ability to create view modes. For the purpose of image embeds, we will create a Media type view mode for each image style we plan on using. The view modes is what content editors will interact with when choosing an image size or aspect ratio during the image embed process. This is where we will use the user-friendly names we defined earlier. Let's go over how this relationship between view modes and image styles works for image embeds.

Configure view modes for the Image media type

  1. In your Drupal site, create an image style for each image size option you wish to provide to users when embedding images.

  2. Next, create a Media view mode for each image style (/admin/structure/display-modes/view). Very iimportant: Remember the view mode's label (name) is where we are going to use the friendly name (i.e. Medium rectangular (720x480)). I like to keep the machine name similar to the label so it's easier to debug or identify in code (i.e. medium_rectangular_720x480).

  3. Now, let's tie 1 & 2 together:

    • Go to the media type you plan on using for media embeds (/admin/structure/media/manage/image/display). I am using Drupal core's Image media type.
    • Scroll down and expand the Custom display settings fieldset.
    • Check each of the view modes you created in step 2 and click Save.
  4. Now click each of the view modes and update the image field to use the respective/matching image style.

Configure the text format

View modes and image styles are all configured. Now let's configure the Text format that authorized users will use to embed images.

  1. Go to the Text formats and editors page (/admin/config/content/formats)
  2. Click Configure next to the text format you plan on using (i.e. Full HTML)
  3. Ensure the right user roles are selected
  4. Within the Toolbar configuration section, drag the Drupal media button from the Available buttons options to the Active toolbar section. You could probably remove the original insert image button since you won't be using it.
  5. Scroll to the Enabled filters section and check the Embed media checkbox
  6. Scroll to the Filter settings section and set the following:
    • Default view mode: This is the default display that will be used if content editors don't pick an option when embedding images. Select any of the view modes that represents the image size you want to use as default.

    • Media types selectable in the Media Library: Select the Media type you plan on using. In my case is Image.

    • View modes selectable in the 'Edit media' dialog: Finally, select each of the view modes you created in the previous section. FYI: View modes will be sorted in alpha order by their machine name. In my case I had to prefix some of the machine names with either "a" or "b" so the list of options for the users to choose from would be nicely organized by their label name. See screnshot below.

    • Click Save configuration

Testing your configuration

Now that we've completed all the configuration we should be able to take it for test drive.

  • Go to any page where there is a text field with a WYSIWYG editor
  • Make sure you are using the right text format by selecting it at the bottom of the text field where you want to embed an image
  • Click the Insert media button from the editor's toolbar
  • Select or upload the image you'd like to embed
  • When the image has been inserted, click on it and you should see several options of actions you can do with the image. Things like align the image, add a caption, link it, and you should also see a selection box listing all the view modes you created.
  • After making your selection you should immediately see the image size/aspect ratio change to the one you selected. When you are happy with your selection, click the Save button to save your page.

Important: Depending on your site's configuration, the options for changing your image size may look different than mine. In my case, I am only using Drupal's core modules and this is how the options look for me:

Image removed.

In closing

Putting a system like this for your image embeds will give you the piece of mind that content editors have options to choose how big or small they would like images to be displayed, and from a performance point of view, if your image styles are done properly, you can rest assurred that bloated images will never be rendered because you have put the guard rails in place to avoid this from happening.

I hope you found this article useful and can put these techniques to use in your own Drupal project. Happy New Year! 🎉 🎊 🎆 👋

Mario Hernandez: Drupal base path

Recently I was building a component that required a static image which was not stored in the database but instead needed to be stored somewhere in the file system of the site. There are several ways for serving a static image for example we could have stored the image in the sites/default/files/images/ directory. A very common approach which in many cases would work just fine, however, in my case I was building a component and I wanted for the component image to be located within the same component's directory. This makes sense because if I wanted to reuse or share this component, all component assets would be included in a single directory.

Requirements

My goal with this task was to dynamically point to the image regardless the site this component was running on. Oh yeah, we are running a multi-site architecture with hundreds of sites and a single code base. So this component needed to work in all of these hundreds of sites. Now the challenge seems a little more... "challenging".

¡Manos a la hobra'!'

I started by doing research to determine the best way possible to achieve this. I read about using a pre-process function that would generate a dynamic base path of the site but I was hoping I could keep things simple and do everything on the front-end with only Twig. This would make it a more appealing approach for front-end developers.

After some research, I came across two little gems that became game-chargers for my project. One of these gems is the {{ url('<front>') }} Twig function. This will provide the current site's homepage/base path. The other very handy Twig function is {{ active_theme_path() }} which prints the path of the current active theme (themes/custom/my_theme). While researching for this task, I also found you can use the {{ directory }} Twig variable in your theme's templates to print the active theme's path. A word of coution when using either the {{ active_theme_path() }} function of the {{ directory }} variable as these could have different results depending on your whether you are using them in a base or sub theme. Here's a drupal.org issue that discusses this in more detail.
Armed with these two little functions, and one Twig variable, we can now work in generating a dynamic path to our theme's directory where the static image for our component is located. So this may seem like a simple thing but remember, our component's image should work regardless of the site the component is used on within our multi-site architecture. Some sites even use a different sub-theme but the parent theme is always the same which is where our image is stored.

Building the dynamic path

Before we can use the first function we need to run it through the |render Twig filter. Since Twig will return an array from the {{ url() }} function, we need to convert it to a string because we need the value of the function. Let's take a look:

{{ url('<front>')|render }} # This will give us http://my-site.com/

Next let's work with the theme path function. Similarly to the function above, we will use the |render Twig filter to convert it from an array to a string.

{{ active_theme_path()|render }} # This will give us themes/custom/my-theme

Now that we have two strings we can joint them together to compose the full path to our image:

<img src="{{ url('<front>')|render }}{{ active_theme_path()|render }}/images/image.jpg" alt="alt text" />

If we want to get fancy we could actually set a variable to shorten things a bit:

{% set theme_url = url('<front>')|render ~ active_theme_path()|render %} <img src="{{ theme_url ~ '/images/image.jpg' }}" alt="alt text" />

And there you have it. A dynamic path that will work on any of our sites.

I realized some people reading this already knew all of this but I didn't. So I figured I would share it because I bet there are others out there that also do not know about the {{ url('<front>') }} or {{ active_theme_path() }} Twig functions as well as the {{ directory }} variable. As I said before, there are many ways to handle this challenge, but in my case this is exactly how I wanted to approachh it. I hope this was helpful. Cheers!

Mario Hernandez: Responsive images in Drupal - a guide

Images are an essential part of a website. They enhance the appeal of the site and make the user experience a more pleasant one. The challenge is finding the balance between enhancing the look of your website through the use of images and not jeopardizing performance. In this guide, we'll dig deep into how to find that balance by going over knowledge, techniques and practices that will provide you with a solid understanding of the best way to serve images to your visitors using the latest technologies and taking advantage of the advances of web browsers in recent years.

Hi, I hope you are ready to dig into responsive images. This is a seven-part guide that will cover everything you need to know about responsive images and how to manage them in a Drupal site. Although the excercises in this guide are Drupal-specific, the core principles of responsive images apply to any platform you use to build your sites.

Where do we start?

Choosing Drupal as your CMS is a great place to start. Drupal has always been ahead of the game when it comes to managing images by providing features such as image compression, image styles, responsive images styles and media library to mention a few. All these features, and more, come out of the box in Drupal. In fact, most of what we will cover in this guide will be solely out of the box Drupal features. We may touch on third party or contrib techniques or tools but only to let you know what's available not as a hard requirement for managing images in Drupal.

It is important to become well-versed with the tools available in Drupal for managing images. Only then you will be able to make the most of those tools. Don't worry though, this guide will provide you with a lot of knowledge about all the pieces that take part in building a solid system for managing and serving responsive images.

Let's start by breaking down the topics this guide will cover:

  1. What are responsive images?
  2. Art Direction using the <picture> HTML element
  3. Image resolution switching using srcset and sizes attributes
  4. Image styles and Responsive image styles in Drupal
  5. Responsive images and Media
  6. Responsive images, wrapping up

What are responsive images?

A responsive image is one whose dimensions adjust to changes in screen resolutions. The concept of responsive images is one that developers and designers have been strugling with ever since Ethan Marcotte published his famous blog post, Responsive Web Design, back in 2010 followed by his book of the same title. The concept itself is pretty straight forward, serve the right image to any device type based on various factors such as screen resolution, internet speed, device orientation, viewport size, and others. The technique for achieving this concept is not as easy. I can honestly say that over 10 years after reponsive images were introduced, we are still trying to figure out the best way to render images that are responsive. Read more about responsive images.

So if the concept of responsive images is so simple, why don't we have one standard for effectively implementing it? Well, images are complicated. They bring with them all sorts of issues that can negatively impact a website if not properly handled. Some of these issues include: Resolution, file size or weight, file type, bandwidth demands, browser support, and more.

Some of these issues have been resolved by fast internet speeds available nowadays, better browser support for file tyes such as webp, as well as excellent image compression technologies. However, there are still some issues that will probably never go away and that's what makes this topic so complicated. One issue in particular is using poorly compressed images that are extremely big in file size. Unfortunately often times this is at the hands of people who lack the knowledge of creating images that are light in weight and properly compressed. So it's up to us, developers, to anticipate the problems and proactively address them.

Ways to improve image files for your website

If you are responsible for creating or working with images in an image editor such as Photoshop, Illustrator, GIMP, and others, you have great tools at your disposal to ensure your images are optimized and sized properly. You can play around with the image quality scale as you export your images and ensure they are not bigger than they need to be. There are many other tools that can help you with compression. One little tool I've been using for years is this little app called ImageOptim, which allows you to drop in your images in it and it compresses them saving you some file size and improving compression.

Depending on your requirements and environment, you could also look at using different file types for your images. One highly recommended image type is webp. With the ability to do lossless and lossy compression, webp provides significant improvements in file sizes while still maintaining your images high quality. The browser support for webp is excellent as it is supported by all major browsers, but do some research prior to start using it as there are some hosting platforms that do not support webp.

To give you an example of how good webp is, the image in the header of this blog post was originally exported from Photoshop as a .JPG, which resulted in a 317KB file size. This is not bad at all, but then I ran the image through the ImageOptim app and the file size was reduced to 120KB. That's a 62% file size reduction. Then I exported the same image from Photoshop but this time in .webp format and the file size became 93KB. That's 71% in file size reduction compared to the original JPG version.

A must have CSS rule in your project

By now it should be clear that the goal for serving images on any website is doing it by using the responsive images approach. The way you implement responsive images on your site may vary depending on your platform, available tools, and skillset. Regardless, the following CSS rule should always be available within your project base CSS styles and should apply to all images on your site:

img { display: block; max-width: 100%; }

Easy right? That's it, we're done 😃

The CSS rule above will in fact make your images responsive (images will automatically adapt to the width of their containers/viewport). This rule should be added to your website's base styles so every image in your website becomes responsive by default. However, this should not be the extend of your responsive images solution. Although your images will be responsive with the CSS rule above, this does not address image compression nor optimization and this will result in performance issues if you are dealing with extremly large file sizes. Take a look at this example where the rule above is being used. Resize your browser to any width including super small to simulate a mobile device. Notice how the image automatically adapts to the width of the browser. Here's the problem though, the image in this example measures 5760x3840 pixels and it weights 6.7 MB. This means, even if your browser width is super narrow, and the image is resized to a very small visual size, you are still loading an image that is 6.7 MB in weight. No good 👎

In the next post of this series, we will begin the process of implementing a solution for handling responsive images the right way.

Navigate posts within this series

Mario Hernandez: Art Direction using the picture HTML element

In the previous article of this guide we covered the concept of responsive images and some of the challenges that come with implementing an effective system for them. In this article we will go in detail about the concept of "Art Direction" and how this applies to responsive images.

What is art direction?

In the context of responsive images, art direction is the ability to display differently-cropped images based on the device size. For example, a large landscape shot of a person rowing in the middle of a lake is shown when viewed on a large desktop device. If we were to use the same image on a mobile device, that image would shrunk down, making the person in the image very small and hard to see. A better option would be to show a different version of the image that zooms in and focuses on the most important part of the image, the person rowing. See an example of this image below.

Image removed.

Enter the <picture> HTML element

In order to achieve art direction we need to be able to query for the size of the device being used to view the website. Once we've identified the device size we instruct the browser which image to use based on the device size. This will allow us to provide a better user experience as each device will display an image intended specifically for that device. Going back to the image above, we can see that the main image has been cropped differently to ensure the most important part of the image is displayed on each divice.

So how do we query for the device size and how do we instruct the browser which image to use? This is where the <picture> element/tag comes in. Let's take a look at the code that makes all this possible and break it down.

<picture> <source media="(min-width: 2400px)" srcset="images/rowing-2400.jpg 1x, images/rowing-4800.jpg 2x" type="image/webp"> <source media="(min-width: 1280px)" srcset="images/rowing-1400.jpg 1x, images/rowing-2800.jpg 2x" type="image/webp"> <source media="(min-width: 640px) and (max-width: 1279px)" srcset="images/rowing-1200.jpg 1x, images/rowing-2400.jpg 2x" type="images/webp"> <img src="images/rowing-1200.jpg" srcset="images/rowing-2400.jpg 2x" alt="Person rowing on a lake" width="1200" height="800"> </picture>

Note: The order in which the media queries are written within the <picture> tag matters. The browser will use the first match it finds even if it's not the intended one. Therefore, consider the media query order very carefully to ensure the right image is served.

  • <picture>: The <picture> tag is simply a wrapper. On its own it does not do anything.
  • <source>: The <picture> HTML element contains zero or more <source> elements. The browser will consider each child <source> element and choose the best match among them. If no matches are found—or the browser doesn't support the <picture> element—the URL of the <img> element's src attribute is selected. The selected image is then presented in the space occupied by the <img> element.
  • Within the <source> element, you will find some very handy attributes (media, srcset, and type):
    • media: Rembember earlier we said we need to query for the device size? Well, within the media attribute you can write media queries much like the media queries you write in CSS (media="(min-width: 600px)"). This is how we check the size of the device when a page is rendered.
    • srcset: This attribute allows us to provide a list of images the browser can use when the media query finds a match (srcset="img-768.jpg, img-1440.jpg").
    • type: The type attribute specifies a MIME type for the resource URL(s). This is optional if using common image types such as JPG, PNG, TIFF, etc. If you plan on providing images in different file formats, you can do so using the type attribute. This is handy in the event the browser does not support a specific file type (type="image/avif"), as you can then provide a supported file type.
  • <img>: The img element serves two purposes:
    • It describes the dimensions of the image and its presentation
    • It provides a fallback in case none of the offered <source> elements are able to provide a usable image.

And there you have it. The <picture> element is a great way to serve different images based on things like device size or screen density. When the <picture> element was first introduced it required a pollyfill as not all browsers supported it. Nowadays, unless you are supporting Internet Explorer 11 (sorry bro), all other major browsers provide native support for it. Take a look at the chart below for current browser support.

Image removed.

Great! Let's use <picture> on all our images ...NOOOOOOOO!!!!!!

Say what? If the <picture> element is so great, why can't we use it for rendering all of our images? Well, as great as the <picture> element is, it should not be the default solution for serving responsive images in your site. The only use case for the <picture> element is when you are trying to achieve "Art Direction" (cropping your images differently for each device size).

Remember at the begining of this post when I said "In order to achieve art direction we need to be able to query for the device size. Once we've identified the device size we instruct the browser which image to use..."? There lies the problem. Let me explain.

The issue with the statement above is that "we are telling the browser which image". Not only that, but we are doing so solely based on the size of the device. This may not always be the best way to determine which image a device should use. Imagine you are using a nice relatively new laptop with a super high density screen. Based on our rules established within the <picture> element code snippet above, we would end up with an image that is 4800px in size. This is a pretty large image but it's the one that meets our creteria defined in the media query above. If you're home with a decent wifi connection you will never see any issue loading an image this large, but imagine you are working out of a coffee shop, or at a conference with poor wifi connection, or worse yet, you're on the road using your phone as a hotspot and your signal is very bad, now you will really experience some performance issues because we are telling the browser to load the largest image possible because your computer screen is big (relatively speaking). With the <picture> element we can't check how fast your internet connection is, or whether there are browser preferences a user has configured to account for slow internet speeds. We are basing everything on the size of the device.

Then why use the picture element? Well, when developing a website, the developer does not have all the information they need to serve the best image. Likewise, when rendering a page and using the <picture> tag, the browser does not know everything about the environment. The table below shows this in more detail and exposes the gap between the developer and the browser.

Identifying the gap when using <picture>

Environment conditions What the developer knows
during development What the browser knows
during image rendering Viewport dimensions No Yes Image size relative to the viewport Yes No Screen density No Yes Images dimensions Yes No

You may be wondering: "Why did you get us all excited about the <picture> element if we can't really use it?" well, if you are trying to achieve art direction, then you use the <picture> element. It's the recommended approach for that use case. If you are looking for resolution switching, a use case for most images in the web, you need to use the srcset and sizes attributes approach. In the next post we'll dive deep into this technique.

Navigate posts within this series

Mario Hernandez: Image resolution switching using srcset and sizes attributes

In the previous article we defined what art direction is and how to address it using the <picture> element. In this post, the focus will be how to address responsive images when the requirement is image resolution switching. Resolution switching, in the context of responsive images, is rendering identical image content on all devices. Unlike art direction where each device gets a differently cropped image that may vary on aspect ratio, resolution switching uses images that are simply larger or smaller based on the device but retain the same aspect ratio and cropping settings. Resolution switching is how most images are rendered (the rule), the <picture> element approach is the exception to the rule. Take a look at an example of resolution switching below.

Image removed.

The image above demonstrate how multiple resolutions of the same image can be served to different devices. All the images in the example above are cropped exactly the same maintaining the same aspect ratio from large to small.

Using srcset and sizes attributes

Using the srcset and sizes image attributes is how most images are rendered in the web today. As indicated before, this is the recommended way for configuring responsive images if all you need is to switch resolution of images rather than art direction. So how does this approach work? Let's take a look at a typical configuration of the <img> tag using the image above as an example of the different image sizes we will want the browser to choose from:

<img srcset="original-image.jpg 2400w, extra-large.jpg 2000w, large.jpg 1600w, medium.jpg 1080w, small.jpg 800w, x-small.jpg 500w" sizes="100vw" src="large.jpg" alt="Image of sky shown at different resolutions" />

Let's break things down so we can understand this approach better.

  • <img>: Right off the bat we start by using a widely supported html tag.
  • srcset: The srcset attribute in the img tag serves two important roles, 1) It stores a list of images that can be used by the browser, 2) Each image provides its width value which plays a role on the browser choosing the right image.
  • sizes: The sizes attribute tells the browser the width, in relation to the viewport, the image should be rendered at. The value of 100vw shown above, means the image will be rendered at 100% the viewport width on all the devices. You could also use media queries like (max-width: 720px) 100vw, 50vw. This means that if the device does not exceed 720px in width, the image will be rendered at 100% the viewport width, otherwise (if the device is larger than 720px), the image will be rendered at 50% the viewport width.
  • src: The src attribute is used as a fallback if everything fails.

What does it all mean?

Let me explain things in more detail because it is important we understand how this approach is so much better than using the <picture> element.

The biggest difference/advantage of using srcset and sizes versus <picture>, is the fact that we let the browser decide which image is the best image to render on any device. This is possible thanks to all the information we have supplied to the browser. For example, in the srcset we are not only providing the browser with a list of images to choose from, but we are also telling the browser how big each image is. This is very important because the browser will use this information when choosing the image to render. In the <picture> element approach, the image size descriptors are not available.

The sizes value tells the browser the size the image needs to be rendered at in relation to the viewport. This too is extremely important information we are providing the browser because if the browser knows the dimensions of all the images to choose from and how big/small the image needs to be rendered, then the browser is able to pick the best image possible.

But that's not all, the browser is smarter and knows more about the web environment than we do when a page or image is rendered. For example, the browser knows the viewport width used when viewing a website, it knows how fast/slow your internet connection is, and it knows about any browser preference settings (if any), setup by the user. Using all this information the browser is able to determine which image from the srcset is the best to use. In contrast, with the <picture> element, we tell the browser which image to use solely based on the device size.

Closing the gap

Now let's see how using the srcset and sizes attributes closes the gap we identified when using the <picture> tag.

Environment conditions What the developer knows
during development What the browser knows
during image rendering Viewport dimensions No Yes Image size relative to the viewport Yes No Yes via sizes Screen density No Yes Images dimensions Yes No Yes via srcset

Pretty nice huh? Now thanks to the srcset and sizes attributes we've closed the gap and the browser has all the information it needs to ensure the best image is served to each device.

The next post of this series will focus on image styles. These are fun but can also get you in a lot of trouble if not properly done. See you there.

In closing

Time for a story: I recently did an experiment that 100% proves the use of resolution switching using srcset and sizes attributes. As most people nowadays, I use a very large second display when working on projects to fit more apps and see things better. My second display is nice but it's not a 4K display. It's double the physical size of my mac's screen, but the mac's screen resolution is higher by almost double (twice the number of pixels). When I look at an image of a project where I've implemented the practices in this guide, in the large display, and inspected the page, I see the browser has selected an image that is 720px which makes complete sense for the use case I am testing. I then unplugged the second display and viewed the page on my mac's screen (higher resolution), I reloaded the page and inspected it, I noticed the browser has now selected an image that is double the size of the first image. This is exactly the behavior I would expect because my mac screen is of higher resolution and my connection speed is very fast. So the browser was able to make the smart decision to use a different images based on my environment.

Navigate posts within this series

Mario Hernandez: Image styles in Drupal

Now that we've gone over some very important concepts of responsive images (art direction and resolution switching), it's time to transfer all that knowledge and put it all in practice in Drupal. One of the key pieces for achieving responsive images in Drupal is by using image styles. Image styles are how Drupal manages the way we crop and size images.

What are image styles?

Before we get to building image styles, let's go over what they are. Think of image styles as templates for cropping, scaling, converting, and sizing images. You can use these templates with any image on your site and as many times as you'd like. Thanks to image styles images will always render within the parameters we define.

A more real-world way of looking at image styles may be if you could imagine for a moment you have a couple of picture frames you'd like to use to hang some pictures in your house or office. One frame is 5x7, another is 4x6 and one last one is 8x10. The picture frames are Drupal's image styles.

So we have some picture frames and we have ordered several pictures from our favorite online photo printing service. There is one picture in particular I really love and I want to frame it using the 3 picture frames. So, although it is the same picture, I ordered different sizes of it (one for each frame size), and this will allow me to hang the 3 pictures in different sizes, aspect ratios and orientation. That in a nutshell are image styles.

Image removed.

Image styles best practices

Image styles are actually pretty easy to create but unfortunately because of this they can be misused or mismanaged. If not done properly, you may end up with a lot more image styles than you really need or image styles that are not well define and do no provide the outcome you are looking for. To avoid this, let's go over best practices for creating image styles which will result in less image styles to manage. Mind you, these are my best practices but I have to admit, they have worked very well for me.

Naming image styles

Have you heard the phrase "naming things is hard"? It's true. Unfortunately when it comes to image styles, if not named properly you can get yourself in a lot of trouble. Quick example, let's say I want to create an image style that I'd like to use on images of news articles that are displayed on the homepage. One might think a good name for the image style may be something like "Homepage news article images". It doesn't seem so bad but let me point out a few issues with this name:

  • The image style is limited to the homepage
  • It is limited to news article images
  • It lacks information about the image dimensions or aspect ratio

One objective with image styles is to create them in a way that they are reusable. The more reusable an image style is the less image styles you will need to create which in turn becomes easier to manage. The main issue with the image style above ("Homepage news article images"), besides the 3 bullet points we called out, is that is not reusable. The name of it limits us to only use it on the homepage and only for news article images. If we want to display similar images elsewhere, we would need to create another image style maybe with the same parameters as the first one. You may be asking yourself, wait, why can't we use the same image style elsewhere? Technically you can, but think about how confusing it will be to use an image style called "Homepage news article images", not on the homepage and not on news article images.

Creating reusable image styles

One very efficient way for creating reusable image styles is to name them based on the image aspect ratio or dimensions, or a combination of both. For example: "16:9 (Max 320px)", or "Box 1:1 (500px)". Here are some reasons why this is a great way to name image styles:

  • They are not specific to any page or type of image (articles, events, etc.)
  • They provide key information about the image aspect ratio and their dimensions
  • I can use these image styles hundreds of times on any image that fits the requirements as well as on any page
  • By creating/naming image styles this way, I may have just saved myself from creating many other image styles

Identifying the images patterns

I have found one of the most effective ways for identifyiing the image styles you need to create is by looking at your website mockups (if you are fortunate enough to have them). This may not always be possible, but if you do have designs for your website, this will tell you exactly which images you will need and how they need to be rendered. Having this information upfront will help you tremendously when creating image styles because you can plan ahead of time how to create reusable image styles that share commom attributes.

Image styles use cases

When naming image styles it helps me to think of the characteristics of the images I am creating image styles for. For example, I have an image that should be rendered in 16:9 aspect ratio and it should not exceed a width of 320px. This is how I arrived at the name 16:9 (Max 320px). This also makes it possible to know which image style to use if I have other images that need to be rendered similarly. By the way, it is perfectly okay to use an image style that is slightly off from what an image needs to be rendered at. For example, Let's say I have an image that should be rendered at 16:9 aspect ratio, but its size should not exceed 250px. for this image, I can still use the 16:9 (Max 320px) image style.

A 100px or even 200px difference between the image style dimensions and the image you need to use it on it's an acceptable thing to do for a couple of reasons:

  • 100 or 200px in most cases will not make a big of an impact in performance, however, if you are rendering 50 of these images in a single page, then this could certainly present performance issues. So my rule is as long as this is a oneoff type of situation, I'm okay with doing this.
  • Keep in mind that just because your image may be resized larger than it actually needs to be rendered, your image will still visually rendered at the right size as I would suppose it is inside a container that will be sized to the right rendering size, via CSS.
  • Being able to reuse an image style that may be slightly larger than needed saves me from creating more image styles.

I hope you see the impact good names for image styles have on your site. When you are working on an enterprise level website, using the best practices above can really help you with the maintenance of your image styles.

Image styles effects

Effects are the rules you set on each image style. Rules such as cropping, sizing, converting, saturating, rotating, and scaling of images is how we determine how to render the images in our site. In most cases, you want to let content creators of your site upload images that are relatively big. Doing so will allow you to use the images in your library in any use case. It is perfectly okay to scale your images down thorugh the use of image styles, but it is not recommended to scale images up. Doing so will result in blurry or pixelated images. This is why is better to upload large images. But you may be thinking, if I upload super large images, this will affect the performance of my site. It will if you are rendering the original images, but since we are using image styles, Drupal uses the original image to make copies at the size and aspect ratio you defined in your image styles. This is why by uploading a single image you are able to use it in many use cases at different sizes or aspect ratios.

Image styles effects can vary from image style to image style. For example, some image styles will require images to be scaled down, then cropped. Others will require images to be resized then cropped and others may just need for images to be resized to specific size. All these actions are called "Effects" in image styles. The effects you apply to your image styles will depend on the end goal for rendering the images. Let's do a quick demo of creating one image styles then applying effects to it.

Hands-on excercise

All the principles covered in this series apply to Drupal 8, 9, and 10. You can follow along or simply watch me do it. To get started, spin up a vanilla Drupal site of the version of your choice. At the begining of this series I mentioned we will stick with only core/out of the box functionality to keep things simple.

Creating a new image style

  1. Login to Drupal as administrator
  2. In your Drupal site navigate to /admin/config/media/image-styles
  3. Click Add image style
  4. For Image style name type: 16:9 (Max 320px)
  5. To keep things nifty, edit the image style machine name so it reads 16_9_max_320px. (Remove the trailing underscore generated by the ending parenthesis in the image style name. Clean machine names are important to me 😃. It actually makes a difference when you are debugging issues and you find your machine name has an extra underscore you didn't catch).
  6. Click Create new style

Image removed.

The image style above follows the best practices for name we covered earlier. This makes this image style reusable on any image that meets the aspect ratio and dimension requirements.

Adding effects to the image style

For the purpose of this exercise, we are going to use the Scale and crop effect. This is probably the effect I use the most because it does exactly what I want, scale the image down to the size I want and crop it in the aspect ratio I need.

  1. While in the page where the new image style was created (/admin/config/media/image-styles/manage/16_9_max_320px), scroll down and you should see the Effect dropdown
  2. Select Scale and crop from the dropdown
  3. Click Add. The Add Scale and Crop effect screen will come up
  4. Type 320 for width and 180 for height. Note: These two values are required when you select the scale and crop effect. In other effects, these values may not always be required. It is important to define fixed dimensions on your image styles. This ensures your images will be sized/cropped at exactly the size you expect them to. How did I figure out the height for a 16:9 image with a width of 320px is 180px? I used this online aspect ratio calculator.
  5. Notice how you can change the focal point of the cropping by clicking any of the circles under Anchor. For this example we'll keep it in the middle circle.
  6. Click Update effect. This will bring you back to the image style page.
  7. We're done!

Image removed.

Now we have one custom image style with specific effects. If you noticed, Drupal comes with a couple of predefined image styles. If they work for your images you should make use of them. In this series we will be creating custom image styles.

As I mentioned earlier, names of image styles should be descriptive but not limiting. In the case of some of the image styles that come out of the box with Drupal, the names Large, Medium, and Wide do not seem like good choices for names because those names are all relative. Large in relation to what? Medium in relation to what? ...just sayin'.

Image multipliers

One thing we have not discussed but it is very important in responsive images, is the use of "Image Multipliers". What are image multipliers? In responsive images, you often think of image sizes in natural dimensions. If I think of an image that needs to be rendered at 720px, I will most likely resize or crop that image at 720px which makes total sense. With the evolution of high density or high resolution (retina) screens on mobile and larger devices, because they contain thousands or millions more pixels than traditional resolution screens, images need to actually be bigger than the intended size so they are rendered at their highest resolution/quality. This is what image multipliers are.

If we go back to the example above of the 720px image. For this image to be rendered as sharp and high-quality as possible in retina screen devices, we should add a 2x or 3x multiplier to it. Meaning, we should create an image styles for this image at twice and three times the intended size (1440px, 2160px). When we do this, the image will still be rendered at 720px (visually), but because we are providing larger images, these images will have twice or three times the number of pixels within them and when viewed in high resolution screens, the image quality will be superior than if we are only providing a regular 720px image. Note: I typically only create a 2x multiplier image styles for most of my images, but there may be situation when there are specific requirements for creating a 3x multiplier image styles.

Create a 2x multiplier image style

The same way you created the original image style above for 16:9 (Max 320px), go ahead and repeat the process but this time create a 2x multiplier image style, 16:9 (Max 640px). Remember, the dimensions of this image style should be 640 x 360px.

So what's next?

With our custom image styles in place, we can now make use of them, but before we do, let's go over another very important concept within Drupal, Responsive image styles ...whaaaaatttt?
We'll comeback to Drupal in a bit but first, we'll talk about responsive image styles in the next post.

Navigate posts within this series

Mario Hernandez: Responsive image styles

In a nutshell, responsive image styles are a collection of image styles. It can be confusing because the similarities in their names, but responsive image styles are a bundle that holds one or more image styles.

What's the use of responsive image styles?

If you read the posts about the <picture> element as well as the one about srcset and sizes attributes, we discussed that whether you are doing art direction or resolution switching, we need to provide the browser with a collection of images to serve to different devices. In Drupal the way we provide the collection of images is by using responsive image styles.

Image removed.

Naming responsive image styles

In the previous post we went in detail about best practices for naming image styles. Properly naming responsive image styles is just as important but there are some differences in guidelines. While naming image styles is mostly based on the characteristics of the images (aspec ratio, orientation, dimensions), naming responsive image styles is typically based on their use case. Let's take a look at some examples.

Let's say we are building a photo gallery where we will use a series of images to display as a slider or photos grid. We may not be as concerned about the images aspect ratio or dimentions because we already have image styles in place to handle that. We're only interested on how the images will be used. In this example the use case is a Gallery. So a name for the responsive image style that makes sense to me would be Gallery or Photo gallery. Another example would be creating a responsive image style for your website's hero component. Regardless of what the images dimensions are, the responsive image style can be called Hero. Both of these examples are for very unique use cases, but there are also cases for more common type of responsive images styles such as a listing of news articles or events, a featured news article or a page for team member photos. In these cases, we can use names that we can reuse elsewhere. Something like Person listing, News full, Content grid, or 16:9 (Max 460px).

Back to hands-on exercises

We are going to create a new responsive image style in which we will make use of the image styles we created in the previous post. Let's pretend the images we are targeting will be used for displaying News and Events listings (Similar to this, and example below).

Image removed.

Most websites use patterns that they repeat across their sections or pages. The news listing example above is a pattern that can be used for not only displying news articles, but maybe also events, social media posts, and more. This is great because identifying these patterns can help us create better image styles and responsive image styles that are reusable. This is what I meant in the previous post when I said that if you are fortunate enough to have designs for your project, you will be able to identify these patterns right away. Let's keep this in mind as we get back into exercise mode.

Resolution switching

Here's where all of our knowledge about <picture> (art direction) and srcset and sizes (resolution switching) comes in handy. We'll start with resolution switching because art direction requires additional tooling and configuration.

In Drupal 8 we used the Picture and Breakpoints contrib modules to handle responsive images. Starting with Drupal 9, Drupal provides the "Responsive image" core module which means we don't need to install any contrib modules. Responsive image is not enabled by default.

  1. Enable the Responsive image core module (/admin/modules)
  2. Once enabled, head over to /admin/config/media/responsive-image-style to begin creating our first responsive image style
  3. Click Add responsive image style
  4. Type Content grid as the label for the responsive image style
  5. Select *Responsive image from the Breakpoint group dropdown
  6. Scroll down and select a Fallback image style (16:9 (Max 320px))
  7. Expand the 1x Viewport Sizing [] fieldset
  8. Under Type, chose Select multiple image styles and use the sizes attribute
  9. Under Sizes type the following: (max-width:640px) 100vw, 30vw (I'll explain shortly)
  10. Under Image styles select the image styles we created before
  11. Scroll down and click Save

Image removed.

Let's go over everything we just did

Since we are doing resolution switching and not art direction, we chose Responsive image from the Breakpoint group dropdown. Doing so presents to us the 1x Vieport Sizing [] screen with the following options:

  • Type: Again, since we are doing resolution switching, the obvious choice here is Select multiple image styles and use the sizes attribute. The other two options are irrelevant in this particular example.
  • Sizes: The Sizes option is where we tell the browser how big/small our images should be rendered in relation to the viewport being used. Depending on our goal, this field accepts a single value or a media query with some conditions. Let's say we wanted our images to always render at full width regardless of the device being used (like a Hero image for example), then the value for the Sizes field would be 100vw (100% the viewport width). In our case however, we want the image to display full width, but only if the viewport/device is not larger than 640px, otherwise, meaning if the viewport/device is larger than 640px, we want the image to display at 30% the viewport width. We could had also used a pixel-based value such as 400px for example.
  • Image styles: This is where we choose the image styles we want to make available for the browser to pick from. Think of this as the srcset attribute in the <img> tag.
  • Fallback image: We pick a fallback image in case all the above fails.

Very important: Remember in the Image resolution switching using srcset and sizes attributes post, our claim was that it's better to let the browser pick the best image possible as the browser is smarter than us? This is exactly what we are doing here. We are providing the browser with a collection of images to pick from. Each image provides its dimensions. Then we tell the browser how big/small we want the images to be rendered. One thing we are not doing is telling the browser which image to use, we let the browser make that decision. This is the complete opposite of what we do when we use <picture>. As a reminder, the method used above is what you would do in most of your images. This is like the default configuration for your responsive images. Only when you need to crop your images differently for each device size is when you would use the art direction approach.

Navigate posts within this series

Mario Hernandez: Integrating Drupal with Storybook components

Hey you're back! 🙂 In the previous post we talked about how to build a custom Drupal theme using Storybook as the design system. We also built a simple component to demonstrate how Storybook, using custom extensions, can understand Twig. In this post, the focus will be on making Drupal aware of those components by connecting Drupal to Storybook.
If you are following along, we will continue where we left off to take advantage of all the prep work we did in the previous post. Topics we will cover in this post include:

  1. What is Drupal integration
  2. Installing and preparing Drupal for integration
  3. Building components in Storybook
  4. Building a basic front-end workflow
  5. Integrating Drupal with Storybook components

What is Drupal integration?

In the context of Drupal development using the component-driven methodology, Drupal integration means connecting Drupal presenter templates such as node.html.twig, block.html.twig, paragraph.html.twig, etc. to Storybook by mapping Drupal fields to component fields in Storybook. This in turn allows for your Drupal content to be rendered wrapped in the Storybook components.

The advantage of using a design system like Storybook is that you are in full control of the markup when building components, as a result your website is more semantic, accessible, and easier to maintain.

Building more components in Storybook

The title component we built in the previous post may not be enough to demonstrate some of the advanced techniques when integrating components. We will build a larger component to put these techniques in practice. The component we will build is called Card and it looks like this:

Image removed.

When building components, I like to take inventory of the different parts that make up the components I'm building. The card image above shows three parts: An image, a title, and teaser text. Each of these parts translates into fields when I am defining the data structure for the component or building the entity in Drupal.

Building the Card component

  • Open the Drupal site in your code editor and within your code editor navigate to the storybook theme (web/themes/custom/storybook)
  • Create two new directories inside components called 01-atoms and 02-molecules
  • Inside 02-molecules create a new directory called card
  • Inside the card directory add the following four files:
    • card.css: component's styles
    • card.twig: component's markup and logic
    • card.stories.jsx: Storybook's story
    • card.yml: component's demo data
  • Add the following code snippet to card.yml:
--- modifier: '' image: <img src="https://source.unsplash.com/cHRDevKFDBw/640x360" alt="Palm trees near city buildings" /> title: level: 2 modifier: '' text: 'Tours & Experiences' url: 'https://mariohernandez.io' teaser: 'Step inside for a tour. We offer a variety of tours and experiences to explore the building’s architecture, take you backstage, and uncover the best food and drink. Tours are offered in different languages and for different levels of mobility.'
  • Add the following to card.twig to provide the markup and logic for the card:
{{ attach_library('storybook/card') }} <article class="card{{ modifier ? ' ' ~ modifier }}{{- attributes ? ' ' ~ attributes.class -}}" {{- attributes ? attributes|without(class) -}}> {% if image %} <div class="card__image"> <figure> {{ image }} </figure> </div> {% endif %} <div class="card__content"> {% if title %} {% include "@atoms/title/title.twig" with { 'level': title.level, 'modifier': title.modifier, 'text': title.text, 'url': title.url, } only %} {% endif %} {% if teaser %} <p class="card__teaser">{{ teaser }}</p> {% endif %} </div> </article>

Code snippet for building card

  • Copy and paste these styles into card.css.

  • Finally, let's create the Storybook card story by adding the following to card.stories.jsx:

import parse from 'html-react-parser'; import card from './card.twig'; import data from './card.yml'; import './card.css'; const component = { title: 'Molecules/Card', }; export const Card = { render: (args) => parse(card(args)), args: { ...data }, }; export default component;

Let's go over a few things regarding the code above:

  • The data structure in card.yml reflects the data structure and type we will use in Drupal.
    • The image field uses the entire <img> element rather than just using the image src and alt attributes. The reason for this is so when we get to Drupal, we can use Drupal's full image entity. This is a good practice for caching purposes.
  • card.twig reuses the title component we created in the previous post. Rather than build a title from scratch for the Card and repeat the code we already wrote, reusing the existing components keeps us DRY.
  • card.stories.jsx in the Storybook story for the Card, notice how the code in this file is very similar to the code in the title.stories.jsx. Even with complex components, when we port them into Storybook as stories, most times the code will be similar as what you see above because Storybook is simply parsing whatever is in .twig and .yml files. There are exceptions when the React code may have extra parameters or logic which typically happens when we're building stories variations. Maybe a topic for a different blog post. 😉

Before we preview the Card, some updates are needed

You may have noticed in card.twig we used the namespace @atoms when nesting the title component. This namespace does not exist, and we need to create it now. In addition, we need to move the title component into the 01-atoms directory:

  • In your code editor or command line (whichever is easier), move the title directory into the 01-atoms directory
  • In your editor, open title.stories.jsx and change the line
    title: 'Components/Title' to title: 'Atoms/Title'. This will display the title component within the Atoms category in Storybook's sidebar.
  • Rather than have you make individual changes to vite.config.js, let's replace/overwrite all its content with the following:
/* eslint-disable */ import { defineConfig } from 'vite' import yml from '@modyfi/vite-plugin-yaml'; import twig from 'vite-plugin-twig-drupal'; import { join } from 'node:path' export default defineConfig({ root: 'src', publicDir: 'public', build: { emptyOutDir: true, outDir: '../dist', rollupOptions: { input: { 'reset': './src/css/reset.css', 'styles': './src/css/styles.css', 'card': './src/components/02-molecules/card/card.css', }, output: { assetFileNames: 'css/[name].css', }, }, sourcemap: true, }, plugins: [ twig({ namespaces: { atoms: join(__dirname, './src/components/01-atoms'), molecules: join(__dirname, './src/components/02-molecules'), }, }), // Allows Storybook to read data from YAML files. yml(), ], })

Let's go over some of the most noticeable updates inside vite.config.js:

  • We have defined a few things to improve the functionality of our Vite project, starting with using src as our app root directory and public for publicDir. This helps the app understand the project structure in a relative manner.

  • Next, we defined a Build task which provides the app with defaults for things like where should it compiled code to (i.e. /dist), and rollupOptions for instructing the app which stylesheets to compile and what to call them.

  • As part of the rollupOptions we also defined two stylesheets for global styles (reset.css and styles.css). We'll create these next.

    Important

    This is as basic as it gets for a build workflow and in no way would I recommend this be your front-end build workflow. When working on bigger projects with more components, it is best to define a more robust and dynamic workflow that provides automation for all the repetitive tasks performed on a typical front-end project.
  • Under the Plugins section, we have defined two new namespaces, @atoms and @molecules, each of which points to specific path within our components directory. These are the namespaces Storybook understands when nesting components. You can have as many namespaces as needed.

Adding global styles

  • Inside storybook/src, create a new directory called css
  • Inside the css directory, add two new files, reset.css and styles.css
  • Here are the styles for reset.css and styles.css. Please copy them and paste them into each of the stylesheets.
  • Now for Storybook to use reset.css and styles.css, we need to update /.storybook/preview.js by adding these two imports directly after the current imports, around line 4.
import '../dist/css/reset.css'; import '../dist/css/styles.css';

Previewing the Card in Storybook

Remember, you need NodeJS v20 or higher as well as NVM installed on your machine
  • In your command line, navigate to the storybook directory and run:
nvm install npm install npm run build npm run storybook

A quick note about the commands above:

  • nvm install and npm install are typically only done once in your app. These commands will first install and use the node version specified in .nvmrc, and will install all the required node packages found in package.json. If you happen to be workign on another project that may use a different version of node, when you comeback to the Storybook project you will need to run nvm use in order to resume using the right node version.
  • npm run build is usually only ran when you have made configuration changes to the project or are introducing new files.
  • npm run storybook is the command you will use all the time when you want to run Storybook.

After Storybook launches, you should see two story categories in Storybook's sidebar, Atoms and Molecules. The title component should be under Atoms and the Card under Molecules. See below:

Image removed.

Installing Drupal and setting up the Storybook theme

We have completed all the prep work in Storybook and our attention now will be all in Drupal. In the previous post all the work we did was in a standalone project which did not require Drupal to run. In this post, we need a Drupal site to be able to do the integration with Storybook. If you are following along and already have a Drupal 10 site ready, you can skip the first step below.

  1. Build a basic Drupal 10 website (I recommend using DDEV).
  2. Add the storybook theme to your website. If you completed the excercise in the previous post, you can copy the theme you built into your site's /themes/custom/ directory, Otherwise, you can clone the previous post repo into the same location so it becomes your theme. After this your theme's path should be themes/custom/storybook.
  3. No need to enable the theme just yet, we'll come back to the theme shortly.
  4. Finally, create a new Article post that includes a title, body content and an image. We'll use this article later in the process.

Creating Drupal namespaces and adding Libraries

Earlier we created namespaces for Storybook, now we will do the same but this time for Drupal. It is best if the namesapces' names between Storybook and Drupal match for consistency. In addition, we will create Drupal libraries to allow Drupal to use the CSS we've written.

  • Install and enable the Components module
  • Add the following namespaces at the end of storybook.info.yml (mind your indentation):
components: namespaces: atoms: src/components/01-atoms molecules: src/components/02-molecules
  • Replace all content in storybook.libraries.yml with the following:
global: version: VERSION css: base: dist/css/reset.css: {} dist/css/styles.css: {} card: css: component: dist/css/card.css: {}
  • Let's go over the changes to both, storybook.info.yml and storybook.libraries.yml files:

    • Using the Components module we created two namespaces: @atoms and @molecules. Each namespace is associated with a specific path to the corresponding components. This is important because Drupal by default only looks for Twig templates inside the /templates directory and without the Components module and the namespaces it would not know to look for our component's Twig templates inside the components directory.
    • Then we created two Drupal libraries: global and card. The Global library includes two CSS stylesheets (reset.css and styles.css), which handle base styles in our theme. the Card library includes the styles we wrote for the Card component. If you noticed, when we created the Card component, the first line inside card.twig is a Twig attach library statement. Basically card.twig is expecting a Drupal library called card.

Turn Twig debugging on

All the pieces are in place to Integrate the Card component so Drupal can use it to render article nodes when viewed in teaser view mode.

  • The first thing we need to do to begin the integration process is to determine which Twig template Drupal uses to render article nodes in teaser view mode. One easy way to do this is by turning Twig debugging on. This used to be a complex configuration but starting with Drupal 10.1 you can now do it directly in Drupal's UI:

    • While logged in with admin access, navigate to /admin/config/development/settings on your browser. This will bring up the Development settings page.
    • Check all the boxes on this page and click Save settings. This will enable Twig debugging and disable caching.
    • Now navigate to /admin/config/development/performance so we can turn CSS and JS aggregation off.
    • Under Bandwidth optimization cleared the two boxes for CSS and Javascript aggregation then click on Save configuration.
    • Lastly, click the Clear all caches button. This will ensure any CSS or JS we write will be available without having to clear caches.
  • With Twig debugging on, go to the homepage where the Article we created should be displayed in teaser mode. If you right-click on any part of the article and select inspect from the context menu, you will see in detail all the templates Drupal is using to render the content on the current page. See example below.

    Note

    I am using a new basic Drupal site with Olivero as the default theme. If your homepage does not display Article nodes in teaser view mode, you could create a simple Drupal view to list Article nodes in teaser view mode to follow along.

Image removed.

In the example above, we see a list of templates that start with node...*. These are called template suggestions and are the names Drupal is suggesting we can assign our custom templates. The higher the template appears on the list, the more specific it is to the piece of content being rendered. For example, changes made to node.html.twig would affect ALL nodes throughout the site, whereas changes made to node--1--teaser.html.twig will only affect the first node created on the site but only when it's viewed in teaser view mode.

Notice I marked the template name Drupal is using to render the Article node. We know this is the template because it has an X before the template name.

In addition, I also marked the template path. As you can see the current template is located in core/themes/olivero/templates/content/node--teaser.html.twig.

And finally, I marked examples of attributes Drupal is injecting in the markup. These attributes may not always be useful but it is a good practice to ensure they are available even when we are writing custom markup for our components.

Create a template suggestion

By looking at the path of the template in the code inspector, we can see that the original template being used is located inside the Olivero core theme. The debugging screenshot above shows a pretty extensive list of templates suggestions, and based on our requirements, copying the file node--teaser.html.twig makes sense since we are going to be working with a node in teaser view mode.

  • Copy /core/themes/olivero/templates/content/node--teaser.html.twig into your theme's /storybook/templates/content/. Create the directory if it does not exist.
  • Now rename the newly copied template to node--article--teaser.html.twig.
  • Clear Drupal's cache since we are introducing a new Twig template.

As you can see, by renaming the template node--article--teaser (one of the names listed as a suggestion), we are indicating that any changes we make to this template will only affect nodes of type Article which are displayed in Teaser view mode. So whenever an Article node is displayed, if it is in teaser view mode, it will use the Card component to render it.

The template has a lot of information that may or may not be needed when integrating it with Storybook. If you recall, the Card component we built was made up of three parts: an image, a title, and teaser text. Each of those are Drupal fields and these are the only fields we care about when integrating. Whenever when I copy a template from Drupal core or a module into my theme, I like to keep the comments on the template untouched. This is helpful in case I need to reference any variables or elements of the template.

The actual integration ...Finally

  1. Delete everything from the newly copied template except the comments and the classes array variable
  2. At the bottom of what is left in the template add the following code snippet:
{% set render_content = content|render %} {% set article_title = { 'level': 2, 'modifier': 'card__title', 'text': label, 'url': url, } %} {% include '@molecules/card/card.twig' with { 'attributes': attributes.addClass(classes), 'image': content.field_image, 'title': article_title, 'teaser': content.body, } only %}
  • We set a variable with content|render as its value. The only purpose for this variable is to make Drupal aware of the entire content array for caching purposes. More info here.
  • Next, we setup a variable called article_title which we structured the same way as data inside card.yml. Having similar data structures between Drupal and our components provides many advantages during the integration process.
    • Notice how for the text and url properties we are using Drupal specific variables (label and url), accordingly. If you look in the comments in node--article--teaser.html.twig you will see these two variables.
  • We are using a Twig include statement with the @molecules namespace to nest the Card component into the node template. The same way we nested the Title component into the Card.
  • We mapped Drupal's attributes into the component's attributes placeholder so Drupal can inject any attributes such as CSS classes, IDs, Data attributes, etc. into the component.
  • Finally, we mapped the image, title and teaser fields from Drupal to the component's equivalent fields.
  • Save the changes to the template and clear Drupal's cache.

Enable the Storybook theme

Before we forget, let's enable the Storybook theme an also make it your default theme, otherwise all the work we are doing will not be visible since we are currently using Olivero as the default theme. Clear caches after this is done.

Previewing the Article node as a Card

Integration is done and we switched our default theme to Storybook. After clearing caches if you reload the homepage you should be able to see the Article node you wrote but this time displayed as a card. See below:
Image removed.

  • If you right-click on the article and select Inspect, you will notice the following:
    • Drupal is now using node--article--teaser.html.twig. This is the template we created.
    • The template path is now themes/custom/storybook/src/templates/content/.
    • You will also notice that the article is using the custom markup we wrote for the Card component which is more semantic, accessible, but in addition to this, the <article> tag is also inheriting several other attributes that were provided by Drupal through its Attributes variable. See below:

Image removed.

If your card's image size or aspect ratio does not look as the one in Storybook, this is probably due to the image style being used in the Article Teaser view mode. You can address this by:

  • Going to the Manage display tab of the Article's Teaser view mode (/admin/structure/types/manage/article/display/teaser).
  • Changing the image style for the Image field for one that may work better for your image.
  • Preview the article again on the homepage to see if this looks better.

In closing

This is only a small example of how to build a simple component in Storybook using Twig and then integrate it with Drupal, so content is rendered in a more semantic and accessible manner. There are many more advantages of implementing a system like this. I hope this was helpful and see the potential of a component-driven environment using Storybook. Thanks for visiting.

Download the code

For a full copy of the code base which includes the work in this and the previous post, clone or download the repo and switch to the card branch. The main branch only includes the previous post code.

Download the code

Mario Hernandez: Building an automated DDEV-based Drupal environment

A successful training experience begins before we step foot in the training room. Or, in these days of distance learning, it begins before students login to your training platform. The challenge is having an environment that is easy for students to setup and provides all the tooling required for the training. Configuring a native development environment is no easy task. Web development tools have gotten more complex with the years and it's a huge barrier for even experienced developers. My goal in setting up this new environment was to have everything completely configured and automated so students only needed to run one command. Very ambitious.

WARNING: The codebase shared in this post is only intended for local development. DO NOT use this project in a production website.

About DDEV: The official name is DDEV-Local. For simplicity I use DDEV in this tutorial.

Here are the tools and configuration requirements for this environment:

  • Docker
  • DDEV
  • Composer
  • Drush
  • Drupal 8 and contrib modules
  • Pre-built Drupal entities (content types, paragraph types, views, view modes, taxonomy, image styles, and more)
  • Custom Drupal 8 theme
  • Twig debugging enabled by default and Drupal cache disabled by default
  • NodeJS, NPM, and NVM
  • Pattern Lab
  • Gulp, ESLint, Sass Lint, BrowserSync, Autoprefixer, and many many more node dependencies

Desired behavior

My main objective was to simplify the building and interaction with the environment, by:

  • Only require Docker and DDEV to be installed on host computer
  • Reducing the number of steps for building the environment to one command, ddev start
  • Execute most if not all commands in the containers, not the host machine (Drush, Composer, Pattern Lab, etc.)
  • Access Pattern Lab running in the web container, from the host machine

Yes, it is crazy, but let's see how this turned out

First things first. As I said before, installing Docker and DDEV are the only two things students will need to install. There is no way around this. Luckily there are a lot of resources to help you with this. The DDEV docs is a great place to start.

Setting up a Drupal site

Before we start, I'd like to clarify that this post focuses on outlining the process and steps for automating a local environment. However, before we can automate, we need to build all the pieces. If you just want to grab the final product here's the repo, otherwise, read on.

As of Drupal 8.8.0, Composer project templates are now available as part of Drupal core. These project templates are recommended for building new Drupal sites as they serve as a starting point for creating a Composer-managed Drupal site.

Two things we will be doing with our Drupal project:

  1. Drupal dependencies and modules will not be commited to the git repo. They will be downloaded with Composer when the project is being built.
  2. Composer will not be installed in the host system, instead, we will use the composer version that comes with DDEV's web container.

Let's start

  1. Create a new directory for your project. The directory name should be lowercase and alpha-numeric characters only. For this example I will name it drupaltraining and will create it in my /Sites directory. Using your command line tool create the new directory

    mkdir drupaltraining
  2. Now navigate into the newly created directory

    cd drupaltraining
  3. Run the DDEV command below to setup a new Drupal project

    ddev config --project-type=drupal8 --docroot=web --create-docroot
    • This will create a .ddev directory with basic Drupal configuration.
    • It will also create settings.php and settings.ddev.php` files inside web/sites/default.
    • Finally, it creates a Drush directory.
    • Keep in mind, Drupal is not in place yet, this simply sets the environment for it.
  4. Now start DDEV to create the containers.

    ddev start
    • After the project has been built, you will see a link to open Drupal in the browser. For this project the link should be https://drupaltraining.ddev.site (ignore for now). If you have not installed mkcert (mkcert -install), the link you see may use http instead of https. That's fine, but I'd recommend always using https by installing mkcert.
  5. Now we are going to create the codebase for Drupal by using the official Drupal community
    Composer template
    .

    ddev composer create "drupal/recommended-project:^8"

    Respond Yes when asked if it's okay to override everything in the existing directory.

    • This will setup the codebase for Drupal. This could take a while depending on your connection.
    • composer.json will be updated so Drupal is added as a project's dependency.
    • New settings.php and settings.ddev.php files are created.
    • Using ddev composer create uses the version of composer that comes as part of the DDEV's web container. This means composer is not required to be installed in the host's computer.
    • One thing I love about using the new composer template is the nice list of next steps you get after downloading Drupal's code base. So useful!
  6. Now let's grab some modules

    ddev composer require drupal/devel drupal/admin_toolbar drupal/paragraphs drupal/components drupal/viewsreference drupal/entity_reference_revisions drupal/twig_field_value
    • This will download the modules and update composer.json to set them as dependencies along with Drupal. The modules and Drupal's codebase will not be commited to our repo.
  7. Let's add Drush to the project

    ddev composer require drush/drush
    • Installing Drush as part of the project which will run in the web container will make it possible to run drush commands (ddev drush <command>), even if the host does not have drush installed.
  8. Now launch Drupal in the browser to complete the installation

    ddev launch
    • This will launch Drupal's intall page. Drupal's url will be https://drupaltraining.ddev.site. Complete the installation by using the Standard profile. Since the settings.ddev.php file already exists, the database configuration screen will be skipped from the installation.
  9. Make a copy of example.settings.local.php into web/sites/default

    cp web/sites/example.settings.local.php web/sites/default/settings.local.php
    • This is recommended to override or add new configuration to your Drupal site.
  10. Update settings.php to include settings.local.php.

    if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) { include $app_root . '/' . $site_path . '/settings.local.php'; }
    • I'd suggest adding the above code right after the settings.ddev.php include already in settings.php. This will allow us to override configuration found in settings.ddev.php.
  11. Let's change Drupal's default config directory. Doing this will export any configuration outside the web directory in Drupal. This is a good security measure. Let's also ignore a couple of folders to avoid drupal errors.

    if (empty($settings['config_sync_directory'])) { $settings['config_sync_directory'] = '../config/sync'; } $settings['file_scan_ignore_directories'] = [ 'node_modules', 'bower_components', ];
    • First block changes the default sync directory to drupaltraining/config/sync. If you look inside settings.ddev.php you will see that this directory is inside web. We want to store any configuration changes outside the web directory.
    • Second block sets up Drupal to ignore node_modules and bower_components. Drupal may look for twig templates inside these directories and could cause Drupal to crash. Ignoring these directories solves these issues.
  12. Since we've made changes to DDEV's configuration, restart DDEV

    ddev restart

That's quite the process, isn't it? The good news is this entire process will be eliminated when we finish automating the environment.

Drupal 8 custom theme

The project's theme is called training_theme. This is a node-based theme, and will be built with Mediacurrent's theme generator, which will provide:

  • A best-practices Drupal 8 theme
  • Pattern Lab integration
  • Automated Front-End workflow
  • Component-based-ready environment
  • Production-ready theme

The final DDEV project will include the new Drupal 8 theme so there is no need to create it now, but if you want to see how the Theme Generator works, Watch the video tutorial I recorded.

Automating our environment

Now that Drupal has been setup let's begin the automation process.

Dockerfile

A web Docker container comes with Node and NPM installed. This will work in most cases, but the Drupal theme may use a version of node not currently available in the container. In addition, the web container does not include Node Version Manager (NVM), to manage multiple node versions. If the tools we need are not available in the web or db images/containers, there are ways to modify them to include the required tools. One of those ways is an add-on Dockerfile in your project's .ddev/web-build or .ddev/db-build, depending which container you are trying to modify.

Inside .ddev/web-build create a file called Dockerfile (case sensitive), and in it, add the following code:

ARG BASE_IMAGE FROM $BASE_IMAGE ENV NVM_DIR=/usr/local/nvm ENV NODE_DEFAULT_VERSION=v14.2.0 RUN curl -sL https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh -o install_nvm.sh RUN mkdir -p $NVM_DIR && bash install_nvm.sh RUN echo "source $NVM_DIR/nvm.sh" >>/etc/profile RUN bash -ic "nvm install $NODE_DEFAULT_VERSION && nvm use $NODE_DEFAULT_VERSION" RUN chmod -R ugo+w $NVM_DIR

The code above sets an environment variable for default node version (v14.2.0), which is the node version the theme uses at the time of this setup. It installs and configures NVM, and makes NVM executable by updating the container's bash profile. A Dockerfile runs while the image or container is being built to alter any default configuration with the code found in the Dockerfile.

Custom DDEV commands

The Drupal 8 theme for this project uses Node for most of its tasks. To compile CSS, JavaScript, and Twig we need to run commands such as npm install, npm run build, npm run watch, and others. Our goal is to be able to run these commands in the web container, not the host computer. Doing this eliminates the need for students to install any node-related tools which can get really complicated. While we could achieve this by asking students to first SSH into the web container (ddev ssh), then navigate into /web/themes/custom/training_theme/, then run the commands, I want to make it even easier for them. I want them to be able to run the commands from any directory within the project and to not have to ssh into the web container. To achieve this we need to create a couple of custom commands.

Custom commands can be created to run in containers as well as the host machine. Since we want to run these commands in the web container, we are going to create the commands inside .ddev/commands/web/. Custom commands are bash script files.

NVM custom command

  • Inside .ddev/commands/web/ create a new file called nvm
  • Add the following code in the file:
#!/bin/bash ## Description: Run any nvm command. ## Usage: nvm [flags] [args] ## Example: "nvm use or nvm install" source /etc/profile && cd /var/www/html/web/themes/custom/training_theme && nvm $@
  • Since this is a bash script, #!/bin/bash is required as the first line in the file.
  • A description of the script is a good practice to explain what the script does.
  • Pay close attention to ## Usage: nvm [flgas] [args]. This is what makes the commands work. The [flags] and [args] are ways to pass arguments to the command. For example, nvm on its own won't do much, but use or install can be passed as parameters to complete the commands nvm use or nvm install. Being able to run these commands will allow us to install new versions of Node later on if needed.
  • Next we are adding examples of potential commands that can be run.
  • Finally, you see the code or actual commands. source /etc/profile is basically resetting the container's bash profile so NVM can run. Then we navigate into the training_theme directory within the container where the nvm commands will be executed. So technically in the script above we are running 3 commands in one. Using && in between each command lets us combine or concatenate them. The $@ after nvm represent the flags or arguments we can pass (i.e. use or install).

Running the new commands: Every custom command needs to be executed by adding ddev before the command. For example: ddev nvm use. Using ddev infront of the command instructs the system to run the commands in the containers, rather than the host computer.

NPM custom commands

We will also create a custom script to run NPM commands. This will be similar to the NVM script. This new script will be executed as ddev npm install or ddev npm run build, etc. The npm commands will allow us to install node dependencies by the theme as well as execute tasks like compiling code, linting code, compressing assets, and more.

  • Create a new file inside .ddev/commands/web and call it npm
  • Add the following code in the file:
#!/bin/bash ## Description: Run npm commands inside theme. ## Usage: npm [flags] [args] ## Example: "npm install or npm rebuild node-sass or npm run build or npm run watch" cd /var/www/html/web/themes/custom/training_theme && npm $@
  • Most of the code here is similar to the previous script, except instead of nvm we will run npm.
  • Notice we are again navigating into the theme directory before running the command. This make is possible for the custom commands we are creating to be ran from any directory within our project while still being executed inside the training_theme directory in the container.

Drush custom command

Let's create one last custom command to run drush commands within the DDEV containers

  • Create a new file inside .ddev/commands/web and call it drush
  • Add the following code in the file:
#!/bin/bash ## Description: Run drush inside the web container ## Usage: drush [flags] [args] ## Example: "ddev drush uli" or "ddev drush sql-cli" or "ddev drush --version" drush $@
  • This will allow us to run drush commands in the container but using ddev drush <command> (i.e. ddev drush cr, ddev drush updb -y, etc.).

So that's it for custom commands. By having custom commands for nvm and npm, we can now successfully run any theming related tasks.

Automating Drupal's setup

We want to streamline the drupal installation process. In addition, we want to be able to import a custom database file to have access to all the infrastructure needed during training. This includes content types, paragraph types, views, view modes, image styles, and more. Enter DDEV hooks.

DDEV Hooks

Hooks are a great way to perform tasks before or after DDEV starts. There are tasks that need to happen in specific sequence and hooks allow us to do just that. So what's the differnce between custom commands and hooks? Technically hooks can be considered custom commands, but the difference is that they are executed automatically before or after DDEV starts, whereas custom commands are ran on demand at any time. DDEV needs to be running if custom commands are intended to run in containers. Back to hooks, Let's build a post-start hook.

Hooks can be ran as pre-start, post-start, and after-db-import. Also, hooks can be executed inside containers and/or the host machine. In our case all the tasks we outlined above will be ran after DDEV starts and most of them in Docker's containers.

  • Open .ddev/config.yaml and add the following code at the bottom of the file. There may already be a hooks section in your file. Be sure indentation in the file is correct.
hooks: post-start: - composer: install - exec: /var/www/html/db/import-db.sh - exec: drush updb -y - exec: drush cim -y - exec-host: cp -rf web/sites/example.development.services.yml web/sites/development.services.yml - exec-host: cp -rf web/assets/images/* web/sites/default/files/images/ - exec: drush cr - exec-host: ddev launch /user
  • post-start: indicates tasks declared will run after DDEV container's have started. We want the web and db containers available before running any of the tasks.
  • composer: install will download all dependencies found in composer.json (Drupal core, modules, drush and others).
  • exec: /var/www/html/db/import-db.sh is a custom script which imports a custom database file (provided in this project). We will go over this script shortly. Importing a custom database builds the Drupal site without having to install Drupal as long as Drupal's codebase exists.
  • exec: drush cim -y, exec: drush updb -y, and exec: drush cr, are basic drush commands.
  • exec-host: cp -rf web/sites/example.development.services.yml web/sites/development.services.yml will create a new development.services.yml. This is needed because when Drupal is setup, development.services.yml is overridden and since we are using custom configuration in that file to enable twig debugging, we need to restore the configuration by replacing the file with a copy of our own.
  • exec-host: cp -rf web/assets/images/* web/sites/default/files/images/ copies a collection of images used in the demo content added to the site.
  • Finally, exec-host: ddev launch /user will open a fully configured Drupal website in the browser.

Login to Drupal: Username: admin, password: admin

Import database script

Now let's write the script to import the database we are using in the hook above.

  • In your project's root, create a new directory called db
  • In the new directory create a new file called import-db.sh and add the following code:
#!/bin/bash # Use a table that should exist in your database. if ! mysql -e 'SELECT * FROM node__field_hero;' db 2>/dev/null; then echo 'Importing the database' # Provide path to custom database. gzip -dc /var/www/html/db/drupaltraining.sql.gz | mysql db fi
  • This is again another bash script which performs a database import but only if the database is empty or clean. We do this by checking if one of the tables we expect in the database exists (node__field_hero). If it does, the database is not imported, but if it doesn't it will import the database. This table can be any table you know it should exist.

  • Notice the script calls for a database file named drupaltraining.sql.gz. This means the database file shold exist inside the db directory alongside the import-db.sh script. This database file was created/exported after Drupal was configured with all training required infrastructure and settings.

  • Make the script executable

chmod +x db/import-db.sh
  • This will ensure the script can be executed by DDEV, otherwise we would get a permission denied error.

This does it for automation. The next few tasks are things that improve the development environment. Some of these tasks are optional.

Exposing Pattern Lab's port in host computer

Since our goal is to not have to install any Front-End tools in the host computer, running Pattern Lab has to be done in the web container. The problem is we can't open Pattern Lab in the host's browser if Pattern Lab is running in the container. For this to work we need to expose the port in which Pattern Lab runs to the host machine. In this environment, that port is 3000 (this port number may vary). We identify this port by running npm run watch inside the training_theme directory. This will provide a series of links to access Pattern Lab in the browser.

Under the hood, DDEV uses docker-compose to define and run the multiple containers that make up the local environment for a project. docker-compose supports defining multiple compose files to facilitate sharing Compose configurations between files and projects, and DDEV is designed to leverage this ability.

Creating a new docker-compose.*.yaml

A docker-compose file allows to do many things including exposing ports from the containers to the host computer.

On a typical Pattern lab project if you run npm start you will see Pattern Lab running on http://localhost:3000. In this environment the equivalent command is ddev npm run watch. Since this environment is running Pattern Lab in the web container, the only way to access Pattern Lab in the browser is by having access to the container's port 3000. Exposing the port via the http or https protocols makes it possible to access Pattern Lab's UI page from the host machine.

  1. Inside .ddev/, create a new file called docker-compose.patternlab.yaml

  2. In the new file add the following code:

    # Override the web container's standard HTTP_EXPOSE and HTTPS_EXPOSE # to access patternlab on https://drupaltraining.ddev.site:3000, or http://drupaltraining.ddev.site:3001. version: '3.6' services: web: # ports are a list of exposed *container* ports ports: - "3000" environment: - HTTP_EXPOSE=${DDEV_ROUTER_HTTP_PORT}:80,${DDEV_MAILHOG_PORT}:8025,3001:3000 - HTTPS_EXPOSE=${DDEV_ROUTER_HTTPS_PORT}:80,${DDEV_MAILHOG_HTTPS_PORT}:8025,3000:3000
    • The name of the file is completely optional. It makes sense to use a name that is related to the action, app, or service you are trying to implement. In this example the name docker-compose.patternlab.yaml made sense.
    • How did we arrive at the content above for this file? DDEV comes with two files that can be used as templates for new configuration, one of those files is called .ddev-docker-compose-base.yaml. You can find all the code we added above in this file.
  3. Restart DDEV to allow for the new changes to take effect:

    ddev restart
    • The basics of the code above is modifying the web container's port 3000. We are exposing this port through the http and https protocols on the host machine.
  4. For the above to work Pattern Lab needs to be running in the container:

    ddev npm run watch

Viewing Pattern Lab in the host machine

Using NFS (optional)

Last but not least, enabling NFS in DDEV can help with performance of your application. NFS (Network File System) is a classic, mature Unix technique to mount a filesystem from one device to another. It provides significantly improved webserver performance on macOS and Windows. This is completely optional and in most cases you may not even need to do this. The steps below are for macOS only. Learn more about NFS and how to enable it in other Operating Systems like Windows.

  • In your command line run
id
  • Make note of uid (user id) and gid (group id).
  • Open etc/exports in your code editor, and add the following code, preferably at the top of the file:
/System/Volumes/Data/Users/xxxx/Sites/Docker -alldirs -mapall=502:20 localhost
  • Replace xxxx with your username. The full path shown above is required if you are using macOS Catalina.

  • Replace Sites/Docker (This is my personal project's directory), with the directory name where your DDEV projects are created. Most people would mount the entire user's home directory, but I think only mounting the directory where your projects live is good enough (/Sites/Docker/).

  • Replace 502 and 20 with the values you got when you ran the id command above.

  • Update DDEV's config.yml to enable NFS

nfs_mount_enabled: true
  • Restart DDEV
ddev restart

You should notice an improvement in performance in your Drupal website.

In closing

I realize there is a lot here, but I am pretty happy with how this turned out. Thanks to all the work in this article, when one of the students wants to setup their training environment, all the have to do is run ddev start. I think that's pretty sweet! 🙌 Happy DDEVing!

Giving credit

Before I started on this journey, I knew very little about DDEV. Thanks to the amazing help from Randy Fray from Drud.com and Michael Anello from DrupalEasy.com, I have learned a lot in the past weeks and wanted to share with other community members. You can find both, Randy and Michael on Drupal's DDEV Slack Channel. They are extremely helpful and responsive.

Resources

Mario Hernandez: Automating your Drupal Front-end with ViteJS

Modern web development relies heavily on automation to stay productive, validate code, and perform repetitive tasks that could slow developers down. Front-end development in particular has evolved, and it can be a daunting task to configure effective automation. In this post, I'll try to walk you through basic automation for your Drupal theme, which uses Storybook as its design system.

Recently I worked on a large Drupal project that needed to migrate its design system from Patternlab to Storybook. I knew switching design systems also meant switching front-end build tools. The obvious choice seemed to be Webpack, but as I looked deeper into build tools, I discovered ViteJS.

Vite is considered the Next Generation Frontend Tooling, and when tested, we were extremely impressed not only with how fast Vite is, but also with its plugin's ecosystem and its community support. Vite is relatively new, but it is solid and very well maintained. Learn more about Vite.

The topics covered in this post can be broken down in two categories:

  1. Preparing the Front-end environment

  2. Automating the environment

1. Build the front-end environment with Vite & Storybook

In a previous post, I wrote in detail how to build a front-end environment with Vite and Storybook, I am going to spare you those details here but you can reference them from the original post.

  1. In your command line, navigate to the directory where you wish to build your environment. If you're building a new Drupal theme, navigate to your site's web/themes/custom/
  2. Run the following commands (Storybook should launch at the end):
npm create vite@latest storybook cd storybook npx storybook@latest init --type react

Fig. 1: The first command builds the Vite project, and the last one integrates Storybook into it.

Reviewing Vite's and Storybook's out of the box build scripts

Vite and Storybook ship with a handful of useful scripts. We may find some of them already do what we want or may only need minor tweaks to make them our own.

  • In your code editor, open package.json from the root of your newly built project.
  • Look in the scripts section and you should see something like this:
"scripts": { "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" },

Fig. 2: Example of default Vite and Storybook scripts out of the box.

To run any of those scripts, prefix them with npm run. For example: npm run build, npm run lint, etc. Let's review the scripts.

  • dev: This is a Vite-specific command which runs the Vite app we just build for local development
  • build: This is the "do it all" command. Running npm run build on a project runs every task defined in the build configuration we will do later. CI/CD runners run this command to build your app for production.
  • lint: Will lint your JavaScript code inside .js or .jsx files.
  • preview: This is also another Vite-specific command which runs your app in preview mode.
  • storybook: This is the command you run to launch and keep Storybook running while you code.
  • build-storybook: To build a static version of Storybook to package it or share it, or to run it as a static version of your project.

Building your app for the first time

Getting a consistent environment

In front-end development, it is important everyone in your team use the same version of NodeJS while working in the same project. This ensures consistency in your project's behavior for everyone in your team. Differences in the node version your team uses can lead to inconsistencies when the project is built. One way to ensure your team is using the same node version when working in the same project, is by adding a .nvmrc file in the root of your project. This file specifies the node version your project uses. The node version is unique to each project, which means different projects can use different node versions.

  • In the root of your theme, create a file called .nvmrc (mind the dot)
  • Inside .nvmrc add the following: v20.14.0
  • Stop Storybook by pressing Ctrl + C in your keyboard
  • Build the app:
nvm install npm install npm run build

Fig. 3: Installs the node version defined in .nvmrc, then installs node packages, and finally builds the app.

NOTE: You need to have NVM installed in your system to execute nvm commands.
You only need to run nvm install once per project unless the node version changes. If you switch to a project that uses a different node version, when you return to this project, run nvm use to set your environment back to the right node version.

The output in the command line should look like this:

Image removed.

Fig. 4: Screenshot of files compiled by the build command.

By default, Vite names the compiled files by appending a random 8-character string to the original file name. This works fine for Vite apps, but for Drupal, the libraries we'll create expect for CSS and JS file names to stay consistent and not change. Let's change this default behavior.

  • First, install the glob extension. We'll use this shortly to import multiple CSS files with a single import statement.
npm i -D glob
  • Then, open vite.config.js in your code editor. This is Vite's main configuration file.
  • Add these two imports around line 3 or directly after the last import in the file
import path from 'path'; import { glob } from 'glob';
  • Still in vite.config.js, replace the export default... with the following snippet which adds new settings for file names:
export default defineConfig({ plugins: [ ], build: { emptyOutDir: true, outDir: 'dist', rollupOptions: { input: glob.sync(path.resolve(__dirname,'./src/**/*.{css,js}')), output: { assetFileNames: 'css/[name].css', entryFileNames: 'js/[name].js', }, }, }, })

Fig. 5: Build object to modify where files are compiled as well as their name preferences.

  • First we imported path and { glob }. path is part of Vite and glob was added by the extension we installed earlier.
  • Then we added a build configuration object in which we defined several settings:
    • emptyOutDir: When the build job runs, the dist directory will be emptied before the new compiled code is added.
    • outDir: Defines the App's output directory.
    • rollupOptions: This is Vite's system for bundling code and within it we can include neat configurations:
      • input: The directory where we want Vite to look for CSS and JS files. Here's where the path and glob imports we added earlier are being used. By using src/**/**/*.{css,js}, we are instructing Vite to look three levels deep into the src directory and find any file that ends with .css or .js.
      • output: The destination for where CSS and JS will be compiled into (dist/css and dist/js), respectively. And by setting assetFileNames: 'css/[name].css', and entryFileNames: 'css/[name].js', CSS and JS files will retain their original names.

Now if we run npm run build again, the output should be like this:

Image removed.

Fig. 6: Screenshot of compiled code using the original file names.

The random 8-character string is gone and notice that this time the build command is pulling more CSS files. Since we configured the input to go three levels deep, the src/stories directory was included as part of the input path.

2. Restructure the project

The out of the box Vite project structure is a good start for us. However, we need to make some adjustments so we can adopt the Atomic Design methodology. This is today's standards and will work well with our Component-driven Development workflow. At a high level, this is the current project structure:

> .storybook/ > dist/ > public/ > src/ |- stories/ package.json vite.config.js

Fig. 7: Basic structure of a Vite project listing only the most important parts.

  • > .storybook is the main location for Storybook's configuration.
  • > dist is where all compiled code is copied into and where the production app looks for all code.
  • > public is where we can store images and other static assets we need to reference from our site. Equivalent to Drupal's /sites/default/files/.
  • > src is the directory we work out of. We will update the structure of this directory next.
  • package.json tracks all the different node packages we install for our app as well as the scripts we can run in our app.
  • vite.config.js is Vite's main configuration file. This is probably where we will spend most of our time.

Adopting the Atomic Design methodology

The Atomic Design methodology was first introduced by Brad Frost a little over ten years ago. Since then it has become the standard for building web projects. Our environment needs updating to reflect the structure expected by this methodology.

  • First stop Storybook from running by pressing Ctrl + C in your keyboard.
  • Next, inside src, create these directories: base, components, and utilities.
  • Inside components, create these directories: 01-atoms, 02-molecules, 03-organisms, 04-layouts, and 05-pages.
  • While we're at it, delete the stories directory inside src, since we won't be using it.
NOTE: You don't need to use the same nomenclature as what Atomic Design suggests. I am using it here for simplicity.

Update Storybook's stories with new paths

Since the project structure has changed, we need to make Storybook aware of these changes:

  • Open .storybook/main.js in your code editor
  • Update the stories: [] array as follows:
stories: [ "../src/components/**/*.mdx", "../src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)", ],

Fig. 8: Updating stories' path after project restructure.

The Stories array above is where we tell Storybook where to find our stories and stories docs, if any. In Storybook, stories are the components and their variations.

Add pre-built components

As our environment grows, we will add components inside the new directories, but for the purpose of testing our environment's automation, I have created demo components.

  • Download demo components (button, title, card), from src/components/, and save them all in their content part directories in your project.
  • Feel free to add any other components you may have built yourself. We'll come back to the components shortly.

3. Configure TwigJS

Before we can see the newly added components, we need to configure Storybook to understands the Twig and YML code we are about to introduce within the demo components. To do this we need to install several node packages.

  • In your command line run:
npm i -D vite-plugin-twig-drupal @modyfi/vite-plugin-yaml twig twig-drupal-filters html-react-parser
  • Next, update vite.config.js with the following configuration. Add the snippet below at around line 5:
import twig from 'vite-plugin-twig-drupal'; import yml from '@modyfi/vite-plugin-yaml'; import { join } from 'node:path';

Fig. 9: TwigJS related packages and Drupal filters function.

The configuration above is critical for Storybook to understand the code in our components:

  • vite-plugin-twig-drupal, is the main TwigJS extension for our project.
  • Added two new imports which are used by Storybook to understand Twig:
    • vite-plugin-twig-drupal handles transforming Twig files into JavaScript functions.
    • @modyfi/vite-plugin-yaml let's us pass data and variables through YML to our Twig components.

Creating Twig namespaces

  • Still in vite.config.js, add the twig and yml() plugins to add Twig namespaces for Storybook.
plugins: [ twig({ namespaces: { atoms: join(__dirname, './src/components/01-atoms'), molecules: join(__dirname, './src/components/02-molecules'), organisms: join(__dirname, './src/components/03-organisms'), layouts: join(__dirname, './src/components/04-layouts'), pages: join(__dirname, './src/components/05-pages'), }, }), yml(), ],

Fig. 10: Twig namespaces reflecting project restructure.

Since we removed the react() function by using the snippet above, we can remove import react from '@vitejs/plugin-react' from the imports list as is no longer needed.

With all the configuration updates we just made, we need to rebuild the project for all the changes to take effect. Run the following commands:

npm run build npm run storybook

The components are available but as you can see, they are not styled even though each component contains a CSS stylesheet in its directory. The reason is Storybook has not been configured to find the component's CSS. We'll address this shortly.

4. Configure postCSS

What is PostCSS? It is a JavaScript tool or transpiler that turns a special PostCSS plugin syntax into Vanilla CSS.

As we start interacting with CSS, we need to install several node packages to enable functionality we would not have otherwise. Native CSS has come a long way to the point that I no longer use Sass as a CSS preprocessor.

  • Stop Storybook by pressing Ctrl + C in your keyboard
  • In your command line run this command:
npm i -D postcss postcss-import postcss-import-ext-glob postcss-nested postcss-preset-env
  • At the root of your theme, create a new file called postcss.config.js, and in it, add the following:
import postcssImport from 'postcss-import'; import postcssImportExtGlob from 'postcss-import-ext-glob'; import postcssNested from 'postcss-nested'; import postcssPresetEnv from 'postcss-preset-env'; export default { plugins: [ postcssImportExtGlob(), postcssImport(), postcssNested(), postcssPresetEnv({ stage: 4, }), ], };

Fig. 11: Base configuration for postCSS.

One cool thing about Vite is that it comes with postCSS functionality built in. The only requirement is that you have a postcss.config.js file in the project's root. Notice how we are not doing much configuration for those plugins except for defining them. Let's review the code above:

  • postcss-import the base for importing CSS stylesheets.
  • postcss-import-ext-glob to do bulk @import of all CSS content in a directory.
  • postcss-nested to unwrap nested rules to make its syntax closer to Sass.
  • postcss-preset-env defines the CSS browser support level we need. Stage 4 means we want the "web standards" level of support.

5. CSS and JavaScript configuration

The goal here is to ensure that every time a new CSS stylesheet or JS file is added to the project, Storybook will automatically be aware and begin consuming their code.

NOTE: This workflow is only for Storybook. In Drupal we will use Drupal libraries in which we will include any CSS and JS required for each component.

There are two types of styles to be configured in most project, global styles which apply site-wide, and components styles which are unique to each component added to the project.

Global styles

  • Inside src/base, add two stylesheets: reset.css and base.css.
  • Copy and paste the styles for reset.css and base.css.
  • Inside src/utilities create utilities.css and in it paste these styles.
  • Inside src/, create a new stylesheet called styles.css.
  • Inside styles.css, add the following imports:
@import './base/reset.css'; @import './base/base.css'; @import './utilities/utilities.css';

Fig. 12: Imports to gather all global styles.

The order in which we have imported our stylesheets is important as the cascading order in which they load makes a difference. We start from reset to base, to utilities.

  • reset.css: A reset stylesheet (or CSS reset) is a collection of CSS rules used to clear the browser's default formatting of HTML elements, removing potential inconsistencies between different browsers before any of our styles are applied.
  • base.css: CSS Base applies a style foundation for HTML elements that is consistent for baseline styles such as typography, branding and colors, font-sizes, etc.
  • utilities.css: Are a collection of pre-defined CSS rules we can apply to any HTML element. Rules such as variables for colors, font size, font color, as well as margin, sizes, z-index, animations, etc.

Component styles

Before our components can be styled with their unique and individual styles, we need to make sure all our global styles are loaded so the components can inherit all the base/global styles.

  • Inside src/components create a new stylesheet, components.css. This is where we are going to gather all components styles.
  • Inside components.css add glob imports for each of the component's categories:
@import-glob './01-atoms/**/*.css'; @import-glob './02-molecules/**/*.css';

Fig. 13: Glob import for all components of all categories.

NOTE: Since we only have Atoms and Molecules to work with, we are omitting imports for 03-organisms, 04-layouts, 05-pages. Feel free to add them if you have that kind of components.

Updating Storybook's Preview

There are several ways in which we can make Storybook aware of our styles and javascript. We could import each component's stylesheet and javascript into each *.stories.js file, but this could result in some components with multiple sub-components having several CSS and JS imports. In addition, this is not an automated system which means we need to manually do imports as they become available. The approach we are going to take is importing the stylesheets we created above into Storybook's preview system. This provides a couple of advantages:

  • The component's *.stories.js files are clean without any css imports as all CSS will already be available to Storybook.
  • As we add new components with individual stylesheets, these stylesheets will automatically be recognized by Storybook.

Remember, the order in which we import the styles makes a difference. We want all global and base styles to be imported first, before we import component styles.

  • In .storybook/preview.js add these imports at the top of the page around line 2.
import Twig from 'twig'; import drupalFilters from 'twig-drupal-filters'; import '../src/styles.css'; /* Contains reset, base, and utilities styles. */ import '../src/components/components.css'; /* Contains all components CSS. */ function setupFilters(twig) { twig.cache(); drupalFilters(twig); return twig; } setupFilters(Twig);

Fig. 14: Importing all styles, global and components.

In addition to importing two new extensions: twig and twig-drupal-filters, we setup a setupFilters function for Storybook to read Drupal filters we may use in our components. We are also importing two of the stylesheets we created earlier:

  • styles.css contains all the CSS code from reset.css, base.css, and utilities.css (in that order)
  • components.css contains all the CSS from all components. As new components are added and they have their own stylesheets, they will automatically be included in this import.
IMPORTANT: For Storybook to immediately display changes you make in your CSS, the imports above need to be from the src directory and not dist. I learned this the hard way.

JavaScript compiling

On a typical project, you will find that the majority of your components don't use JavaScript, and for this reason, we don't need such an elaborate system for JS code. Importing the JS files in the component's *.stories.js should work just fine. Since the demo components dont use JS, I have commented near the top of card.stories.js how the component's JS file would be imported if JS was needed.

If the need for a more automated JavaScript processing workflow arose, we could easily repeat the same CSS workflow but for JS.

Build the project again

Now that our system for CSS and JS is in place, let's build the project to ensure everything is working as we expect it.

npm run build npm run storybook

You may notice that now the components in Storybook look styled. This tells us our new system is working as expected. However, the Card component, if you used the demo components, is missing an image. We will address this issue in the next section.

This concludes the preparation part of this post. The remaining part will focus on creating automation tasks for compiling, minifying and linting code, copying static assets such as images, and finally, watching for code changes as we code.

6. Copying images and other assets

Copying static assets like images, icons, JS, and other files from src into dist is a common practice in front-end projects. Vite comes with built-in functionality to do this. Your assets need to be placed in the public directory and Vite will automatically copy them on build. However, sometimes we may have those assets alongside our components or other directories within our project.

In Vite, there are many ways to accomplish any task, in this case, we will be using a nice plugin called vite-plugin-static-copy. Let's set it up.

  • If Storybook is running, kill it with Ctrl + C in your keyboard
  • Next, install the extension by running:
npm i -D vite-plugin-static-copy
  • Next, right after all the existing imports in vite.config.js, import one more extension:
import { viteStaticCopy } from 'vite-plugin-static-copy';
  • Lastly, still in vite.config.js, add the viteStaticCopy function configuration inside the plugins:[] array:
viteStaticCopy({ targets: [ { src: './src/components/**/*.{png,jpg,jpeg,svg,webp,mp4}', dest: 'images', }], }),

Fig. 15: Adds tasks for copying JavaScript and Images from src to dist.

The viteStaticCopy function we added allows us to copy any type of static assets anywhere within your project. We added a target array in which we included src and dest for the images we want copied. Every time we run npm run build, any images inside any of the components, will be copied into dist/images.
If you need to copy other static assets, simply create new targets for each.

  • Build the project again:
npm run build npm run storybook

The missing image for the Card component should now be visible. Pretty sweet! 🍰

7. The Watch task

A watch task makes it possible for developers to see the changes they are making as they code, and without being interrupted by running commands. Depending on your configuration, a watch task watches for any changes you make to CSS, JavaScript and other file types, and upon saving those changes, code is automatically compiled, and a Hard Module Reload (HMR) is evoked, making the changes visible in Storybook.

Although there are extensions to create watch tasks, we will stick with Storybook's out of the box watch functionality because it does everything we need. In fact, I have used this very approach on a project that supports over one hundred sites.

I actually learned this the hard way, I originally was importing the key stylesheets in .storybook/preview.js using the files from dist. This works to an extend because the code is compiled upon changes, but Storybook is not aware of the changes unless we restart Storybook. I spent hours debugging this issue and tried so many other options, but at the end, the simple solution was to import CSS and JS into Storybook's preview using the source files. For example, if you look in .storybook/preview.js, you will see we are importing two CSS files which contain all of the CSS code our project needs:

import '../src/styles.css'; import '../src/components/components.css';

Fig. 16: Importing source assets into Storybook's preview.

Importing source CSS or JS files into Storybook's preview allows Storybook to become aware immediately of any code changes.

The same, or kind of the same works for JavaScript. However, the difference is that for JS, we import the JS file in the component's *.stories.js, which in turn has the same effect as what we've done above for CSS. The reason for this is that typically not every component we build needs JS.

A real watch task

Currently we are running npm run storybook as a watch task. Nothing wrong with this. However, to keep up with standards and best practices, we could rename the storybook command, watch, so we can run npm run watch. Something to consider.

You could also make a copy of the storybook command and name it watch and add additional commands you wish to run with watch, while leaving the original storybook command intact. Choices, choices.

8. Linting CSS and JavaScript

Our workflow is coming along nicely. There are many other things we can do but for now, we will end with one last task: CSS and JS linting.

  • Install the required packages. There are several of them.
npm i -D eslint stylelint vite-plugin-checker stylelint-config-standard stylelint-order stylelint-selector-pseudo-class-lvhfa
  • Next, after the last import in vite.config.js, add one more:
import checker from 'vite-plugin-checker';
  • Then, let's add one more plugin in the plugins:[] array:
checker({ eslint: { lintCommand: 'eslint "./src/components/**/*.{js,jsx}"', }, stylelint: { lintCommand: 'stylelint "./src/components/**/*.css"', }, }),

Fig. 17: Checks for linting CSS and JavaScript.

So we can execute the above checks on demand, we can add them as commands to our app.

  • In package.json, within the scripts section, add the following commands:
"eslint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "stylelint": "stylelint './src/components/**/*.css'",

Fig. 18: Two new npm commands to lint CSS and JavaScript.

  • We installed a series of packages related to ESLint and Stylelint.
  • vite-plugin-checker is a plugin that can run TypeScript, VLS, vue-tsc, ESLint, and Stylelint in worker thread.
  • We imported vite-plugin-checker and created a new plugin with two checks, one for ESLint and the other for Stylelint.
  • By default, the new checks will run when we execute npm run build, but we also added them as individual commands so we can run them on demand.

Configure rules for ESLint and Stylelint

Both ESLint and Stylelint use configuration files where we can configure the various rules we want to enforce when writing code. The files they use are eslint.config.js and .stylelintrc.yml respectively. For the purpose of this post, we are only going to add the .stylelintrc.yml in which we have defined basic CSS linting rules.

  • In the root of your theme, create a new file called .stylelintrc.yml (mind the dot)
  • Inside .stylelintrc.yml, add the following code:
extends: - stylelint-config-standard plugins: - stylelint-order - stylelint-selector-pseudo-class-lvhfa ignoreFiles: - './dist/**' rules: at-rule-no-unknown: null alpha-value-notation: number color-function-notation: null declaration-empty-line-before: never declaration-block-no-redundant-longhand-properties: null hue-degree-notation: number import-notation: string no-descending-specificity: null no-duplicate-selectors: true order/order: - - type: at-rule hasBlock: false - custom-properties - declarations - unspecified: ignore disableFix: true order/properties-alphabetical-order: error plugin/selector-pseudo-class-lvhfa: true property-no-vendor-prefix: null selector-class-pattern: null value-keyword-case: - lower - camelCaseSvgKeywords: true ignoreProperties: - /^--font/

Fig. 19: Basic CSS Stylelint rules.

The CSS rules above are only a starting point, but should be able to check for the most common CSS errors.

Test the rules we've defined by running either npm run build or npm run stylelint. Either command will alert you of a couple of errors our current code contains. This tells us the linting process is working as expected. You could test JS linting by creating a dummy JS file inside a component and writing bad JS in it.

9. One last thing

It goes without saying that we need to add storybook.info.yml and storybook.libraries.yml files for this to be a true Drupal theme. In addition, we need to create the templates directory somewhere within our theme.

storybook.info.yml

The same way we did for Storybook, we need to create namespaces for Drupal. This requires the Components module and storybook.info.yml configuration is like this:

components: namespaces: atoms: - src/components/01-atoms molecules: - src/components/02-molecules organisms: - src/components/03-organisms layouts: - src/components/04-layouts pages: - src/components/05-pages templates: - src/templates

Fig. 20: Drupal namespaces for nesting components.

storybook.libraries.yml

The recommended method for adding CSS and JS to components or a theme in Drupal is by using Drupal libraries. In our project we would create a library for each component in which we will include any CSS or JS the component needs. In addition, we need to create a global library which includes all the global and utilities styles. Here are examples of libraries we can add in storybook.libraries.yml.

global: version: VERSION css: base: dist/css/reset.css: {} dist/css/base.css: {} dist/css/utilities.css: {} button: css: component: dist/css/button.css: {} card: css: component: dist/css/card.css: {} title: css: component: dist/css/title.css: {}

Fig. 21: Drupal libraries for global styles and component's styles.

/templates

Drupal's templates' directory can be created anywhere within the theme. I typically like to create it inside the src directory. Go ahead and create it now.

  • Inside storybook.info.yml, add a new Twig namespace for the templates directory. See example above. Update your path accordingly based on where you created your templates directory.

P.S: When the Vite project was originally created at the begining of the post, Vite created files such as App.css, App.js, main.js, and index.html. All these files are in the root of the project and can be deleted. It won't affect any of the work we've done, but Vite will no longer run on its own, which we don't need it to anyway.

In closing

I realize this is a very long post, but there is really no way around it when covering these many topics in a single post. I hope you found the content useful and can apply it to your next Drupal project. There are different ways to do what I've covered in this post, and I challenge you to find better and more efficient ways. For now, thanks for visiting.

Download the theme

A full version of the Drupal theme built with this post can be downloaded.

Download the theme

Make sure you are using the theme branch from the repo.