Wat ik leerde van een meertalige Gatsby-site met Drupal
Vorig jaar had ik een beetje met Gatsby gespeeld. Dat was leuk, maar ik had niet echt het gevoel te weten wat ik deed. Toen mijn vrouw een paar maanden geleden om een nieuwe website vroeg (hier het eindresultaat), leek me dat een goede gelegenheid om echt onder de motorkap te kijken. Het werd een leerzame ervaring, omdat ik vooral aan de backend met Drupal/PHP werkte, maar ook omdat mijn vrouw behoorlijk wat eisen had (lastige klant). Ik probeerde haar sommige dingen uit het hoofd te praten. Meertalig? Heb je dat echt nodig? Webforms? Dat is zó jaren negentig. Maar goed, dat werkte niet... Dit zijn een paar dingen die ik onderweg leerde:
Mijn respect voor frontend kreeg een nieuwe impuls
Ik wist dat de frontendwereld tegenwoordig snel verandert, dus ik zette me schrap en probeerde de best practices te vinden. Toen ik de officiële Gatsby-documentatie over styling las, dacht ik eerst dat CSS Modules de juiste keuze waren. Automatisch beperkt tot elk component: dat klinkt goed bij componentgericht ontwikkelen.
Ik begrijp hoe CSS Modules een goede oplossing kunnen zijn, vooral als je je app in een app van een derde partij opneemt. Maar ik vermoed dat ze vooral populair zijn omdat ze “veiliger” zijn: er kan minder misgaan als je niet precies weet wat je doet. Ik ben in ieder geval niet de enige die vindt dat CSS Modules niet je standaardkeuze zouden moeten zijn. Wat zijn de opties dan? Er zijn minstens vier manieren om een React-app te stylen. Uiteindelijk koos ik voor gewone CSS-stylesheets. Ik voegde alleen SASS en PostCSS toe, want ik wil die mooie @media-custom queries gebruiken! Nu kan ik mijn componenten gericht stylen, maar ook overerving en alle SASS/PostCSS-extra's gebruiken. Hoera!
gatsby-config.js
{ resolve: `gatsby-plugin-sass`, options: { postCssPlugins: [ require(`postcss-preset-env`)({ stage: 1, }), ], }, },
Andere dingen die ik ben gaan gebruiken, zijn CSS Grid en browserlist.
Gatsby begrijpen betekent React begrijpen
Uiteindelijk is Gatsby een React-app. Natuurlijk dook ik er gewoon in zonder React eerst goed te leren. Hoe moeilijk kan het zijn ;) ? Je blijkt behoorlijk ver te komen zonder een aantal fundamentele React-concepten te begrijpen, maar op een gegeven moment raak je flink gefrustreerd. Het belangrijkste wat ik leerde, was het verschil tussen functionele componenten en klassecomponenten. Daarna vond ik dit artikel, dus ik ben in ieder geval niet de enige. Blijkbaar is de hele React-community nu bezig met hooks en functioneel ontwerpen in plaats van classes. Dat voelt wat vreemd vanuit de Drupal-wereld, waar juist de tegenovergestelde beweging naar volledig OOP plaatsvond. Maar goed, ik snap het: hooks zien er eenvoudiger en minder complex uit. Inmiddels vind ik ze eigenlijk best fijn.
Blijkbaar is de hele React-community nu bezig met hooks.
Ook moet je het verschil begrijpen tussen buildtijd op de server en runtime in de browser. Gatsby rendert de site tijdens de build met Node op de server. Daarna wordt de site gehydrateerd tot een React-app en kun je tijdens runtime de normale React-functionaliteit gebruiken. Dat klinkt vanzelfsprekend, totdat je tijdens de build libraries probeert te gebruiken die de DOM nodig hebben. Ik moest ook mijn JavaScript-kennis flink bijspijkeren. Al die mooie ES6-mogelijkheden, zoals arrow functions, destructuring, map en reduce: ik ben fan! Zo kan controleren van diep geneste eigenschappen handig zijn. Zelfs dingen die we uit PHP kennen, zoals de coalesce-operator (??), komen naar ES2020/Babel 7.8.x.
Ik koos GraphQL in plaats van JSON API
Er zijn meerdere manieren om content uit Drupal in Gatsby te krijgen. De aanbevolen en best ondersteunde is de plugin gatsby-source-drupal, die de data via JSON API ophaalt. De meeste handleidingen gebruiken die ook. Maar de meeste laten zien hoe je eenvoudige sites bouwt. Toen ik met JSON API begon, liep ik bij vertaalde content al snel tegen beperkingen aan: JSON API in core biedt momenteel geen uniforme taalonderhandeling. Er is een meta-issue, met in de reacties workarounds die mensen specifiek voor Gatsby gebruiken. Maar meerdere endpoints (/en/jsonapi, /nl/jsonapi) doorlopen om vertalingen op te halen leek me veel te gekunsteld. Hoe zit het met GraphQL? Mensen gebruiken GraphQL met succes als endpoint in plaats van JSON API.
Kunnen we één endpoint gebruiken en onze talen opvragen? Dat blijkt vrij eenvoudig te kunnen. Zo kunnen we alle vertalingen van elke node genereren, elk met de eigen alias uit 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, language: langcode.toUpperCase() } ).then(result => { result.data.drupal.nodeQuery.entities.map(entity => { 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(), }, }); }); }); } };
In de createPages-API kun je op deze manier variabelen gebruiken en pageContext-variabelen doorgeven aan de templates, zoals ik hier deed met de entiteits-ID. In staticQueries kan dat niet. Dat is het verschil tussen de twee, en het wordt wat vervelend als je een component wilt bouwen dat data opvraagt op basis van een variabele zoals de taal.
Een vaker voorkomend probleem is het afbeeldingscomponent. Veel ontwikkelaars willen vroeg of laat een algemener afbeeldingscomponent met een dynamische bestandsnaam. Gelukkig wordt er gewerkt aan ondersteuning voor dynamische queries. En vergeet niet een vorm van authenticatie aan je endpoint toe te voegen; simple_oauth werkt prima.
Maar waar zijn mijn afbeeldingen?
Toen ik eerder gatsby-source-drupal gebruikte, was ik gewend aan de geweldige mogelijkheden van childImageSharp voor afbeeldingen uit Drupal. Maar met de GraphQL-source-plugin ontbraken die GraphQL-velden op mijn afbeeldingsvelden. Afbeeldingen uit Drupal worden namelijk niet automatisch beschikbaar in Gatsby GraphQL als je gatsby-source-graphql gebruikt. Er wordt gewerkt aan het herschrijven van die plugin, hopelijk ook met een betere manier om schema's van derden uit te breiden. Voorlopig kun je de afbeeldingsvelden eenvoudig toevoegen via een andere Gatsby-API: createResolvers.
gatsby-node.js
exports.createResolvers = ( { actions, cache, createNodeId, createResolvers, store, reporter }, ) => { 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-mutaties bieden uitkomst
Ik wist dat webforms een uitdaging zouden worden. Aanvankelijk bouwde en hostte ik de site op Netlify en dacht ik hun formulierondersteuning te gebruiken. Die is makkelijk te implementeren en werkt goed. Maar het gratis abonnement heeft beperkingen en ik wilde ook dat redacteuren de inzendingen in Drupal konden bekijken, zoals ze gewend zijn. Er zijn oplossingen zoals gatsby-drupal-webform, maar die gebruiken REST en JSON API, en ik wilde het bij mijn GraphQL-endpoint houden. In de kanalen #gatsby en #graphql op de Drupal-Slack merkte ik dat ik niet de enige was die hiermee bezig was. Uiteindelijk gebruikte ik de Drupal-module graphql_webform, met Formik/Yup aan de React-kant. Op de contactpagina kun je een formulier in actie zien. Hiermee kun je specifieke formulieren en velden ophalen en ze versturen via een GraphQL-mutatie. In mijn geval staat de formulierconfiguratie nog wel hardcoded in specifieke Gatsby-componenten. Ik ben van plan een plugin te maken die Drupal-webforms automatisch in Gatsby opbouwt.
Ik leerde de Context API te gebruiken voor i18n
Weet je nog: React begrijpen? Op een gegeven moment moeten componenten met elkaar communiceren en zijn props niet genoeg. Dan wordt de Context API erg handig. Voor een meertalige site moest ik de huidige taal opslaan, in mijn geval op basis van het URL-prefix, zodat componenten weten welke taal ze voor vertaalde teksten moeten gebruiken. Zoals t() in Drupal werkt. Ik gebruikte de Context API om de taal op te slaan en i18next rechtstreeks aan te spreken, zonder extra frameworks zoals react-i18next, die voor mijn toepassing overbodig lijken.
import React, { useState } from 'react'; import i18next from "../i18n/config"; const SiteContext = React.createContext({}); const Provider = props => { const languages = { en: 'English', nl: 'Dutch' }; const currentLanguageByPath = () => Object.keys(languages).find(langcode => props.path.startsWith(`/${langcode}`)) ?? 'en'; i18next.changeLanguage(currentLanguageByPath()); return ( {props.children} ); }; export default ({ element }) => ( {element} ); export { SiteContext };
Door deze context via wrapPageElement in gatsby-ssr.js en gatsby-browser.js toe te voegen, kunnen we het i18next-object vanuit elk component ophalen:
import { SiteContext } from "../../context/site-context"; const MyComponent = () => { const siteContext = useContext(SiteContext); const i18next = siteContext.i18next; return (
{`${i18next.t('Current language')}: ${i18next.language}`}
); };
Dit wordt extra krachtig zodra we dynamische queries met variabelen in componenten kunnen gebruiken, zoals eerder genoemd. Vertalingen van teksten kun je trouwens in JSON-bestanden opslaan, vergelijkbaar met Drupal's .po-bestanden. Zie de documentatie van i18next.
Alles optimaliseren
Ik ben nog allerlei dingen aan het afstellen. Gatsby is standaard al behoorlijk snel, maar voor een Lighthouse-score van bijna 100 moet je op detailniveau optimaliseren. Ik koos ervoor om niet alle CSS in de head te zetten (ik bedoel, wtf) en extra breakpoints voor afbeeldingen toe te voegen. Er staan nog andere optimalisaties op de backlog. Ook breidde ik mijn helmet/meta-component uit met SEO-verbeteringen voor social cards. Er is nog een lijstje kleinere punten, maar voorlopig ben ik erg blij met het resultaat.
Over de auteur
- Albert Skibinski is product engineer en co-founder van Jafix.
- Ik schrijf over web development, lange fietstochten en lekker eten!
Misschien ook interessant