I dipped my toes into Gatsby waters a bit last year and it was pretty fun, but I felt I didn't really know what I was doing. So, when the missus asked for a new website (end result here) a couple of months ago, I thought it might be a good opportunity to really get under the hood.
It turned out to be a great learning experience since I mostly used to work in backend Drupal/PHP but also because my wife had some serious requirements (tough client). I tried talking her out of some of it (Multilingual? You really need that? Webforms? ..That's soooo 90'ies). Anyway, that didn't work... Here are some things I learned along the way:
My respect for frontend was renewedI knew things in the frontend world are moving fast these days, so I braced myself and tried figuring out best practices. At first, reading the official Gatsby styling documentation I thought CSS modules was the way to go. Automatically scoped to each component which sounds good in component driven development.
Until I showed my first setup to a CSS specialist and he threw up in his mouth a bit when he looked at the output like this:
<h2 class="index-module--subheader--1NIJh">Example</h2>
I understand how CSS modules might be a good solution, especially when you are embedding your app in a third-party app. But my guess is CSS modules are popular because they are "safer". Which means less can go wrong if you don't know what you're exactly doing. Anyway I'm not the only one who thinks CSS modules should not be your default choice.
So what are the options? There are (at least) four ways to style a react app. Eventually I went with the regular CSS stylesheets method. I just added SASS and PostCSS to the mix, because I want to use those fancy @media-custom queries!
Now I can scope-style my components, but I can also use inheritance and of course all the SASS/PostCSS goodies. Yay!
gatsby-config.js
{
resolve: `gatsby-plugin-sass`,
options: {
postCssPlugins: [
require(`postcss-preset-env`)({
stage: 1,
}),
],
},
},
Some other things I got around using were CSS Grid and browserlist.
Understanding Gatsby means understanding React##In the end Gatsby is a React app. Of course I just dived in without learning React properly. I mean: how hard can it be ;)? Turns out you can get pretty far without understanding some fundamental React concepts but at one point you will get very frustrated. The most important thing I learned was understanding the difference between Functional and Class components (and then I found this [post](Like this ) so at least I'm not the only one).
Apparently the whole react community is now into hooks and functional design instead of classes, which is a bit weird coming from the Drupal world which had the exact opposite trend going full OOP. But it's okay, I get it, hooks do look easier/less complex. It's fine, I actually like them now.
Apparently the whole React community is now into hooks
Another thing to understand is the difference between server-side build time and client-side runtime. Gatsby renders the site with node on the server during build time. Then the site is hydrated into a React app and you can use the regular React functionality during runtime. It may sound obvious, until you try to use libraries which use the DOM on build time.
Also, I seriously needed to level up my javascript skills. All that fancy ES6 goodness (arrow functions, destructering, map, reduce), I like it! For example you might find deep property checking useful. Even stuff we love from PHP like the coalesce operator (??) is coming to ES2020/Babel 7.8.x.
I chose Graphql instead of JSON API##There are multiple ways to get your content from Drupal into Gatsby. The recommended and best supported way to do this is using the gatsby-source-drupal plugin, which uses JSON API to pull the data in. Most guides also seem to use this plugin. But most guides show how to build simple sites and when I started out using JSON API I quickly discovered the limitations when dealing with translated content: JSON API in core currently does not provide a uniform language negotiation mechanism. There is a meta issue and you'll find workarounds people used specifically for Gatsby in the comments there, but looping through several endpoints (/en/jsonapi, /nl/jsonapi) to get the translated content seemed way to hackish.
So, what about Graphql? People are using Graphql as an endpoint instead of JSON API succesfully. Could we use one endpoint and query for our languages? Turns out you can quite easily.
This way we can generate all the translations for each node, each with its alias from Drupal:
gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const {createPage} = actions;
const langcodes = ['nl', 'en'];
for (const langcode of langcodes) {
await graphql(`
query ($langcode: [String], $language: Drupal_LanguageId!) {
drupal {
nodeQuery(filter: {conditions: [
{field: "type", operator: EQUAL, value: "article"},
{field: "langcode", operator: EQUAL, value: $langcode},
{field: "status", operator: EQUAL, value: "1"}
]
}
) {
entities {
... on Drupal_NodeArticle {
entityTranslation(language: $language) {
entityId,
entityUrl {
path
}
}
}
}
}
}
}
`, {langcode: langcode, language: langcode.toUpperCase()}
).then(result => {
result.data.drupal.nodeQuery.entities.map((entity) => {
// Fallback to node/#id urls if no path alias is provided.
if (entity.entityTranslation.entityUrl.path === null) {
entity.entityTranslation.entityUrl.path = `\\node\\{entity.entityTranslation.entityId}`;
}
createPage({
path: `${entity.entityTranslation.entityUrl.path}`,
component: path.resolve(`./src/templates/article.js`),
context: {
id: entity.entityTranslation.entityId,
languageId: langcode.toUpperCase(),
},
})
});
});
}
}
You can use variables like this in the createPages API and pass pageContext variables to the templates (like I did here with the entity ID) but you can't use variables in staticQueries. That's the difference between the two and it gets a bit annoying when you want to build a component which queries data based on a variable like language.
But a more common problem is the image component. Sooner or later many developers want a more generic image component with a dynamic filename. Fortunately, work is in progress to allow for dynamic queries.
Oh and don't forget to add some kind of authentication (simple_oauth works fine) on your endpoint.
But where are my images?When I previously used the gatsby-source-drupal plugin, I was used to the fantastic options childImageSharp provides for images pulled from Drupal. But the graphql fields were not available on my image fields when I used the graphql source plugin.
That's because images from Drupal won't be automatically available in your Gatsby graphql if gatsby-source-graphql is used. There are ongoing efforts to rewrite the Graphql source plugin for Gatsby which would hopefully also provide a better way to extend third-party schemas, but for now you can easily add the image fields using another API of Gatsby: createResolvers. In the example below the field 'imageGatsbyFile' becomes available in Graphql.
gatsby-node.js
exports.createResolvers = (
{
actions,
cache,
createNodeId,
createResolvers,
store,
reporter,planning
},
) => {
const {createNode} = actions;
createResolvers({
Drupal_FieldNodeArticleFieldImage: {
imageGatsbyFile: {
type: `File`,
resolve(source, args, context, info) {
return createRemoteFileNode({
url: source.url,
store,
cache,
createNode,
createNodeId,
reporter,
})
},
},
},
});
Graphql mutations to the rescue
I knew webforms would become a challenge. At first, I was hosting/building the site with Netlify and thought I would use their forms support, which is actually very easy to implement and works pretty well. But there are limitations on a free tier, and I also wanted an editors to be able to view the submissions in Drupal like they're used to.
Existing solutions are gatsby-drupal-webform but it uses REST + JSON API and I wanted to stick with only my Graphql endpoint. Hanging around the #gatsby and #graphql channels in the Drupal Slack I noticed I was not the only one figuring this out. In the end I used the graphql_webform Drupal module + Formik/Yup on the React side. You can see a form in action on the contact page.
This allows for retrieving specific forms and fields and submitting them using a Graphql mutation. However, in my case form configuration is still hardcoded in specific components in Gatsby. I'm planning to create a plugin which will automatically create the webforms from Drupal in Gatsby.
I learned to use the context API for i18n##Remember understanding React? At one point you will need to communicate between components and props won't cut it. That's where the Context API gets really useful.
Building a multilingual site I needed a way to store the current language (based on URL prefix in my case) so components would know which language to use to show translated strings, the way t() works in Drupal. I used the Context API to store the language using i18next directly without using additional frameworks like react-i18next which seem to be overkill for my usecase.
import React, {useState} from 'react';
import i18next from "../i18n/config";
const SiteContext = React.createContext({});
// We store the i18next object for multilingual purposes.
const Provider = props => {
// So, i18next does not really provide a list of languages which was a bit unclear.
// We can't use i18next.languages for this
// See https://github.com/i18next/i18next/issues/1068
const languages = {
'en': 'English',
'nl': 'Dutch'
};
// We set the site language based on a language prefix. So find the
// available language to set the current language.
const currentLanguageByPath = () => Object.keys(languages).find(langcode => props.path.startsWith(`/${langcode}`)) ?? 'en';
i18next.changeLanguage(currentLanguageByPath());
return (
<SiteContext.Provider value={{ i18next }}>
{props.children}
</SiteContext.Provider>
)
};
export default ({ element, props }) => (
<Provider {...props}>
{element}
</Provider>
);
export { SiteContext }
Adding this context using gatsby-ssr.js and gatsby-browser.js with wrapPageElement allows us to retrieve the i18next object from any component:
import {SiteContext} from "../../context/site-context";
const MyComponent = () => {
const siteContext = useContext(SiteContext);
const i18next = siteContext.i18next;
return (
<p>{`${i18next.t('Current language')}: ${i18next.language}`}</p>
)
}
This will be particularly powerful when we can use dynamic queries with variables in components, as mentioned before. By the way: string translations can be stored in json files, equivalent to Drupal's .po files. See i18next documentation.
Optimize all the thingsI'm still finetuning a lot of things. For instance, Gatsby is pretty fast out of the box but if you want a near 100 score in lighthouse you need to micro-optimize. I chose not to put all the CSS in the head (I mean wtf) and add additional breakpoints for my images. There are some other optimizations on the backlog.
I also expanded my helmet/meta component with some SEO improvements for social cards and have a backlog with smaller issues, but for now I'm pretty happy with the result.
Any thoughts, tips or improvements are welcome in the comments. Which bythe way are now Disqus but I'm looking into other options.