Our new website uses a lot of interesting new technology. It is built using the JAMstack methodology, which stands for:
- JavaScript
- APIs
- Markup
What this means in reality is that Drupal is used for content management, Drupal then serves this content using JSON API (now in Drupal core) and this API is consumed by Gridsome to build the front end.
Gridsome
Gridsome is an up and coming VueJS based system for building PWA and PRPL compliant sites. It started out as a VueJS clone of the famous Gatsby, including the same concept of consuming an API (or flat files, markdown, CSVs etc) and converting it into GraphQL which can be used as the data source of the site.
This build process frees up the developer from worrying about the CMS and Drupal's 'not wonderful' theming layer and allows them to focus on developing the front end in a component based approach.
The benefits of a PWA (Progressive Web App) and PRPL compliance (Push, Render, Pre-cache, Lazy-load) is a blazing fast site (100 in Google lighthouse speed scores) that feels great to navigate for the user.
Gridsome renders a static site (set of HTML files) but it also 'hydrates' it with JavaScript after the initial load. This means you have all the power of VueJS with the benefit of the speed of flat HTML files, especially for the initial page load. Links are prefetched using IntersectionObserver so navigating between pages feels instant.
Agile Collective website
We knew we wanted to try building a decoupled Drupal site so we looked at various front end systems that could consume the API and build out a static site. As James and I had just completed the Bonn Challenge Barometer project and had a really positive experience working with VueJS I was keen to find something that would allow me to work in VueJS again. The two big players in the JAMstack arena are Nuxt (VueJS) and Gatsby (React). A newer system just coming onto the scene was Gridsome (VueJS). While evaluating Gridsome I was impressed with the support from the two main developers and the inclusive community and I found myself being drawn to that system more than the others.
At the time of starting the site build Gridsome was on 0.4 version so using it was definitely a risk. But due to the support of the Gridsome team I found that anytime there were blockages the developers would willingly help out and unblock the issue, either by finding a workaround or by writing a feature. As the needs of the site are fairly simple I felt the risks were mitigated by the support and this feeling has proven true.
Drupal plugin for Gridsome
One of the areas I was able to help out with was the Drupal source plugin for Gridsome. A source plugin consumes data from a source and adds it to GraphQL in Gridsome. With the source plugin written (by Matthew King) I was able to get started building the site.
Developer experience
Building applications in VueJS is a very pleasant experience coming from Drupal. This is mostly because of the .vue file format which splits a file into three sections, the template section with the html, the script section with the JavaScript and the style section with the css styles. VueJS strongly encourages a developer to think in terms of components which really helps with re-usability and single purpose components.
Building in Gridsome adds another section to the .vue file which is the page-query (or static query) section. This is a GraphQL query which provides the data for the component. This might look something like this for a simple Drupal Taxonomy Term query:
<static-query>
query Work {
terms: allDrupalTaxonomyTermWorkSector (sortBy: "title" order: ASC) {
edges {
node {
title
id
}
}
}
}
</static-query>
The result of this query is then available in the template section inside the $static variable.
On our site we have a page showing a grid of our previous work. Above it is a list of taxonomy terms which are used to filter the result set. To output that list we make use of the GraphQL results from the example above like this:
<ul class="projects-filter__list">
<li
v-for="edge in $static.terms.edges"
:key="edge.node.id"
class="projects-filter__item">
<a
href="#"
class="projects-filter__link"
@click="e => updateFilter(e, edge.node.id)">
{{ edge.node.title }}
</a>
</li>
</ul>
That produces the list of filter links and attaches the click handler which triggers the update.
Structured content
In Drupal we like to use the Paragraphs module to allow editors to structure their content. We kept this approach in the new site and I developed a method to have each Paragraph type be it's own VueJS component. This makes for great re-usability and control over the output.
A very simple example of a query which fetches the data for a Paragraph type follows:
<page-query>
query Post {
post: drupalNodePage (path: "/page-who-we-are") {
title
pagetitle
excerpt
content_sections {
name: __typename
...on Node {
...on DrupalParagraphText {
id
text {
processed
}
}
}
}
}
}
</page-query>
Using the result of this query in a loop we can use a dynamic component with the `:is` attribute and pass in the name of the paragraph type. VueJS will then try to load that named component:
<main class="main">
<h1>{{ $page.post.pagetitle}}</h1>
<component
v-for="contentSection in $page.post.content_sections"
:is="contentSection.name"
:fields="contentSection"
:key="contentSection.id"/>
</main>
These components need to be imported and declared:
<script>
import DrupalParagraphText from '@/components/ParagraphText.vue'
export default {
components: {
DrupalParagraphText,
}
}
</script>
The component itself receives the props `fields` which contains the field values to be output (see content_section in the query). This is the component (`/components/ParagraphText.vue`)
<template>
<section
v-if="fields && fields.text && fields.text.processed"
class="content-section content-section--text"
v-html="fields.text.processed" />
</template>
<script>
export default {
props: [
'fields'
],
}
</script>
Component building blocks
By constructing the backend using Drupal content types and Paragraphs we can match this up nicely with Gridsome templates (which match content types) and VueJS components (which match Paragraphs). This allows for a lot of reusable components in the site and the more custom pages are easily created as pages in Gridsome.
Our `src/components` folder in Gridsome looks like this:
├── Address.vue
├── BackLink.vue
├── CallToAction.vue
├── Clients.vue
├── ContentSectionBase.vue
├── Footer.vue
├── Header.vue
├── Logo.vue
├── ParagraphAccordion.vue
├── ParagraphAccordionPaneText.vue
├── ParagraphCodeBlock.vue
├── ParagraphCustomComponent.vue
├── ParagraphImage.vue
├── ParagraphTestimonial.vue
├── ParagraphText.vue
├── ParagraphTextImage.vue
├── ParagraphThreeImages.vue
├── ParagraphTitleAndSubtitle.vue
├── People.vue
├── ResponsiveImage.vue
├── Splash.vue
├── TastyBurgerButton.vue
└── WorkList.vue
Address.vue is a reusable component (used on nearly every page) as well as all the Paragraph components. The `ResponsiveImage.vue` component is used whenever there is a large image to be shown where we want lazy loading and multiple sizes for different browser widths and pixel densities.
In the templates folder we have:
├── DrupalNodeBlogPost.vue
├── DrupalNodePage.vue
├── DrupalNodePerson.vue
└── DrupalNodeWork.vue
These are a Gridsome concept where if a Drupal node's content type name matches one of those files it is used to display the node on a specified route. In `gridsome.config.js` we specify the routes for these content types like this:
routes: {
'node--person': '/our-people/:title',
'node--blog_post': '/blog/:slug',
'node--page': '/:slug',
'node--work': '/our-work/:pagetitle'
}
Finally we have specific pages we want more control over and these live in `/src/pages`:
├── Blog.vue
├── Contact.vue
├── Index.vue
├── OurWork.vue
└── WhatWeDo.vue
The filename is used to match to a route, so `OurWork` matches `/our-work`.
With this combination of components, pages and templates we can create more complex sites and still have the benefit of reusable components that are easy to extend. If a template doesn't do everything a specific page needs then an individual page component can be created which will override a matching template on that route.
Images
One area where Gridsome shines is in displaying images, optimised nicely and lazy loaded. Unfortunately the `<g-image>` component only currently works with local images which have been declared in GraphQL properly. For our purposes we needed to access Drupal's media library for our images and at the time of writing this post the most sensible approach was to handle images ourselves.
I wrote some code which uses a Gridsome hook `beforeBuild` which fetches any referenced media image files from the Drupal backend and stores them locally. It also fetches all possible image styles for that image and stores them. Finally it fetches a `webp` optimised version of the file and stores that locally too.
We have a component called `ResponsiveImage.vue` which is used repeatedly through the site for displaying images. This component is passed some props and then processes them to output a `<picture>` element with a `webp` source and a `jpeg` fallback source. It uses the lazy sizes library to lazy load the image which really helps with initial page load speed. The `webp` version of the image has fantastic quality to file size ratio and the `jpeg` is still there as a backup (for Safari mostly).
<template>
<div :class="`responsive-image responsive-image--${stylename}`">
<picture>
<source
:data-srcset="srcset('.webp')"
type="image/webp" />
<source
:data-srcset="srcset()"
type="image/jpeg" />
<img
data-sizes="auto"
:src="fallbackImage()"
:alt="alt"
class="lazyload" />
</picture>
</div>
</template>
<script>
export default {
props: {
stylename: {
type: String,
},
filename: {
type: String,
},
set: {
type: Object,
},
sizes: {
type: String,
},
fallback: {
type: String,
},
alt: {
type: String,
}
},
methods: {
srcset(ext = '') {
const set = this.set
const srcset = [];
for (const [size, width] of Object.entries(set)) {
srcset.push(`/files/${this.stylename}_${size}-${this.filename}${ext} ${width}`)
}
return srcset.join(', ')
},
fallbackImage: function() {
return `/files/${this.stylename}_${this.fallback}-${this.filename}`
},
},
}
</script>
<style lang="scss">
.responsive-image {
img {
display: block;
max-width: 100%;
width: 100%;
}
}
</style>
Gridsome has remote images as a feature request so we should see an alternative to this approach soon.
Satisfaction
Developing this web site has been a satisfying process. The experience of writing components with styles and functionality bundled with them is much more intuitive than I initially thought. GraphQL has been interesting to learn the basics of and is a simple but powerful language. Drupal's JSON API module (now in Drupal core) outputs standardised JSON and is really easy for a front end to consume.
While the JAMstack approach is flexible and produces very fast web sites, it's not necessarily suitable for every project. There are still some hard issues to work out, like having a menu system, but for relatively simple sites the experience of developing with Drupal as the backend and Gridsome/VueJS as the front end has been very satisfying.