Mario Hernandez: Adding Social Share Links to Gatsby

Sharing is caring.

I've been working on my personal blog (this site), for a while. I built it with Gatsby and little by little I have been adding extra functionality. Today I'm going to show you how I added social sharing links to allow visitors to share my posts with others using Twitter, Facebook, LinkedIn, and other channels.

For an example of the Sharing links, look at the icons above the hero image on this and every post on this site.

There are many ways to accomplish this but from the begining I wanted to use something that was simple and did not require too much overhead to run. There are solutions out there that require third libraries and scripts and I wanted to avoid that. A while back I was introduced to Responsible Social Share Links. The beauty of Responsible Social Links is that they do not need any Javascript to work. They use the sharing links available for most social media channels.

Let's take a look at some examples of what these links look like:

Facebook

https://www.facebook.com/sharer/sharer.php?u=URL_TO_SHARE

Twitter

<a href="https://twitter.com/intent/tweet/ ?text=Check this out &url=https://mariohernandez.io &via=imariohernandez" target="_blank">Share on Twitter</a>

Most of these links accepts several parameters. You can see these parameters in more details at the Responsible Social Share Links page for additional information. In addtion, some systems may require you to encode the links but luckily for us Reacts does this for us automatically.

Using the links in a Gatsby site (or React for that matter)

You may think, that's so easy, just modify each of the links with my personal information and done. That's true to an extend. However, the tricky part is dynamically passing the current page's URL and post title to your sharing link. So here's how I did it:

  1. Edit your blog post template. In my case my blog post template is /src/templates/blog-post.js This is based on the Gatsby starter I used. Your mileage may vary.

  2. Add the following code where you wish to display the sharing links to generate a twitter share link:

<Share> <ShareLink href={`https://twitter.com/intent/tweet/?text=${post.frontmatter.title} &url=https://mariohernandez.io${post.frontmatter.path}%2F&via=imariohernandez`}> // Optional icon <LinkLabel>Share on Twitter</LinkLabel> </ShareLink> </Share>

The example above creates a twitter share link and uses the data variables I am already using to print the blog post content. As you know, Gatsby uses GraphQL to query the posts and by doing this you have access to each of the fields in your post (i.e. title, path, tags, date, etc.).

In the example above, I am passing ${post.frontmatter.title} so when the post is shared the title of the post is included as your tweet text. In addition, I am linking to the current post by passing ${post.frontmatter.path}. Finally I am passing my twitter handle.

There are other parameters you can pass to your share links. Things like hashtags, mentions, and more. Following the same pattern you can do the same for Facebook, LinkedIn and others.

A much cleaner approach

You may have noticed that I created the sharing snippet directly in the blog-post.js template. A much cleaner approach would be to create a new React component for all yoru sharing links and include the component in your blog-post.js.

Here's the full snippet for all the social channels I am using:

<Share> <ShareLabel>Share this post</ShareLabel> <ShareSocial> <ShareItem> <ShareLink href={`https://twitter.com/intent/tweet/?text=${ post.frontmatter.title }&url=https://mariohernandez.io${post.frontmatter.path}%2F&via=imariohernandez`} > <span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-twitter" > <path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z" /> </svg> </span> <LinkLabel>Share on Twitter</LinkLabel> </ShareLink> </ShareItem> <ShareItem> <ShareLink href={`https://www.facebook.com/sharer/sharer.php?u=https://mariohernandez.io${ post.frontmatter.path }`} target="_blank" > <span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-facebook" > <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" /> </svg> </span> <LinkLabel>Share on Facebook</LinkLabel> </ShareLink> </ShareItem> <ShareItem> <ShareLink href={`https://www.linkedin.com/shareArticle?mini=true&url=https://mariohernandez.io${ post.frontmatter.path }&title=${post.frontmatter.title}&source=${post.frontmatter.title}`} target="_blank" > <span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-linkedin" > <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" /> <rect x="2" y="9" width="4" height="12" /> <circle cx="4" cy="4" r="2" /> </svg> </span> <LinkLabel>Share on LinkedIn</LinkLabel> </ShareLink> </ShareItem> </ShareSocial> </Share>

In Closing

If you want to have a clean and light weight way to share your content with others, the Responsible Sharing Links may just be what you need.

Mario Hernandez: Styling Forms Elements

Whether you are a veteran or just getting started with web development, working with web forms is not the sexiest thing but there is no avoiding them. Forms on a website are critical to the website's mission and the visitors using them. In this post I am going to show you how to properly style webforms and form elements so you can ensure they are consistent with your brand and provide a pleasant user experience.

Resources:

I have recording video tutorials on some of the concepts covered in this post. Scroll to the bottom if you are the kind of person who learns better watching tutorials.

Where to start

One thing about forms you can count on is that they are not consistent. In today's API era forms can be generated from any source and you don't always have control of their HTML. I personally work with Drupal a lot and even within it forms can originate from Views, Blocks, Nodes, Paragraphs, Modules, and more. I imagine this is the case with most Content Management Systems. So how do you address something where you can't always predict source or markup of forms?

If you can't beat them, join them

Whether is Drupal or other system, it is best to let that system dictate the markup of forms. Try to bend a system to your needs can prove challenging and not worth the trouble. This is the approach I take and so far this has worked very well for me.

Attributes are your friends

As a Front-End developer for many years, I love a good css class. They make my job a lot easier than not having a class. When it comes to form elements however, it is best to ignore classes and rely solely in attributes. What attributes you may be asking yourself? Let's take a look

From element types

The type attribute in form elements make it easy to style them in a global manner. Rather than styling form elements based on their css class or ID, the type attribute is the best approach. Each form field has a type attribute which makes it possible for browsers to render the element as expected. The type attribute can also play a factor in Javascript and form handling, but for now we are going to focus on styling.

  • Input fields: Input fields such as text fields, are probably the most common form elements in a form. But don't let its simplicity fool you because text fields come in many types; the most common being text. this is the type for a typical text box where you can type just about any kind of information. This sometimes leads to developers using it for anything including for data that should be in a different form type. Let's take a look at the various types a text field can be:

    • text: Used for most text fields and accept any string of text
    • email: Used to capture email address. Using this type is recommended for email addresses as it provides regular expressions to validate the data entered in the field meets the patterns of an email addresses. In addition, using this type of field makes it possible for your mobile devices to show a custom keyboard where the "@" sign is available without having to toggle your keyborad characters to find it.
    • password: As you can imagine, this makes it possible to hide passwords from view as you type them.
    • search: Elements of type search are text fields designed for the user to enter search queries into. These are functionally identical to text inputs, but may be styled differently by the user agent.
    • tel: Similarly to the email address, using this type allows for the keyboard on mobile devices to display numbers by default rather than alpha characters.
    • url: Similarly to email, this provides you with a custom keyboard on mobile devices that make it easier to type a common url.

As you can see from the examples above, text fields may seem simple but they come in all flavors. In most cases, the goal of form elements is to be styled the same across an entire site. So it is recommended you style these elements globally so no matter where they are used they automatically inherit the intended styles. So how do you style them globally? Let's take a look:

Here are a couple of examples of differnt input fields which we will style to ensure they all look similarly everywhere.

<input type="text" name="firstname"> <input type="password" name="password"> <input type="email" name="email">

Styling

input[type='text'], input[type='password'], input[type='email'], input[type='tel'], input[type='search'], textarea { background-color: #ffffff; border: 1px solid #000000; height: 40px; padding: 10px; width: 100%; } textarea { height: initial; }

By using the element's type attribute we can collectively style them with a single set of rules. As you may have noticed I included textarea as on of the elements because I want textareas boxes (multi-row textbox), to inherit all of the styles from text boxes with the exception of the height property; thus we override the height property while keeping all other styles the same as text boxes.

Button elements

Buttons are elements you typically want to display with consistent styles across your entire website. This includes buttons you may create yourself and those provided by the content management system you may be working with. So similarly to input fields above, we can't always rely on IDs or classes to style them. However, similarly to the input fields above, we can rely on the type attribute to style the different type of buttons in our site. Different type of buttons? yes, there are different types. Let's tae a look.

Markup

<a class='button'>I want to look like a button</a> <button>I'm an actual button</button> <input type='submit' value='Yo tambien'> <input type='reset' value='Me too'>
  • All elements above will have the same look and feel.
  • The first 3 elements should look identical, but the last one (type=['reset']) should look slightly different because it's not a primary button, it's more of a secondary button.
  • Sometimes this secondary button will be used to cancel or to reset someting on a page. While the top 3 will be used for important actions such as submit, save, etc.
  • Let's style them now

Styles

.button, button, input[type='submit'], input[type='reset'] { background-color: rebeccapurple; border-radius: 2px; border: 2px solid rebeccapurple; color: #ffffff; cursor: pointer; display: inline-block; font-size: 18px; line-height: 1; padding: 20px 40px; text-align: center; text-decoration: none; white-space: nowrap; margin-bottom: 20px; } .button:hover, .button:focus, button:hover, button:focus, input[type='submit']:hover, input[type='submit']:focus { background-color: rgb(126, 63, 189); /* lighter purple */ color: #ffffff; text-decoration: none; }
  • We are including all the button elements in our initial set of styles. The reason for this is that we want all the buttons (regardless of their type of function), to share the same shape, font size, font-weight, border width, hover behavior, and other properties.
input[type='reset'] { background-color: #ffffff; border-color: rebeccapurple; color: rebeccapurple; } input[type='reset']:hover, input[type='reset']:focus { background-color: #ffffff; border-color: #444444; color: #444444; }
  • Then for the reset type only, we are styling this button with white background and purple text. This will ensure this button will not stand out as much as the other buttons.

Radio buttons and Check boxes

Probably the hardest elements to style due to their dynamic nature, they have a hard time fitting in with the rest of HTML elements. Rather than link you to another blog post where you can copy and paste the necessary CSS to style these elements, I am walk you through the process of styling them.

Checkout the Codepen for these elements.

Markup

The typical markup for a checkbox or radio button looks like this:

<div class="checkbox__item"> <input type="checkbox" id="checkbox1" name="checkboxname" value="Item 2"> <label for="checkbox1">Item 2</label> </div> <div class="checkbox__item"> <input type="checkbox" id="checkbox2" name="checkboxname" value="Item 2"> <label for="checkbox2">Item 2</label> </div> <div class="radio__item"> <input type="radio" id="radio1" name="radioname" value="Item 2"> <label for="radio1">Item 1</label> </div> <div class="radio__item"> <input type="radio" id="radio2" name="radioname" value="Item 2"> <label for="radio2">Item 2</label> </div>

Styles

Out of the box, you can not style the native checkbox nor radio button elements. To be able to apply custom styles we need to do it in a hacky way. This is my favorite way to style these elements.

Drawing the checkbox

/* visually hide the native checkbox and radio buttons. */ input[type='checkbox'], input[type='radio'] { position: absolute; opacity: 0; }
  • By setting absolute position and opacity to zero, we are hiding the native radio and checkbox elements from view. However, we need the elements to still function as expected so using visibility-hidden or display: none are not viable options.
  • Although we are visually hiding the elements, they are still available in the DOM so we can interact with them and also so they can be available to assistive technologies such as screen readers.
/* Add space to the left of the label for later use. */ label { position: relative; padding-left: 35px; }
  • We set relative position on the label as we prepare to add pseudo elements in the next steps.
  • The left padding here will allow us to draw a box or a circle for the checkbox/radio button. More on this later.
/* Draw a square box with border using a pseudo element (`::before`). */ input[type='checkbox'] + label::before { border: 2px solid #e15b00; content: ''; display: inline-block; height: 24px; left: 0; position: absolute; top: -2px; width: 24px; }
  • First we define a sibling element of the checkbox with the + sign. The sibling element being the label element. If you look at the markup we wrote above, you will notice that the checkbox and the label are siblings. This is also true for the radio button and its label.
  • Using a ::before pseudo element on the label, we are drawing a 24px square box.
  • For any pesudo element to be visible on the page, we need to assign the property of content:, even if it's value is empty as shown above.
  • We then add a 2px border on the box. This box is what will be presented as the actual checkbox for the user to interact with (check/uncheck).
  • By setting the box with absolute position, we are positioning just to the left of the label. The position of the box is in relation to the label's position (hence relative position on the label).
/* Draw checkmark using a ::after pseudo elment on the label */ input[type='checkbox'] + label::after { border-bottom: 3px solid #fff; border-left: 3px solid #fff; content: ''; display: inline-block; height: 10px; left: 4px; position: absolute; top: 2px; transform: rotate(-45deg); width: 18px; }
  • The same way we drew the box previously, now we draw a checkmark symbol using a ::after pseudo element on the label. This means we are drawing the checkmark symbol after the label element.
  • In reality, we are drawing a rectangular box with left and bottom borders of 3px. By rotating the box negative 45 degrees, this makes it look like a checkmark.
  • We then use absolute position on the checkmark so we can position it right in the middle of the box we drew before.

Drawing the radio button

Before proceeding with the styles, let's repeat the steps above but this time for the radio buttons. These styles are extremely similar with the exception of in stead of drawing a square box, we will be drawing a circle with an outline, and instead of a checkmark, we will be drawing a filled circle in the middle of the outlined circle.

/* Draw a circle and add borders to it */ input[type='radio'] + label::before { border-radius: 50%; border: 2px solid #e15b00; content: ''; display: inline-block; height: 24px; left: 2px; position: absolute; top: -4px; width: 24px; } /* Draw an inner circle */ input[type='radio'] + label::after { background-color: #e15b00; border-radius: 50%; content: ''; display: inline-block; height: 20px; left: 6px; position: absolute; top: 0; width: 20px; }
  • This time we are targeting the input type of radio and we are using border-radius to draw a perfect circle.
/* Hide checkmark and inner circle by default */ .radio input[type='radio'] + label::after, .checkbox input[type='checkbox'] + label::after { content: none; }
  • By default, we don't want either the checkboxes or radio buttons to be checked. So we set the value of content: to none. This will present the checkboxes and radio buttons unchecked.
/* Show checkmark and inner circle when input is checked */ input[type='radio']:checked + label::after, input[type='checkbox']:checked + label::after { content: ''; }
  • Using the :checked pseudo class (input[type='radio']:checked, input[type='checkbox']:checked), we then show the checkmark or inner circle by setting the content: property's value to empty ('' i.e. removing none). The :checked pseudo class becomes available when the checkbox or radio buttons are click-checked and when that happens, then we show the checkmark or inner circle.
/* When box is checked, add background color. */ input[type='checkbox']:checked + label::before { content: ''; background-color: #e15b00; }
  • Since the checkmark we drew is white, we are adding a background color inside the checkbox to display the checkmark. We are doing this only when the :checked pseudo class is present.
/* Add focus styles when elements are in focus */ input[type='radio']:focus + label::before, input[type='checkbox']:focus + label::before { outline: rgb(59, 153, 252) auto 5px; }
  • Finally, using the :focus pseudo class, we are adding basic outline styles to show the "on focus" state of the inputs. We need to do this since we are not using the native input elements. If we were, the browser would automatically add these styles to our inputs.

Resources

I have recorded tutorials on severalof these things. Check them out if you are the kind of person who learns better through video tutorials.

Mario Hernandez: Handling Drupal attributes in components

In Drupal's twig templates you'll often see an attributes variable being output within the template. This variable is how core and contrib modules inject their CSS classes, an ID, or data attributes onto template markup. You'll also find title_prefix and title_suffix variables. These are used by core and contrib modules to inject markup into twig templates. A good example of this is the core Contextual Links module. If you were to remove the attributes , title_prefix , and title_suffix variables from a node template, for example, then the Contextual Links module would no longer have a way to add its drop-down to the display of nodes.

In some cases this may not be an issue for you, but in general it's best to plan to accommodate those Drupal-specific variables in your component markup so that when you integrate Drupal content into your components, other features can be available too.

Since the attributes variable can include class, id, and data attributes in one variable, we need to make sure we only combine Drupal’s classes with ours, and let the other attributes render without Drupal classes. This can be accomplished on the main wrapper of the component template.

<article class="card{{ attributes ? ' ' ~ attributes.class }}" {{ attributes ? attributes|without(class) }}> {{ title_prefix }} {{ title_suffix }} {% if image %} <div class="card__image"> {{ image }} </div> {% endif %} <div class="card__content"> {% if heading %} {% include '@components/heading/heading.twig' with { "heading": { "title": heading.title, "url": heading.url, "heading_level": heading.heading_level, "classes": 'card__heading' } } only %} {% endif %} </div> </article>

Note that the without twig filter in this example is a Drupal-specific filter, so for the component we'll want to make sure we’re using one that supports Drupal’s custom filters (most design systems such as KSS node, and Pattern Lab have configuration options that support Drupal twig filters).

Now if we integrate our card component with Drupal (i.e. node--card.html.twig), we can ensure Drupal's attributes and contextual links will be available when the component is rendered. The node template, also known as presenter template, would look something like this:

{% set rendered_content = content|render %} {% set heading = { title: label, url: url, heading_level: '4', attributes: title_attributes } %} {% embed '@components/card/card.twig' with { attributes: attributes, title_prefix: title_prefix, title_suffix: title_suffix, heading: heading, image: content.field_image is not empty ? content.field_image, } only %}
  • First we're triggering a full render of the content variable.
  • Then we set up a variable for the Heading field,
  • Finally we are using an embed twig statement to integrate the Card component. In the embed we are mapping all the Card fields with Drupal's data. We also pass in Drupal-specific items such as title_prefix, title_suffix, attributes, etc.

Our card component will be rendered with all Drupal's attributes and the ability to be edited inline thanks to the contextual links.

Hope this helps you in your component development journey.

Mario Hernandez: Five principles for building better components

When working on a component-based project, building components or patterns, can be a liberating experience because we are able to draft the best markup possible to build the most flexible, scalable and reusable components. If you work with third party platforms such as SharePoint, WordPress, Drupal, or others, this becomes even more rewarding as in most cases content management systems are not known for producing the best markup possible. However, this liberating experience can come a a price if you are not taking the appropriate measures and checks when building your library of patterns.
In this post I am going to discuss five critical principles I follow when building components. My goal is always to achieve first and foremost, semantic markup that works in all devices including assistive technologies, reusability, scalability, and finally, ease of use. In some cases it is hard to achieve all of these goals so it is important to understand where you draw the line and what is more important in the long term.

Disclaimer: Components and Paterns are used interchangeably.

Find patterns in your patterns

Ideally, before you begin building patterns, or components, you want to fully understand how and when these patterns will be used. Certainly there are times when we don’t have all this information when we first start working on a project, but if you do, take a detailed inventory of your patterns. Doing this will help you build better and more reusable patterns.
Failing to identify the patterns in your patterns can lead to bad decisions when building them. For example, let’s say we have a card component for displaying latest news articles, if we don’t know that there are cards for events that look similar to news articles, we may miss an opportunity to turn the card component into one we can use for multiple purposes in our website. But don’t let this get you down, building a website is an iteration process. If you later discover that the news card component can be leveraged for events, it’s perfectly okay to update the card component so it can be used with events as well. My point is that it is preferred if you can catch these patterns early on, but we all know that sometimes designs are not always available in full when a project is started and when they become available we may need to re-think our process of component-building.

Don't sacrifice simplicity over reusability

Reusability when working with components is important and should be at the forefront when building components, however, don’t paint yourself in a corner by over-engineering your components because you found a way to turn a component into a multi-level reusable machine. Trying to make your components do too much can lead to over complex development and in the long run, very hard to maintain. It is better to have components that are simple and easy to maintain even if it means you may have some components that are similar. It is hard to find the balance between simplicity and reusability and this discovery will only come as you work on your components. If you have to choose between simple and reusable, simple may be the way to go.

One set of markup for all devices

Earlier I mentioned that before you start building components you should have a full picture of how and when those components will be used by looking at the design comps. Of course this is not always possible, but in an ideally designs comps give us opportunities to identify areas of improvement or areas that could lead to potential problems. An example of a problem would be if you are provided the designs for mobile, tablet, and desktop. This is great, but let’s say the information displayed at each device type changes in such a way that you don’t know how to best write the markup so it works on all devices. In a perfect world, you should not have to alter the markup of your components per breakpoint. Doing so could lead to confusion on how content is rendered, not to mention problems with accessibility or even SEO. If you find that markup may be a challenge from breakpoint to breakpoint, it is perfectly okay to go back to the designer and propose updates to the design so a unified markup structure could serve all devices. In my experience, the designer would be okay with your recommendations as long as you are not completely changing the layout of the website but instead are simply proposing minor adjustments that don’t change the overall look and feel of the website.
In some cases a designer may not be fully aware that the designs they are proposing could lead to issue when the site is rendered in different device sizes, so it’s okay to bring these issues to their attention and you may find that they are onboard with your suggestions.

Find the right names for your patterns

Another way to paint yourself in a corner is by the name you choose for your components. Component names should be generic and yet descriptive so they can be used on different scenarios. Conversely, names that are too specific will limit when and how you can use those components. Let’s take a look at some bad component names and propose a better name:

  • News Card: If we think back of how we can reuse components, naming a component News Card will limit us on what kind of content we can use this component on. What if Events use a similar card with minor differences? A better name for this component may be Card or Content Card. This allows us to use on any type of content.

  • FAQ Accordion: We have seen this before, we want to build a collapsible list of Frequently Asked Questions so the name makes sense. However, what if we want the same functionality for other type of content such as Forums or Knowledge base content? A better name would simply be Accordion which describes the functionality of the component, not the content that it can hold.

  • Latest News List: Again, we are limiting ourselves to only showing content of type news. A list of content should be generic so we can use them for any kind of content (i.e. Blog posts, events, popular content etc.). A more appropriate name would be Content List.

  • Finally, Homepage Hero: It is not uncommon for Hero sections to be in multiple pages other than the homepage. By naming this component simply Hero, we can leverage it and use it in more than just the homepage. You may wonder, what if I have other types of heroes in non-homepage pages? Well, perhaps we can find alternative names for those, things like Hero Short, or Page Breaker, something that is descriptive and yet provides the ability to be used in multiple pages.

The above are only some examples of how a bad name can not only limit where or what type of content you can use your components with, but it can also lead to building unnecessary components.

Document your components

One of the great things about design systems like Pattern Lab, Storybook and others, is that they provide a way to document the details of your components. Documenting components can go a long way to ensure your entire team is fully aware of the why and how of your components.
I practice that I follow when building components is documenting the technical aspects of the component such as its name, where it is used, and details about any variants of the components. Just as important, I like to define the components fields, their data type and values they accept. This architectural information ensures gives your team and your stakeholders a detailed view of a components role in your project.

In closing, before you begin building components, take the time to study your design comps and take notes of how you feel you can make the most out of your components library. You will not get it right the first time, that’s okay. As long as you are giving yourself the room to iterate through the process, you can always go back and update a component if it will give you more flexibility and will turn your project into a solid component-based system.

In closing

You may not always get it righ the first time, but keeping these principles in mind will help you achieve better implementation of your website's patterns.

Mario Hernandez: Building a Drupal Theme with the Theme Generator

On one of my last training workshops I took a chance and decided to let students pick their environment of choice to use during training. As always I hosted an online call prior to training to assist anyone who needed help setting up their environment. About five of the students showed up to the call. This is a first. In the past when I've used a preconfigured training environment typically no one shows up to this prep call because the environment I've put together for them has been fully tested and any potential issues have been addressed.

Although I was able to help everyone get ready for training, and no big issues were encountered during training, I learned that perhpas having a preconfigured training environment is the best way to go. Having done this in the past I found that a preconfigured environment not only provides a consistent experience for everyone but it makes things more predictable for everyone.

I'm going to show you the latest setup I am using when training people. This is a new setup I put together using DDev with a host of other tools including Drupal

See the Theme Generator's project page on Github.

Watch the full tutorial below:

Mario Hernandez: Our best training yet

For the past few years we've been working on a training curriculum around Component Based Development and every time we finish updating it for an upcoming event we feel really good about it and think we finally got it. In reality, a training curriculum for a topic so complex as Component Development is probably never finished.

After BADCamp's training workshop last year we thought we were done updating the training material and we felt it was so good that it would carry us all the way to DrupalCon Seattle 2019. We could not have been more wrong. Soon after BADCamp we made the decision to move from KSS Node to Pattern Lab for handling our pattern's workflow and living styleguide. This meant our entire training material needed to be updated in time for DrupalCon. Needless to say we spent months ensuring the material was current, relevant and effective.

DrupalCon Seattle was a total success. Our training sold out in about two weeks and end up with almost 50 people in attendance which is probably a record for us. But with more students the chances of more issues or something going wrong increase. We make a big effort to ensure we provide an automated environment for student to use. This allows us provide all tools needed during training pre-configued and tested ensuring a more predictable and consistent behavior by students' systems. However, there are always cases in which a student decides not to use our environment and either due to technical skills or strict restrictions on their systems they need to use a workflow we have not tested.
This was certainly the case at DrupalCon and we had to find ways to help those students to be able to follow along during training.

One way to help people with their local setup is by conducting what we call "Office Hours". We reserve an hour or two weeks before the training where anyone who needs assistance with their local environment can join us online and we attempt to get them up and running. We helped several individuals this way. Although it is more work for us it allows us to address any issues off hours rather than spending valuable training time fixing computer problems.

Image removed.

The training was engaging and we received great feedback. Our curriculum, although not perfect, is relevant and well received. Having done this a few times at small and large events gives us confidence and makes it easier next time we do it.

Our team is dedicated and passionate about training and this would not be possible without them. Especial thanks go to Eric Huffman who has done most of the heavy lifting with the local environment automation. Kelly Dassing, who as a Director of Project Management provides a unique perspective to those in attendance who may not be developers. Tobias Williams who is an all around great FE develper and Engineer, and Mark Casias, who is able to answer the hard Back-end question that the rest of the team may not be as proficient as he is.

Should you have training needs please reach out and we would be happy to put together a curriculum that fits your specific needs or that of your team.

For a more in-depth look at how we prepare for a training workshop ready my blog post on Planning for an Effective Training Workshop.

Until next time ... Cheers!.

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

The Drop Times: Shawn Perritt on Reintroducing One of the Pioneers of the Internet to the World

Discover the transformative journey of Drupal's rebranding in an exclusive interview with Shawn Perritt, Brand and Creative Director at Acquia. During DrupalCon Portland, Shawn unveiled the refreshed Drupal brand, highlighting the collaborative efforts to modernize its identity while preserving its core values. In conversation with Alka Elizabeth, Sub-editor at The DropTimes, Shawn shares insights into the strategic thinking behind the rebrand, the unique challenges faced, and the aspirations driving this significant change. Join us for an in-depth look at how Drupal is poised to reintroduce itself to the world.