Ga naar inhoud

Inline Drupal-media in Gatsby

Albert Skibinski •
Een mooi weggetje

Dit blog is gebouwd met Gatsby en de content komt uit Drupal 8. Ik heb veel geleerd tijdens het bouwen, maar er stond nog iets op mijn backlog: inline Drupal-media weergeven in de Gatsby-frontend. Daar is Drupal 8.8. Een van de mooie verbeteringen in 8.8 is een mediabibliotheek en de integratie daarvan, waarmee redacteuren makkelijk media kunnen invoegen. Hoera! Precies wat ik nodig heb om eenvoudiger afbeeldingen toe te voegen:

Meeko
De hond van mijn ouders — een grote witte herder (ook handig als whiteboard)

Een alternatief zou zijn om Paragraphs te gebruiken en daarvoor een integratie te bouwen. Dat kan ook, maar voor mijn blog wilde ik alles zo eenvoudig mogelijk houden. Paragraphs is heel flexibel, maar brengt ook veel overhead en complexiteit mee. Hoe pakken we dit aan? Tot nu toe gebruikte ik gewoon de `processed`-waarde uit Drupal. Dat was redelijk veilig zolang aan de Drupal-kant de juiste tekstformaatfilters werden gebruikt.

<div className="page-body" dangerouslySetInnerHTML={{ __html: entity.body.processed }}></div>

Drupal biedt ook een filter om media in te sluiten. Dit filter doorzoekt de HTML op mediatags en zet ze om naar het juiste media-item. Voor de hond hierboven ziet de ruwe waarde in de editor er zo uit:

<drupal-media data-align="center" data-caption="My parents dog - a great white shepherd (also great as whiteboard)" data-entity-type="media" data-entity-uuid="16954f2e-e03c-4d00-85a6-5be1cde42833" data-view-mode=""></drupal-media>

Maar als we de verwerkte waarde blijven gebruiken, wordt die in Drupal vervangen door een afbeeldings-URL en verliest Gatsby de verwijzing naar het media-item. Het idee is dus om de ruwe waarde uit het tekstveld te gebruiken en onze eigen filters toe te passen voor het opschonen en omzetten. Je haalt de ruwe waarde eenvoudig op door die aan je GraphQL-query toe te voegen:

            ...
            body {
                processed
                value
            }
            ...       

Er blijkt een handige parser te zijn: react-html-parser. Die helpt niet alleen bij het omzetten van de media, maar maakt ook dangerouslySetInnerHTML overbodig. Mooi! Ik besloot een herbruikbaar Body-component te maken dat alle logica bevat. Daarnaast maakte ik een Media-component en een component voor elk type mediabundel dat ik gebruik (image, video, remote-video, enzovoort).

body.js


import React from "react";
import { useStaticQuery, graphql } from "gatsby";
import ReactHtmlParser from "react-html-parser";
import sanitizeHtml from "sanitize-html";
import Media from "../media/media";

const Body = (props) => {
    const mediaItems = useStaticQuery(graphql`
        query mediaQuery {
            drupal {
                mediaQuery {
                    entities {
                        entityId
                        entityBundle
                        entityUuid
                        ... on Drupal_MediaImage {
                            mid
                            uuid
                            fieldMediaImage {
                                mediaGatsbyFile {
                                    publicURL
                                    childImageSharp {
                                        fluid {
                                            ...GatsbyImageSharpFluid
                                        }
                                    }
                                }
                                entity {
                                    url
                                }
                            }
                        }
                        ... on Drupal_MediaVideo {
                            mid
                            uuid
                            fieldMediaVideoFile {
                                mediaGatsbyFile {
                                    publicURL
                                }
                                entity {
                                    url
                                }
                            }
                        }
                        ... on Drupal_MediaRemoteVideo {
                            mid
                            uuid
                            fieldMediaOembedVideo
                        }
                    }
                }
            }
        }
    `);

    const processBody = (body) => {
        body = sanitizeHtml(body, {
            allowedTags: sanitizeHtml.defaults.allowedTags.concat(['drupal-media']),
            allowedAttributes: Object.assign(
                { 'drupal-media': ['data-*'] },
                sanitizeHtml.defaults.allowedAttributes
            ),
        });

        return ReactHtmlParser(body, {
            transform: (node) => {
                if (node.type === 'tag' && node.attribs['data-entity-type'] === 'media') {
                    let uuid = node.attribs['data-entity-uuid'];
                    for (let media of mediaItems.drupal.mediaQuery.entities) {
                        if (media.uuid === uuid) {
                            return ;
                        }
                    }
                } else {
                    return undefined;
                }
            },
        });
    };

    return {processBody(props.children)};
};

export default Body;
        

Eerst schoon ik de data op, omdat we nu de ruwe waarde gebruiken. sanitizeHTML is hier dus het equivalent van het Drupal-filter 'Toegestane HTML-tags beperken en foutieve HTML corrigeren'. Ik hoefde alleen drupal-media en de bijbehorende attributen aan de lijst met toegestane tags toe te voegen, anders zouden ze worden verwijderd.

react-html-parser voorkomt geen XSS — schoon je data op.

Vervolgens gebruiken we ReactHtmlParser. Het tweede argument is een object met twee beschikbare functies: preprocessNodes en transform. Ik gebruik alleen de transform-functie. Die zoekt mediatags door het attribuut data-entity-type te controleren. Als dat gevonden wordt, haalt de functie de UUID uit data-entity-uuid en gebruikt die ID om in de opgevraagde mediaItems een overeenkomst te vinden.

Let op: in de query hierboven vraag ik alleen afbeeldingen, video's en externe video's op, omdat ik geen documenten of audio nodig had. Als er een overeenkomst is, wordt het media-entiteitsobject samen met de attributen doorgegeven aan het Media-component. Die attributen bevatten onder meer bijschriften, alternatieve tekst, uitlijning en view-mode. Alles wordt teruggegeven als een verzameling React-elementen. Daarom hebben we dangerouslySetInnerHTML niet meer nodig.

Bij een afbeelding zoals hierboven wordt het media-image-component gebruikt. Met de data uit de fluid-query kunnen we Gatsby's Img-component gebruiken:

const MediaImage = (props) => {
    const { attr, fluid } = props;
    return (            
            {attr['data-caption'] && {attr['data-caption']}}
    );
}; 

Ik gebruik zelf geen view modes, maar het zou makkelijk zijn om verschillende weergaven op basis van de attribuutwaarde te implementeren.

Je vraagt je misschien af waarom we niet één specifiek media-item opvragen, aangezien we de ID kennen. Dat noemde ik ook in het andere blogartikel: het is een beperking van staticQueries, die geen variabelen kunnen gebruiken. Waarschijnlijk wordt dat in de toekomst wel mogelijk.

Laat weten wat je ervan vindt, of als je een betere manier hebt om inline media-afbeeldingen uit Drupal in Gatsby te tonen!

Albert Skibinski

Over de auteur

  • Albert Skibinski is product engineer en co-founder van Jafix.
  • Ik schrijf over web development, lange fietstochten en lekker eten!